Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: custom extension rendering #994

Open
wants to merge 36 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
db17606
Custom extension value render
Apr 19, 2024
a52c230
Merge branch 'master' into feat/custom-extension-value-render
asyncapi-bot Apr 24, 2024
22f07b3
Add parent to ExtensionComponentProps and move changes to Extensions …
Apr 25, 2024
18654d2
Merge remote-tracking branch 'origin/feat/custom-extension-value-rend…
Apr 25, 2024
9b978b8
Address Sonar issue
Apr 25, 2024
de451e8
Merge branch 'master' into feat/custom-extension-value-render
ductaily Apr 30, 2024
13a8a6c
Apply suggestions from code review
ductaily Apr 30, 2024
23401a9
Clean up Extension class
Apr 30, 2024
6c4f19f
Merge branch 'refs/heads/master' into feat/custom-extension-value-render
May 2, 2024
37bc952
Merge branch 'master' into feat/custom-extension-value-render
ductaily May 2, 2024
3b19209
Merge remote-tracking branch 'origin/feat/custom-extension-value-rend…
May 2, 2024
95bfa86
Merge branch 'master' into feat/custom-extension-value-render
derberg May 13, 2024
8492e7a
Merge branch 'master' into feat/custom-extension-value-render
asyncapi-bot May 13, 2024
389e407
Merge remote-tracking branch 'origin/feat/custom-extension-value-rend…
May 13, 2024
973c1da
Address eslint errors
May 13, 2024
4b03c14
Fix handling of concatenatedConfig for extensions
May 21, 2024
3b548f5
Add config modification docs
May 21, 2024
cae5861
Add test
May 22, 2024
8caea6e
Add example for x-x extension
May 22, 2024
f986398
Add viewBox to x logo
May 22, 2024
979777d
Merge branch 'master' into feat/custom-extension-value-render
ductaily Jun 12, 2024
a34f50a
Update package-lock.json
Jun 19, 2024
32cf262
Fix lint error
Jun 19, 2024
c630f89
Merge branch 'master' into feat/custom-extension-value-render
pavelkornev Jun 19, 2024
eed70c4
Merge branch 'refs/heads/master' into feat/custom-extension-value-render
Jul 23, 2024
350646a
Merge remote-tracking branch 'origin/feat/custom-extension-value-rend…
Jul 23, 2024
d91df2a
Merge branch 'master' into feat/custom-extension-value-render
ductaily Jul 31, 2024
3e57a3f
Add x-x extensions example.
Jul 31, 2024
1740980
Merge branch 'master' into feat/custom-extension-value-render
asyncapi-bot Jul 31, 2024
361a2c2
Merge branch 'master' into feat/custom-extension-value-render
ductaily Sep 2, 2024
aa7423b
Merge branch 'master' into feat/custom-extension-value-render
pavelkornev Sep 2, 2024
8384cd8
Merge branch 'master' into feat/custom-extension-value-render
Dec 16, 2024
3563ab1
Add x-x extensions example in info component
Dec 17, 2024
df932da
Merge branch 'master' into feat/custom-extension-value-render
AceTheCreator Dec 19, 2024
a25f1d2
Address review comments
Dec 19, 2024
ec52216
Merge remote-tracking branch 'origin/feat/custom-extension-value-rend…
Dec 19, 2024
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
38 changes: 37 additions & 1 deletion docs/configuration/config-modification.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface ConfigInterface {
receiveLabel?: string;
requestLabel?: string;
replyLabel?: string;
extensions?: Record<string, React.ComponentType<ExtensionComponentProps>>;
}
```

Expand Down Expand Up @@ -100,6 +101,11 @@ interface ConfigInterface {
This field contains configuration responsible for customizing the label for response operation. This takes effect when rendering AsyncAPI v3 documents.
This field is set to `REPLY` by default.

- **extensions?: Record<string, React.ComponentType<ExtensionComponentProps>>**

This field contains configuration responsible for adding custom extension components.
This field will contain default components.

## Examples

See exemplary component configuration in TypeScript and JavaScript.
Expand All @@ -110,6 +116,7 @@ See exemplary component configuration in TypeScript and JavaScript.
import * as React from "react";
import { render } from "react-dom";
import AsyncAPIComponent, { ConfigInterface } from "@asyncapi/react-component";
import CustomExtension from "./CustomExtension";

import { schema } from "./mock";

Expand All @@ -126,13 +133,28 @@ const config: ConfigInterface = {
expand: {
messageExamples: false,
},
extensions: {
'x-custom-extension': CustomExtension
}
};

const App = () => <AsyncAPIComponent schema={schema} config={config} />;

render(<App />, document.getElementById("root"));
```

```tsx
// CustomExtension.tsx
import { ExtensionComponentProps } from '@asyncapi/react-component/lib/types/components/Extensions';

export default function CustomExtension(props: ExtensionComponentProps<string>) {
return <div>
<h1>{props.propertyName}</h1>
<p>{props.propertyValue}</p>
</div>
}
```

### JavaScript

