Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/features/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Plugin System

The AsyncAPI React component supports a flexible plugin system to extend and customize your documentation.

> Currently supports Operation level slots only.

## Usage

### Static Registration (via props)

Use this when you know all plugins upfront:

```typescript
import { AsyncApiPlugin, PluginAPI, PluginSlot } from '@asyncapi/react-component';

const myPlugin: AsyncApiPlugin = {
name: 'my-plugin',
version: '1.0.0',
install(api: PluginAPI) {
api.registerComponent(PluginSlot.OPERATION, MyComponent);
api.onSpecLoaded((spec) => console.log('Spec loaded:', spec));
}
};

<AsyncApi schema={mySchema} plugins={[myPlugin]} />
```

### Dynamic Registration

Use this when you need to add/remove plugins at runtime:

```typescript
import { useState } from 'react';

function MyApp() {
const [pluginManager, setPluginManager] = useState(null);

const handleEnablePlugin = () => {
pluginManager?.register(myPlugin);
};

const handleDisablePlugin = () => {
pluginManager?.unregister('my-plugin');
};

return (
<>
<button onClick={handleEnablePlugin}>Enable Plugin</button>
<button onClick={handleDisablePlugin}>Disable Plugin</button>
<AsyncApi
schema={mySchema}
onPluginManagerReady={(pm) => setPluginManager(pm)}
/>
</>
);
}
```

## Plugin Structure

```typescript
interface AsyncApiPlugin {
name: string; // Unique identifier
version: string; // Semantic version
description?: string; // Optional description
install(api: PluginAPI): void;
}
```

## PluginAPI Methods

| Method | Purpose |
|--------|---------|
| `registerComponent(slot, component, options?)` | Register a React component in a slot. `options`: `{ priority?: number; label?: string }` |
| `onSpecLoaded(callback)` | Called when AsyncAPI spec loads |
| `getContext()` | Get current plugin context with schema |
| `on(eventName, callback)` | Subscribe to events |
| `off(eventName, callback)` | Unsubscribe from events |
| `emit(eventName, data)` | Emit custom events |

## Component Props

```typescript
interface ComponentSlotProps {
context: PluginContext;
onClose?: () => void;
}

const MyComponent: React.FC<ComponentSlotProps> = ({ context, onClose }) => (
<div>Custom content here</div>
);
```

## Available Slots

- `PluginSlot.OPERATION` - Renders within operation sections
1 change: 1 addition & 0 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"@types/node": "^18.0.0",
"@types/react": "^18.2.79",
"@types/react-dom": "^18.2.25",
"@types/testing-library__jest-dom": "^5.14.9",
"autoprefixer": "^10.2.5",
"cross-env": "^7.0.3",
"cssnano": "^4.1.11",
Expand Down
48 changes: 48 additions & 0 deletions library/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import krakenMultipleChannels from './docs/v3/kraken-websocket-request-reply-mul
import streetlightsKafka from './docs/v3/streetlights-kafka.json';
import streetlightsMqtt from './docs/v3/streetlights-mqtt.json';
import websocketGemini from './docs/v3/websocket-gemini.json';
import { PluginAPI, PluginSlot } from '../types';

jest.mock('use-resize-observer', () => ({
__esModule: true,
Expand Down Expand Up @@ -266,4 +267,51 @@ describe('AsyncAPI component', () => {
expect(result.container.querySelector('#custom-extension')).toBeDefined();
});
});

test('should work with plugin registration', async () => {
const TestPluginComponent = () => (
<div data-testid="plugin-component">Test Plugin Rendered</div>
);

const testPlugin = {
name: 'test-plugin',
version: '1.0.0',
install: (api: PluginAPI) => {
api.registerComponent(PluginSlot.OPERATION, TestPluginComponent);
},
};

const schema = {
asyncapi: '2.0.0',
info: {
title: 'Test API with Plugins',
version: '1.0.0',
},
channels: {
'test/channel': {
subscribe: {
message: {
payload: {
type: 'object',
properties: {
id: { type: 'string' },
},
},
},
},
},
},
};

const result = render(
<AsyncApiComponent schema={schema} plugins={[testPlugin]} />,
);

await waitFor(() => {
expect(result.container.querySelector('#introduction')).toBeDefined();
const pluginComponent = result.getByTestId('plugin-component');
expect(pluginComponent).toBeDefined();
expect(pluginComponent.textContent).toContain('Test Plugin Rendered');
});
});
});
40 changes: 40 additions & 0 deletions library/src/components/PluginSlotRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { PluginManager } from '../helpers/pluginManager';
import { PluginContext, PluginSlot } from '../types';

interface SlotRendererProps {
slot: PluginSlot;
context: PluginContext;
pluginManager?: PluginManager;
}

const SlotRenderer: React.FC<SlotRendererProps> = ({
slot,
context,
pluginManager,
}) => {
if (!pluginManager) {
return null;
}

const components = pluginManager.getComponentsForSlot(slot);

if (!components || components.length === 0) {
return null;
}

return (
<div className={`asyncapi-react-plugin-slot-${slot}`} data-slot={slot}>
{components.map((Component, index) => (
<React.Suspense
key={`${slot}-${index}`}
fallback={<div>Loading plugin...</div>}
>
<Component context={context} />
</React.Suspense>
))}
</div>
);
};

export { SlotRenderer };
Loading