-
-
Notifications
You must be signed in to change notification settings - Fork 126
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
base: master
Are you sure you want to change the base?
feat: custom extension rendering #994
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Welcome to AsyncAPI. Thanks a lot for creating your first pull request. Please check out our contributors guide useful for opening a pull request.
Keep in mind there are also other channels you can use to interact with AsyncAPI community. For more details check out this issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume that docs, test and playground showcase is not here yet as you first what to have us look at implementation to give green light for more?
than please at least extend PR with default extensions components that are supported by react component by default? probably best if they are in new dir like /library/src/components/extensions
. You can take x-x
and x-linkedin
as first official extensions under info
object
@ductaily Hi! Great work, but you shouldn't override the logic for |
Hi, thank you for reviewing. import React, { useContext, useState } from 'react';
import { Schema } from './Schema';
import { SchemaHelpers } from '../helpers';
import { AsyncAPIDocumentInterface } from '@asyncapi/parser';
import { useConfig, useSpec } from '../contexts';
import { CollapseButton } from './CollapseButton';
interface Props {
name?: string;
item: any;
}
export interface ExtensionComponentProps<V = any> {
propertyName: string;
propertyValue: V;
document: AsyncAPIDocumentInterface;
}
const SchemaContext = React.createContext({
reverse: false,
});
export const Extensions: React.FunctionComponent<Props> = ({
name = 'Extensions',
item,
}) => {
const { reverse } = useContext(SchemaContext);
const [expanded, setExpanded] = useState(false);
const [deepExpand, setDeepExpand] = useState(false);
const config = useConfig();
const document = useSpec();
const extensions = SchemaHelpers.getCustomExtensions(item);
if (!extensions || !Object.keys(extensions).length) {
return null;
}
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 (
<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>
<button
type="button"
onClick={() => setDeepExpand(prev => !prev)}
className="ml-1 text-sm text-gray-500"
>
{deepExpand ? 'Collapse all' : 'Expand all'}
</button>
</>
</div>
</div>
<div
className={`rounded p-4 py-2 border bg-gray-100 ${
reverse ? 'bg-gray-200' : ''
} ${expanded ? 'block' : 'hidden'}`}
>
{Object.keys(extensions).map(extensionKey => {
if (config.extensions && config.extensions[extensionKey]) {
const CustomExtensionComponent = config.extensions[extensionKey];
return (
<CustomExtensionComponent
propertyName={extensionKey}
propertyValue={extensions[extensionKey]}
document={document}
/>
);
} else {
const extensionSchema = SchemaHelpers.jsonToSchema(
extensions[extensionKey],
);
return (
<div className="mt-2">
<Schema schemaName={extensionKey} schema={extensionSchema} />
</div>
);
}
})}
</div>
</div>
);
}; It would duplicate lots of logic of the Schema component. |
Hello, @ductaily! 👋🏼
|
/please-take-a-look |
@fmvilas @magicmatatjahu Please take a look at this PR. Thanks! 👋 |
@ductaily Hi! From the perspective of code maybe it looks like the code duplication, but extensions are not a "extended" schemas, but separate sections of spec, so even if in code it's better to have new logic inside Schema component, from spec perspective (and user) is better to have separate component. Also, if you will add logic in Schema component, new logic will be executed for all schemas in spec. Atm we treat extensions as schemas - we don't have logic to render extensions in custom way so it was ok, now it is not. Also, some people uses the Schema component outside the AsyncAPI component in their code, so using Next thing, I added (in my comment #819 (comment)) Also as I know, you can have a problem with circular references between extension. It's not a problem for extension because if extension allows to have circular refs, then author for custom component should focus on resolving that problem - still if extension won't have a custom component, then Schema component should render it and it should be handled (I see in your new example, and you handled that case). A couple of comments for the new code:
If you will have a problems, lets us know! |
I would oppose to this requirement. It's better to sort extensions by name (alphabetically) and render in that order. In our use case (SAP) we have some fields with shared prefixes. Some of those fields will have a custom component and some won't. It would mean they will be rendered apart from each other. Example:
Expected behaviour: both fields are rendered close to each other in UI. |
…er' into feat/custom-extension-value-render # Conflicts: # library/src/components/Extensions.tsx
@pavelkornev HI! Sorry for delay!
Ok, I understand :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some things what I noticed. I have to also run that code locally and check some edge cases. Please update branch with latest changes. I hope it won't be a problem if I will have to change slighty the code in branch? But overall, great job! :)
Co-authored-by: Maciej Urbańczyk <urbanczyk.maciej.95@gmail.com>
@derberg I will add the docs, test and playground showcase. Regarding your second comment just to clarify. ...
extensions: {
'x-x': DefaultExtensionComponent,
'x-linkedin': DefaultExtensionComponent,
}, Having something like export default function DefaultExtensionComponent(
props: ExtensionComponentProps,
) {
return (
<div className="mt-2">
<div className="flex py-2">
<div className="min-w-1/4 mr-2">
<span className="break-anywhere text-sm italic">
{props.propertyName}
</span>
</div>
<div>
<div className="text-sm prose">{props.propertyValue}</div>
</div>
</div>
</div>
);
} Is that correct? |
@ductaily not default component, but specific component for each extension, just like you will have probably some specific component for so in case of so they are default in a way they are included in the library, but still custom, if you know what I mean. great is that they will be a nice, production showcase how to add extensions |
@derberg you mean to keep all extension components within this repo? We have use cases where we would need to set internal links, therefore we cannot commit such components to this public repo. I think both use cases can co-exist. |
Quality Gate passedIssues Measures |
Quality Gate passedIssues Measures |
# Conflicts: # package-lock.json
…er' into feat/custom-extension-value-render
sorry for being late, we had some issues. Me and few other maintainers lost funding https://www.asyncapi.com/blog/2024-june-summary#postman-partnership-changes. Please join AsyncAPI Slack where you can ping me when you see an unusual delay on review
just add example also, once you do it, please add a screen shot to show where it can be found. I have to admit I quickly added it and could not locate where it is rendered |
Hey @derberg, thank you for the guidance :) |
Quality Gate passedIssues Measures |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the only problem is that x-x
extension should be used only in info
object level, not channel
https://github.com/asyncapi/extensions-catalog/blob/master/extensions/x.md
@magicmatatjahu can you also have a look please
Custom extensions from Info block are not rendered by UI at all. We can replace the example with some other custom field. Which field can you recommend to use instead as an example? |
Quality Gate passedIssues Measures |
export const Extensions: React.FunctionComponent<Props> = ({ | ||
name = 'Extensions', | ||
item, | ||
}) => { | ||
const [expanded, setExpanded] = useState(false); | ||
const [deepExpand, setDeepExpand] = useState(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deepExpand
is set but seems like an incomplete functionality. I think you should remove it if you don't see it adding any functionality to your changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you. I removed deepExpand
.
export interface ExtensionComponentProps<V = any> { | ||
propertyName: string; | ||
propertyValue: V; | ||
document: AsyncAPIDocumentInterface; | ||
parent: BaseModel; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should probably move this to the types file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I moved it into the types file
…er' into feat/custom-extension-value-render
Quality Gate passedIssues Measures |
<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> |
There was a problem hiding this comment.
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?
propertyValue, | ||
}: ExtensionComponentProps<string>) { | ||
const onClickHandler = () => { | ||
window.open(`https://x.com/${propertyValue}`, '_blank'); |
There was a problem hiding this comment.
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
Description
Changes proposed in this pull request:
Example
Related issue(s)
See also #819