```jsx
Expand Down Expand Up @@ -162,6 +184,16 @@ const App = () => <AsyncAPIComponent schema={schema} config={config} />;
render(<App />, document.getElementById("root"));
```

```jsx
// CustomExtension.jsx
export default function CustomExtension(props) {
return <div>
<h1>{props.propertyName}</h1>
<p>{props.propertyValue}</p>
</div>
}
```

In the above examples, after concatenation with the default configuration, the resulting configuration looks as follows:

```js
Expand All @@ -188,6 +220,10 @@ In the above examples, after concatenation with the default configuration, the r
sendLabel: 'SEND',
receiveLabel: 'RECEIVE',
requestLabel: 'REQUEST',
replyLabel: 'REPLY'
replyLabel: 'REPLY',
extensions: {
// default extensions...
'x-custom-extension': CustomExtension
}
}
```
52 changes: 51 additions & 1 deletion library/src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import React from 'react';
import { render, waitFor } from '@testing-library/react';
import AsyncApiComponent from '..';
import AsyncApiComponent, { ExtensionComponentProps } from '..';
import adoaKafka from './docs/v3/adeo-kafka-request-reply.json';
import krakenMessageFilter from './docs/v3/kraken-websocket-request-reply-message-filter-in-reply.json';
import krakenMultipleChannels from './docs/v3/kraken-websocket-request-reply-multiple-channels.json';
Expand Down Expand Up @@ -216,4 +216,54 @@ describe('AsyncAPI component', () => {
expect(result.container.querySelector('#introduction')).toBeDefined(),
);
});

test('should work with custom extensions', async () => {
const schema = {
asyncapi: '2.0.0',
info: {
title: 'Example AsyncAPI',
version: '0.1.0',
},
channels: {
'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured':
{
subscribe: {
message: {
$ref: '#/components/messages/lightMeasured',
},
},
},
},
components: {
messages: {
lightMeasured: {
name: 'lightMeasured',
title: 'Light measured',
contentType: 'application/json',
'x-custom-extension': 'Custom extension value',
},
},
},
};

const CustomExtension = (props: ExtensionComponentProps) => (
<div id="custom-extension">{props.propertyValue}</div>
);

const result = render(
<AsyncApiComponent
schema={schema}
config={{
extensions: {
'x-custom-extension': CustomExtension,
},
}}
/>,
);

await waitFor(() => {
expect(result.container.querySelector('#introduction')).toBeDefined();
expect(result.container.querySelector('#custom-extension')).toBeDefined();
});
});
});
68 changes: 62 additions & 6 deletions library/src/components/Extensions.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */

import React from 'react';
import React, { useState } from 'react';

import { Schema } from './Schema';

import { SchemaHelpers } from '../helpers';
import { useConfig, useSpec } from '../contexts';
import { CollapseButton } from './CollapseButton';

interface Props {
name?: string;
Expand All @@ -17,18 +19,72 @@ export const Extensions: React.FunctionComponent<Props> = ({
name = 'Extensions',
item,
}) => {
const [expanded, setExpanded] = useState(false);

const config = useConfig();
const document = useSpec();

const extensions = SchemaHelpers.getCustomExtensions(item);
if (!extensions || !Object.keys(extensions).length) {
return null;
}

const schema = SchemaHelpers.jsonToSchema(extensions);
if (!config.extensions || !Object.keys(config.extensions).length) {
const schema = SchemaHelpers.jsonToSchema(extensions);
return (
schema && (
<div className="mt-2">
<Schema schemaName={name} schema={schema} onlyTitle={true} />
</div>
)
);
}

return (
schema && (
<div className="mt-2">
<Schema schemaName={name} schema={schema} onlyTitle />
<div>
<div className="flex py-2">
<div className="min-w-1/4">
<>
<CollapseButton
onClick={() => setExpanded((prev) => !prev)}
expanded={expanded}
>
<span className={`break-anywhere text-sm ${name}`}>{name}</span>
</CollapseButton>
</>
</div>
</div>
<div
className={`rounded p-4 py-2 border bg-gray-100 ${expanded ? 'block' : 'hidden'}`}
>
{Object.keys(extensions)
.sort((extension1, extension2) =>
extension1.localeCompare(extension2),
)
.map((extensionKey) => {
if (config.extensions?.[extensionKey]) {
const CustomExtensionComponent = config.extensions[extensionKey];
return (
<CustomExtensionComponent
key={extensionKey}
propertyName={extensionKey}
propertyValue={extensions[extensionKey]}
document={document}
parent={item}
/>
);
} else {
const extensionSchema = SchemaHelpers.jsonToSchema(
extensions[extensionKey],
);
return (
<div key={extensionKey} className="mt-2">
<Schema schemaName={extensionKey} schema={extensionSchema} />
</div>
);
}
})}
</div>
)
</div>
);
};
36 changes: 36 additions & 0 deletions library/src/components/supportedExtensions/XExtension.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';

import { ExtensionComponentProps } from '../../types';

/**
* See <https://github.com/asyncapi/extensions-catalog/blob/master/extensions/x.md>.
*/
export default function XExtension({
propertyValue,
}: ExtensionComponentProps<string>) {
const onClickHandler = () => {
window.open(`https://x.com/${propertyValue}`, '_blank');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for best practices we should include the noopener,noreferrer attribute

};

return (
<div
title={`https://x.com/${propertyValue}`}
style={{ display: 'inline-block' }}
>
<svg
onClick={onClickHandler}
style={{ cursor: 'pointer' }}
width="15px"
height="15px"
viewBox="0 0 1200 1227"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M714.163 519.284L1160.89 0H1055.03L667.137 450.887L357.328 0H0L468.492 681.821L0 1226.37H105.866L515.491 750.218L842.672 1226.37H1200L714.137 519.284H714.163ZM569.165 687.828L521.697 619.934L144.011 79.6944H306.615L611.412 515.685L658.88 583.579L1055.08 1150.3H892.476L569.165 687.854V687.828Z"
fill="black"
/>
</svg>
</div>
Comment on lines +16 to +34
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity why aren't we wrapping it an anchor tag?

);
}
3 changes: 3 additions & 0 deletions library/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ExtensionComponentProps } from '../types';

