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

Conversation

ductaily
Copy link
Contributor

Description

Changes proposed in this pull request:

  • Extends the Extension component
  • Allows customized rendering of extensions passed as configuration

Example

import * as React from "react";
import { render } from "react-dom";
import AsyncApiComponent, { ConfigInterface } from "@asyncapi/react-component";
import { ExtensionComponentProps }  from "@asyncapi/react-component/src/components/Extensions"

const schema = `
asyncapi: '2.0.0'
info:
  title: Example
  version: '0.1.0'
channels:
  example-channel:
    subscribe:
      message:
        payload:
          type: object
          x-custom-extension: 'Customized Extension'
          properties:
            exampleField:
              type: string
            exampleNumber:
              type: number
            exampleDate:
              type: string
              format: date-time
`;

function MyCustomExtensionRender(props: ExtensionComponentProps) {
  return (
    <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 className="text-sm prose">
        <a target="_blank" href="#">
          {props.propertyValue}
        </a>
      </div>
    </div>
  )
}

const config: ConfigInterface = {
  schemaID: 'custom-spec',
  show: {
    operations: false,
    errors: false,
  },
  extensions: {
  'x-custom-extension': MyCustomExtensionRender
 }
};

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

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

Bildschirmfoto 2024-04-22 um 18 44 26

Related issue(s)

See also #819

Copy link

@github-actions github-actions bot left a 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.

@ductaily ductaily changed the title feat: Custom extension rendering feat: custom extension rendering Apr 22, 2024
Copy link
Member

@derberg derberg left a 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

@magicmatatjahu
Copy link
Member

@ductaily Hi! Great work, but you shouldn't override the logic for Schema component, but for Extensions like in my #819 (comment) comment. By this you will be able to add extension not only for schemas but also for another sections of spec with possibility to define extensions, like channels, operations etc.

@ductaily
Copy link
Contributor Author

@ductaily Hi! Great work, but you shouldn't override the logic for Schema component, but for Extensions like in my #819 (comment) comment. By this you will be able to add extension not only for schemas but also for another sections of spec with possibility to define extensions, like channels, operations etc.

Hi, thank you for reviewing.
We don't understand the difference between our changes and how it would if we made the changes in the Extensions.tsx component.

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.
Should we have a call?

@asyncapi-bot
Copy link
Contributor

Hello, @ductaily! 👋🏼

    I'm 🧞🧞🧞 Genie 🧞🧞🧞 from the magic lamp. Looks like somebody needs a hand!

    At the moment the following comments are supported in pull requests:

    - `/please-take-a-look` or `/ptal` - This comment will add a comment to the PR asking for attention from the reviewrs who have not reviewed the PR yet.
    - `/ready-to-merge` or `/rtm` - This comment will trigger automerge of PR in case all required checks are green, approvals in place and do-not-merge label is not added
    - `/do-not-merge` or `/dnm` - This comment will block automerging even if all conditions are met and ready-to-merge label is added
    - `/autoupdate` or `/au` - This comment will add `autoupdate` label to the PR and keeps your PR up-to-date to the target branch's future changes. Unless there is a merge conflict or it is a draft PR. (Currently only works for upstream branches.)
    - `/update` or `/u` - This comment will update the PR with the latest changes from the target branch. Unless there is a merge conflict or it is a draft PR. NOTE: this only updates the PR once, so if you need to update again, you need to call the command again.

@ductaily
Copy link
Contributor Author

/please-take-a-look

@asyncapi-bot
Copy link
Contributor

@fmvilas @magicmatatjahu Please take a look at this PR. Thanks! 👋

@magicmatatjahu
Copy link
Member

@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 useSpec and useConfig hooks (in my perspective) is not allowed - even if we will use default values for React context.

