Skip to content

Commit 0167c7e

Browse files
feat: add plugin support for the react component (#1185)
Co-authored-by: acethecreator <devaze007@gmail.com>
1 parent 7ea711b commit 0167c7e

File tree

17 files changed

+1251
-27
lines changed

17 files changed

+1251
-27
lines changed

docs/features/plugins.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Plugin System
2+
3+
The AsyncAPI React component supports a flexible plugin system to extend and customize your documentation.
4+
5+
## Usage
6+
7+
### Static Registration (via props)
8+
9+
Use this when you know all plugins upfront:
10+
11+
```typescript
12+
import { AsyncApiPlugin, PluginAPI, PluginSlot } from '@asyncapi/react-component';
13+
14+
const myPlugin: AsyncApiPlugin = {
15+
name: 'my-plugin',
16+
version: '1.0.0',
17+
install(api: PluginAPI) {
18+
api.registerComponent(PluginSlot.OPERATION, MyComponent);
19+
api.onSpecLoaded((spec) => console.log('Spec loaded:', spec));
20+
}
21+
};
22+
23+
<AsyncApi schema={mySchema} plugins={[myPlugin]} />
24+
```
25+
26+
### Dynamic Registration
27+
28+
Use this when you need to add/remove plugins at runtime:
29+
30+
```typescript
31+
import { useState } from 'react';
32+
33+
function MyApp() {
34+
const [pluginManager, setPluginManager] = useState(null);
35+
36+
const handleEnablePlugin = () => {
37+
pluginManager?.register(myPlugin);
38+
};
39+
40+
const handleDisablePlugin = () => {
41+
pluginManager?.unregister('my-plugin');
42+
};
43+
44+
return (
45+
<>
46+
<button onClick={handleEnablePlugin}>Enable Plugin</button>
47+
<button onClick={handleDisablePlugin}>Disable Plugin</button>
48+
<AsyncApi
49+
schema={mySchema}
50+
onPluginManagerReady={(pm) => setPluginManager(pm)}
51+
/>
52+
</>
53+
);
54+
}
55+
```
56+
57+
## Plugin Structure
58+
59+
```typescript
60+
interface AsyncApiPlugin {
61+
name: string; // Unique identifier
62+
version: string; // Semantic version
63+
description?: string; // Optional description
64+
install(api: PluginAPI): void;
65+
}
66+
```
67+
68+
## PluginAPI Methods
69+
70+
| Method | Purpose |
71+
|--------|---------|
72+
| `registerComponent(slot, component, options?)` | Register a React component in a slot. `options`: `{ priority?: number; label?: string }` |
73+
| `onSpecLoaded(callback)` | Called when AsyncAPI spec loads |
74+
| `getContext()` | Get current plugin context with schema |
75+
| `on(eventName, callback)` | Subscribe to events |
76+
| `off(eventName, callback)` | Unsubscribe from events |
77+
| `emit(eventName, data)` | Emit custom events |
78+
79+
## Component Props
80+
81+
```typescript
82+
interface ComponentSlotProps {
83+
context: PluginContext;
84+
onClose?: () => void;
85+
}
86+
87+
const MyComponent: React.FC<ComponentSlotProps> = ({ context, onClose }) => (
88+
<div>Custom content here</div>
89+
);
90+
```
91+
92+
## Available Slots
93+
94+
- `PluginSlot.OPERATION` - Renders within operation sections
95+
- `PluginSlot.INFO` - Renders within info section

library/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@
9595
"@types/node": "^18.0.0",
9696
"@types/react": "^18.2.79",
9797
"@types/react-dom": "^18.2.25",
98+
"@types/testing-library__jest-dom": "^5.14.9",
9899
"autoprefixer": "^10.2.5",
99100
"cross-env": "^7.0.3",
100101
"cssnano": "^4.1.11",

library/src/__tests__/index.test.tsx

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import krakenMultipleChannels from './docs/v3/kraken-websocket-request-reply-mul
1313
import streetlightsKafka from './docs/v3/streetlights-kafka.json';
1414
import streetlightsMqtt from './docs/v3/streetlights-mqtt.json';
1515
import websocketGemini from './docs/v3/websocket-gemini.json';
16+
import { PluginAPI, PluginSlot } from '../types';
1617

1718
jest.mock('use-resize-observer', () => ({
1819
__esModule: true,
@@ -266,4 +267,51 @@ describe('AsyncAPI component', () => {
266267
expect(result.container.querySelector('#custom-extension')).toBeDefined();
267268
});
268269
});
270+
271+
test('should work with plugin registration', async () => {
272+
const TestPluginComponent = () => (
273+
<div data-testid="plugin-component">Test Plugin Rendered</div>
274+
);
275+
276+
const testPlugin = {
277+
name: 'test-plugin',
278+
version: '1.0.0',
279+
install: (api: PluginAPI) => {
280+
api.registerComponent(PluginSlot.OPERATION, TestPluginComponent);
281+
},
282+
};
283+
284+
const schema = {
285+
asyncapi: '2.0.0',
286+
info: {
287+
title: 'Test API with Plugins',
288+
version: '1.0.0',
289+
},
290+
channels: {
291+
'test/channel': {
292+
subscribe: {
293+
message: {
294+
payload: {
295+
type: 'object',
296+
properties: {
297+
id: { type: 'string' },
298+
},
299+
},
300+
},
301+
},
302+
},
303+
},
304+
};
305+
306+
const result = render(
307+
<AsyncApiComponent schema={schema} plugins={[testPlugin]} />,
308+
);
309+
310+
await waitFor(() => {
311+
expect(result.container.querySelector('#introduction')).toBeDefined();
312+
const pluginComponent = result.getByTestId('plugin-component');
313+
expect(pluginComponent).toBeDefined();
314+
expect(pluginComponent.textContent).toContain('Test Plugin Rendered');
315+
});
316+
});
269317
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { PluginManager } from '../helpers/pluginManager';
3+
import { PluginContext, PluginSlot } from '../types';
4+
5+
interface SlotRendererProps {
6+
slot: PluginSlot;
7+
context: PluginContext;
8+
pluginManager?: PluginManager;
9+
}
10+
11+
const SlotRenderer: React.FC<SlotRendererProps> = ({
12+
slot,
13+
context,
14+
pluginManager,
15+
}) => {
16+
if (!pluginManager) {
17+
return null;
18+
}
19+
20+
const components = pluginManager.getComponentsForSlot(slot);
21+
22+
if (!components || components.length === 0) {
23+
return null;
24+
}
25+
26+
return (
27+
<div className={`asyncapi-react-plugin-slot-${slot}`} data-slot={slot}>
28+
{components.map((Component, index) => (
29+
<React.Suspense
30+
key={`${slot}-${index}`}
31+
fallback={<div>Loading plugin...</div>}
32+
>
33+
<Component context={context} />
34+
</React.Suspense>
35+
))}
36+
</div>
37+
);
38+
};
39+
40+
export { SlotRenderer };

0 commit comments

Comments
 (0)