export interface ConfigInterface {
schemaID?: string;
show?: ShowConfig;
Expand All @@ -11,6 +13,7 @@ export interface ConfigInterface {
receiveLabel?: string;
requestLabel?: string;
replyLabel?: string;
extensions?: Record<string, React.ComponentType<ExtensionComponentProps>>;
}

export interface ShowConfig {
Expand Down
4 changes: 4 additions & 0 deletions library/src/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
SEND_LABEL_DEFAULT_TEXT,
SUBSCRIBE_LABEL_DEFAULT_TEXT,
} from '../constants';
import XExtension from '../components/supportedExtensions/XExtension';

export const defaultConfig: ConfigInterface = {
schemaID: '',
Expand All @@ -33,4 +34,7 @@ export const defaultConfig: ConfigInterface = {
receiveLabel: RECEIVE_TEXT_LABEL_DEFAULT_TEXT,
requestLabel: REQUEST_LABEL_DEFAULT_TEXT,
replyLabel: REPLIER_LABEL_DEFAULT_TEXT,
extensions: {
'x-x': XExtension,
},
};
4 changes: 4 additions & 0 deletions library/src/containers/AsyncApi/Standalone.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ class AsyncApiComponent extends Component<AsyncApiProps, AsyncAPIState> {
...defaultConfig.sidebar,
...(!!config && config.sidebar),
},
extensions: {
...defaultConfig.extensions,
...(!!config && config.extensions),
},
};

if (!asyncapi) {
Expand Down
9 changes: 8 additions & 1 deletion library/src/containers/Info/Info.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { Href, Markdown, Tags } from '../../components';
import { Extensions, Href, Markdown, Tags } from '../../components';
import { useSpec } from '../../contexts';
import {
TERMS_OF_SERVICE_TEXT,
Expand All @@ -23,6 +23,7 @@ export const Info: React.FunctionComponent = () => {
const termsOfService = info.termsOfService();
const defaultContentType = asyncapi.defaultContentType();
const contact = info.contact();
const extensions = info.extensions();

const showInfoList =
license ?? termsOfService ?? defaultContentType ?? contact ?? externalDocs;
Expand Down Expand Up @@ -128,6 +129,12 @@ export const Info: React.FunctionComponent = () => {
<Tags tags={asyncapi.info().tags()} />
</div>
)}

{extensions.length > 0 && (
<div className="mt-4">
<Extensions name="Info Extensions" item={info} />
</div>
)}
</div>

<div className="panel-item--right" />
Expand Down
2 changes: 1 addition & 1 deletion library/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import AsyncApiComponentWP from './containers/AsyncApi/Standalone';

export { AsyncApiProps } from './containers/AsyncApi/AsyncApi';
export { ConfigInterface } from './config/config';
export { FetchingSchemaInterface } from './types';
export { FetchingSchemaInterface, ExtensionComponentProps } from './types';

import { hljs } from './helpers';

Expand Down
10 changes: 9 additions & 1 deletion library/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AsyncAPIDocumentInterface } from '@asyncapi/parser';
import { AsyncAPIDocumentInterface, BaseModel } from '@asyncapi/parser';

export type PropsSchema =
| string
Expand Down Expand Up @@ -77,3 +77,11 @@ export interface ErrorObject {
endOffset: number;
}[];
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export interface ExtensionComponentProps<V = any> {
propertyName: string;
propertyValue: V;
document: AsyncAPIDocumentInterface;
parent: BaseModel;
}
1 change: 1 addition & 0 deletions playground/specs/streetlights.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const streetlights = `asyncapi: '2.6.0'
id: 'urn:com:smartylighting:streetlights:server'
info:
x-x: AsyncAPISpec
title: Streetlights API
version: '1.0.0'
description: |
Expand Down
Loading