Next thing, I added (in my comment #819 (comment)) parent prop to the custom extension component not in vain. You can have extension with this same name in two different places, e.g. channel and operation objects and you should be able to write logic base on that info, if you operate on operation extension or channel extension or another one.

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:

  • please add that parent prop (you can pass item prop from ExtensionsProps)

  • render all extensions with custom components before all extensions without them, you can even put all rest extensions to single Schema and render it as:

    const schema = SchemaHelpers.jsonToSchema(restExtensions);
    return (
      schema && (
        <div className="mt-2">
          <Schema schemaName={name} schema={schema} onlyTitle={true} />
        </div>
      )
    );

    so you will end with less code.

If you will have a problems, lets us know!

@pavelkornev
Copy link

pavelkornev commented Apr 25, 2024

@magicmatatjahu

render all extensions with custom components before all extensions without them, you can even put all rest extensions to single Schema and render it as:

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:

  • x-sap-odm-entity-name (for this field we plan to have a custom component with a link to elsewhere from the field value)
  • x-sap-odm-oid (no need for a custom component)

Expected behaviour: both fields are rendered close to each other in UI.

Duc Tai Ly added 3 commits April 25, 2024 16:33
…er' into feat/custom-extension-value-render

# Conflicts:
#	library/src/components/Extensions.tsx
@magicmatatjahu
Copy link
Member

@pavelkornev HI! Sorry for delay!

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.

Ok, I understand :)

Copy link
Member

@magicmatatjahu magicmatatjahu left a 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! :)

library/src/components/Extensions.tsx Outdated Show resolved Hide resolved
library/src/components/Extensions.tsx Outdated Show resolved Hide resolved
library/src/components/Extensions.tsx Outdated Show resolved Hide resolved
ductaily and others added 2 commits April 30, 2024 09:13
@ductaily
Copy link
Contributor Author

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

@derberg I will add the docs, test and playground showcase.

Regarding your second comment just to clarify.
If I understood you correctly, you want to add in library/src/config/default.ts the following:

 ...
  extensions: {
    'x-x': DefaultExtensionComponent,
    'x-linkedin': DefaultExtensionComponent,
  },

Having something like library/src/components/supportedExtensionComponents/DefaultExtensionComponent.tsx with the content:

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?

@derberg
Copy link
Member

derberg commented Apr 30, 2024

@ductaily not default component, but specific component for each extension, just like you will have probably some specific component for x-sap-odm-oid

so in case of x-x there is a component that renders X logo that is a clickable link pointing to twitter. Same with LinkedIn.

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

@pavelkornev
Copy link

@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.

Copy link

Quality Gate Passed Quality Gate passed

Issues
2 New issues
0 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
0.0% Duplication on New Code

See analysis details on SonarCloud

Copy link

Duc Tai Ly added 2 commits July 23, 2024 14:14
@derberg
Copy link
Member

derberg commented Jul 25, 2024

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

I am unsure how the changes for the Playground showcases should look like. Could you please guide me here

just add example x-x: AsyncAPISpec to https://github.com/asyncapi/asyncapi-react/blob/master/playground/specs/streetlights.ts so when playground is loaded, the example is showcased there and visible

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

@ductaily
Copy link
Contributor Author

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

I am unsure how the changes for the Playground showcases should look like. Could you please guide me here

just add example x-x: AsyncAPISpec to https://github.com/asyncapi/asyncapi-react/blob/master/playground/specs/streetlights.ts so when playground is loaded, the example is showcased there and visible

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 :)
I added an example:

image

Copy link

Copy link
Member

@derberg derberg left a 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

@pavelkornev
Copy link

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?

Copy link

sonarqubecloud bot commented Sep 2, 2024

export const Extensions: React.FunctionComponent<Props> = ({
name = 'Extensions',
item,
}) => {
const [expanded, setExpanded] = useState(false);
const [deepExpand, setDeepExpand] = useState(false);
Copy link
Member

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.

Copy link
Contributor Author

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.

Comment on lines 20 to 26
export interface ExtensionComponentProps<V = any> {
propertyName: string;
propertyValue: V;
document: AsyncAPIDocumentInterface;
parent: BaseModel;
}

Copy link
Member

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.

Copy link
Contributor Author

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

Comment on lines +16 to +34
<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>
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?

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants