diff --git a/library/src/constants.ts b/library/src/constants.ts index ca1d8328a..2d436200f 100644 --- a/library/src/constants.ts +++ b/library/src/constants.ts @@ -21,6 +21,9 @@ export const ONE_OF_FOLLOWING_MESSAGES_SUBSCRIBE_TEXT = 'You can subscribe to one of the following messages:'; export const ONE_OF_FOLLOWING_MESSAGES_SUBSCRIBE_SINGLE_TEXT = 'You can subscribe to the following message:'; +export const RAW_MESSAGE_SUBSCRIBE_TEXT = + 'You can subscribe to the following message:'; +export const RAW_MESSAGE_PUBLISH_TEXT = 'You can send the following message:'; export const CONTACT_TEXT = 'Contact'; export const NAM_TEXTE = 'Name'; @@ -55,6 +58,13 @@ export const MESSAGE_PAYLOAD_TEXT = 'Message Payload'; export const PAYLOAD_EXAMPLE_TEXT = 'Example of payload'; export const SCHEMA_EXAMPLE_TEXT = 'Example'; +export const SERVER_BINDINGS_TEXT = 'Server Bindings'; +export const CHANNEL_BINDINGS_TEXT = 'Channel Bindings'; +export const OPERATION_BINDINGS_TEXT = 'Operation Bindings'; +export const MESSAGE_BINDINGS_TEXT = 'Message Bindings'; + +export const BINDINGS_SCHEMA_OBJECT_TEXT = 'Schema Object'; + export const NONE_TEXT = 'None'; export const ANY_TEXT = 'Any'; export const ERROR_TEXT = 'Error'; diff --git a/library/src/containers/Bindings/Binding.tsx b/library/src/containers/Bindings/Binding.tsx new file mode 100644 index 000000000..b13c93a68 --- /dev/null +++ b/library/src/containers/Bindings/Binding.tsx @@ -0,0 +1,45 @@ +import React from 'react'; + +import { bemClasses } from '../../helpers'; + +import { BindingFieldComponent } from './BindingField'; + +const className = `binding`; +interface Props { + name: string; + binding: any; +} + +export const BindingComponent: React.FunctionComponent = ({ + name, + binding, +}) => { + if (!binding) { + return null; + } + + return ( +
+
+
+
+ {name} +
+
+
+ {Object.entries(binding).map(([key, value]) => ( + + ))} +
+ ); +}; diff --git a/library/src/containers/Bindings/BindingField.tsx b/library/src/containers/Bindings/BindingField.tsx new file mode 100644 index 000000000..1dffc6e3b --- /dev/null +++ b/library/src/containers/Bindings/BindingField.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import { bemClasses, bindingsHelper } from '../../helpers'; +import { SchemaComponent } from '../Schemas/Schema'; +import { BINDINGS_SCHEMA_OBJECT_TEXT } from '../../constants'; + +const className = `binding-field`; +interface Props { + value: any; + context: string; + bindingType: string; +} + +export const BindingFieldComponent: React.FunctionComponent = ({ + value, + context, + bindingType, +}) => { + if (value === null || value === undefined) { + return null; + } + + // three cases - simple value (need to render boolean correctly), nested object or SchemaObject + const isObject: boolean = bindingsHelper.isObject(value); + const isSchemaObject: boolean = bindingsHelper.isSchemaObject( + context, + bindingType, + ); + + if (isSchemaObject) { + return ( + <> +
+
+
+ {context} +
+
+
{BINDINGS_SCHEMA_OBJECT_TEXT}
+
+
+ +
+ + ); + } + + if (isObject) { + return ( + <> + {Object.entries(value).map(([key, val]) => ( + + ))} + + ); + } + + // simple value is the default return + return ( +
+
+
+
+ {context} +
+
+
+ {typeof value === 'boolean' ? JSON.stringify(value) : value} +
+
+
+ ); +}; diff --git a/library/src/containers/Bindings/Bindings.tsx b/library/src/containers/Bindings/Bindings.tsx new file mode 100644 index 000000000..f15583187 --- /dev/null +++ b/library/src/containers/Bindings/Bindings.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { bemClasses } from '../../helpers'; +import { BaseBindings } from '../../types'; +import { BindingComponent } from './Binding'; + +const className = `bindings`; +interface Props { + bindings?: BaseBindings; + title?: string; +} + +export const BindingsComponent: React.FunctionComponent = ({ + bindings, + title, +}) => { + if (!bindings || Object.keys(bindings).length === 0) { + return null; + } + const content = ( +
+ {Object.entries(bindings).map(([name, binding]) => ( + + ))} +
+ ); + return ( +
+
+

{title}

+
+ {content} +
+
+
+ ); +}; diff --git a/library/src/containers/Channels/Channel.tsx b/library/src/containers/Channels/Channel.tsx index 29c23f2ca..711f9c69c 100644 --- a/library/src/containers/Channels/Channel.tsx +++ b/library/src/containers/Channels/Channel.tsx @@ -2,11 +2,16 @@ import React from 'react'; import { OperationComponent } from './Operation'; import { Parameters as ParametersComponent } from './Parameters'; +import { BindingsComponent } from '../Bindings/Bindings'; import { Badge, BadgeType, Markdown, Toggle } from '../../components'; import { bemClasses, removeSpecialChars } from '../../helpers'; -import { MESSAGE_TEXT, ITEM_LABELS, CONTAINER_LABELS } from '../../constants'; -import { Channel, RawMessage, isRawMessage, PayloadType } from '../../types'; +import { + ITEM_LABELS, + CONTAINER_LABELS, + CHANNEL_BINDINGS_TEXT, +} from '../../constants'; +import { Channel, isRawMessage, PayloadType } from '../../types'; interface Props { name: string; @@ -26,10 +31,6 @@ export const ChannelComponent: React.FunctionComponent = ({ removeSpecialChars(name), ]); - const message = - (channel.publish && channel.publish.message) || - (channel.subscribe && channel.subscribe.message); - const oneOfPublish = channel.publish && channel.publish.message && @@ -40,8 +41,6 @@ export const ChannelComponent: React.FunctionComponent = ({ channel.subscribe.message && !isRawMessage(channel.subscribe.message); - const oneOfExists = Boolean(oneOfPublish || oneOfSubscribe); - const header = (

    @@ -95,20 +94,13 @@ export const ChannelComponent: React.FunctionComponent = ({ 'parameters', ])} /> + {channel.bindings && ( + + )}
    - {oneOfExists ? null : ( -
    -

    - - {(message as RawMessage)?.title || - (message as RawMessage)?.name || - MESSAGE_TEXT} - -

    -
    - )}
      {channel.subscribe && (
    • = ({

+ {operation.summary && ( +
+ {operation.summary} +
+ )} {operation.description && (
{operation.description}
)} + {operation.bindings && ( + + )} ); } + const operationTitle = operation.summary + ? operation.summary + : payloadType === PayloadType.PUBLISH + ? RAW_MESSAGE_PUBLISH_TEXT + : RAW_MESSAGE_SUBSCRIBE_TEXT; + return ( -
+
+
+

+ {isPublish && isSubscribe ? ( + + ) : null} + {operationTitle} +

+
+ {operation.description && (
{operation.description}
)} + {operation.bindings && ( + + )}
); diff --git a/library/src/containers/Messages/Message.tsx b/library/src/containers/Messages/Message.tsx index 31f488639..5d465345d 100644 --- a/library/src/containers/Messages/Message.tsx +++ b/library/src/containers/Messages/Message.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { SchemaComponent } from '../Schemas/Schema'; import { PayloadComponent } from './Payload'; +import { BindingsComponent } from '../Bindings/Bindings'; import { bemClasses, @@ -19,6 +20,7 @@ import { HEADERS_EXAMPLE_TEXT, CONTAINER_LABELS, ITEM_LABELS, + MESSAGE_BINDINGS_TEXT, } from '../../constants'; interface Props { @@ -56,11 +58,11 @@ export const MessageComponent: React.FunctionComponent = ({ if (!isRawMessage(message)) { return ( -
    +
      {message.oneOf.map((elem, index) => (
    • = ({ ); const header = !(title || summary) ? null : ( -

      +
      {message.deprecated && (
      = ({
      )} {title ? ( - - {title} - +
      +

      {title}

      +
      ) : null} - - {summary} - -

      +
); const headersID = !inChannel @@ -176,6 +175,12 @@ export const MessageComponent: React.FunctionComponent = ({ <> {headers} {payload} + {message.bindings && ( + + )} ); @@ -205,13 +210,19 @@ export const MessageComponent: React.FunctionComponent = ({ > {!isBody ? null : ( <> + {summary} {description} {content} )} ) : ( - <>{content} + <> + {header} + {summary} + {description} + {content} + )} ); diff --git a/library/src/containers/Servers/Server.tsx b/library/src/containers/Servers/Server.tsx index 0741017bb..b0cee1410 100644 --- a/library/src/containers/Servers/Server.tsx +++ b/library/src/containers/Servers/Server.tsx @@ -2,11 +2,16 @@ import React from 'react'; import { ServerVariablesComponent } from './Variables'; import { ServerSecurityComponent } from './Security'; +import { BindingsComponent } from '../Bindings/Bindings'; import { bemClasses, removeSpecialChars } from '../../helpers'; import { Toggle, Markdown } from '../../components'; import { Server, SecurityScheme } from '../../types'; -import { ITEM_LABELS, CONTAINER_LABELS } from '../../constants'; +import { + ITEM_LABELS, + CONTAINER_LABELS, + SERVER_BINDINGS_TEXT, +} from '../../constants'; interface Props { server: Server; @@ -82,6 +87,12 @@ export const ServerComponent: React.FunctionComponent = ({ dataIdentifier={dataIdentifier} /> )} + {server.bindings && ( + + )} ); diff --git a/library/src/helpers/bindingsHelpers.ts b/library/src/helpers/bindingsHelpers.ts new file mode 100644 index 000000000..5b63b673e --- /dev/null +++ b/library/src/helpers/bindingsHelpers.ts @@ -0,0 +1,25 @@ +class BindingsHelper { + /** + * + * Since we do not have a reliable way to identify schema objects via a spec, schema or similar - using a list of known + * binding properties of type SchemaObject + * Change it when AsyncAPI will support JSON Schema specification (definition) for bindings + */ + private schemaObjectKeys: string[] = [ + 'http.query', + 'kafka.groupId', + 'kafka.clientId', + 'kafka.key', + 'ws.headers', + 'ws.query', + ]; + + isSchemaObject(context: string, bindingType: string): boolean { + return this.schemaObjectKeys.includes(`${bindingType}.${context}`); + } + + isObject(value: any): boolean { + return !!value && typeof value === 'object'; + } +} +export const bindingsHelper = new BindingsHelper(); diff --git a/library/src/helpers/index.ts b/library/src/helpers/index.ts index e6fa4564d..593d3e599 100644 --- a/library/src/helpers/index.ts +++ b/library/src/helpers/index.ts @@ -7,6 +7,7 @@ export * from './searchForNestedObject'; export * from './generateExampleSchema'; export * from './inContainer'; export * from './stateHelpers'; +export * from './bindingsHelpers'; export * from './parser'; export * from './removeSpecialChars'; export * from './toKebabCase'; diff --git a/library/src/styles/fiori.css b/library/src/styles/fiori.css index 571641730..ec2410e47 100644 --- a/library/src/styles/fiori.css +++ b/library/src/styles/fiori.css @@ -565,6 +565,14 @@ .asyncapi__messages-oneOf-list-item { } +.asyncapi__message-raw-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.asyncapi__message-raw-list-item { +} .asyncapi__message { position: relative; border-radius: 4px; @@ -576,27 +584,31 @@ } .asyncapi__message-header { - padding: 12px; + background-color: #fafafa; + padding: 0px; } -.asyncapi__message-header > h3 { +.asyncapi__message-header-title > h3 { color: #0b74de; - font-size: 14px; + padding: 12px; + margin: 0px; } .asyncapi__message-header-title { - font-size: 14px; - margin-right: 10px; + background-color: #fafafa; } .asyncapi__message-header-summary { font-size: 14px; font-weight: 500; + border-top: solid 1px rgba(151, 151, 151, 0.26); + padding: 12px; } .asyncapi__message-header-deprecated-badge { } .asyncapi__message-summary { + padding: 12px; } .asyncapi__message-description { @@ -649,7 +661,6 @@ .asyncapi__message-payload-oneOf-header { color: #32363a; background-color: #fafafa; - border-top: solid 1px rgba(151, 151, 151, 0.26); } .asyncapi__message-payload-oneOf-header > h4 { padding: 12px; @@ -685,7 +696,6 @@ .asyncapi__message-payload-anyOf-header { color: #32363a; background-color: #fafafa; - border-top: solid 1px rgba(151, 151, 151, 0.26); } .asyncapi__message-payload-anyOf-header > h4 { padding: 12px; @@ -1167,8 +1177,9 @@ .asyncapi__channel-operations { } + .asyncapi__channel-operations .asyncapi__message { - border: none; + margin: 12px; } .asyncapi__channel-operations-header { @@ -1216,11 +1227,54 @@ .asyncapi__channel-operations-list-item { } +.asyncapi__channel-operations-subscribe { +} + +.asyncapi__channel-operation-message > .asyncapi__bindings { + border-radius: 4px; + border: solid 1px rgba(151, 151, 151, 0.26); + margin: 12px; +} +.asyncapi__channel-operation-message + > .asyncapi__bindings + > .asyncapi__bindings-header + > h4 { + margin: 0px; + border-top: 0px; +} + +.asyncapi__channel-operation-message-header { + color: #32363a; + border-top: solid 1px rgba(151, 151, 151, 0.26); +} +.asyncapi__channel-operation-message-header > h4 { + padding: 12px; + margin: 0; +} +.asyncapi__channel-operation-message-header > h4 > .asyncapi__badge { + margin-right: 6px; +} + +.asyncapi__channel-operation-message .asyncapi__message-header-title { + border-top: 0px; +} + +.asyncapi__channel-operation-oneOf-subscribe > .asyncapi__bindings { + border-radius: 4px; + border: solid 1px rgba(151, 151, 151, 0.26); + margin: 12px; +} +.asyncapi__channel-operation-oneOf-subscribe + > .asyncapi__bindings + > .asyncapi__bindings-header + > h4 { + margin: 0px; + border-top: 0px; +} + .asyncapi__channel-operation-oneOf-subscribe-header { color: #32363a; - background-color: #fafafa; border-top: solid 1px rgba(151, 151, 151, 0.26); - border-bottom: solid 1px rgba(151, 151, 151, 0.26); } .asyncapi__channel-operation-oneOf-subscribe-header > h4 { padding: 12px; @@ -1230,11 +1284,26 @@ margin-right: 6px; } +.asyncapi__channel-operation-oneOf-subscribe .asyncapi__message-header-title { + border-top: 0px; +} + +.asyncapi__channel-operation-oneOf-publish > .asyncapi__bindings { + border-radius: 4px; + border: solid 1px rgba(151, 151, 151, 0.26); + margin: 12px; +} +.asyncapi__channel-operation-oneOf-publish + > .asyncapi__bindings + > .asyncapi__bindings-header + > h4 { + margin: 0px; + border-top: 0px; +} + .asyncapi__channel-operation-oneOf-publish-header { color: #32363a; - background-color: #fafafa; border-top: solid 1px rgba(151, 151, 151, 0.26); - border-bottom: solid 1px rgba(151, 151, 151, 0.26); } .asyncapi__channel-operation-oneOf-publish-header > h4 { padding: 12px; @@ -1244,11 +1313,69 @@ margin-right: 6px; } +.asyncapi__channel-operation-oneOf-publish .asyncapi__message-header-title { + border-top: 0px; +} + .asyncapi__channel-operation { } +.asyncapi__message-headers { +} + +.asyncapi__bindings { +} + +.asyncapi__bindings-header { + color: #32363a; +} +.asyncapi__bindings-header > h4 { + background-color: #fafafa; + border-top: solid 1px rgba(151, 151, 151, 0.26); + border-bottom: solid 1px rgba(151, 151, 151, 0.26); + padding: 12px; + margin-left: 0px; + margin-right: 0px; + margin-bottom: 0px; + margin-top: 10px; +} + +.asyncapi__binding { +} +.asyncapi__binding-protocol { + color: #18873d; + margin-right: 6px; +} + +.asyncapi__binding-table { + border-top: solid 1px rgba(151, 151, 151, 0.26); +} +.asyncapi__binding-table > table { + margin: 0; +} +.asyncapi__binding-field-name { + font-weight: bold; + padding-left: 0px; +} + +.asyncapi__binding-field-schema { + margin: 0; +} +.asyncapi__binding-field-schema > .asyncapi__schema { + padding: 0; + border: none; +} + +.asyncapi__binding-field-schema .asyncapi__schema-table { + border-top: none; +} +.asyncapi__binding-field-schema > .asyncapi__schema:before { + content: ''; + position: relative; + border: none; +} .asyncapi__channel-operation-description { - padding: 16px 16px 0; + padding: 12px 12px 0; } .asyncapi__channel-description { diff --git a/library/src/types.ts b/library/src/types.ts index 217b17256..ca82e59de 100644 --- a/library/src/types.ts +++ b/library/src/types.ts @@ -47,6 +47,10 @@ export enum BindingsType { } export type Bindings = keyof typeof BindingsType; +export interface BaseBindings { + [key: string]: any; +} + export interface AsyncAPI { asyncapi: AsyncAPIVersion; id?: UniqueID; @@ -90,7 +94,7 @@ export interface Server { description?: DescriptionHTML; variables?: ServerVariables; security?: SecurityRequirement[]; - bindings?: ServerBindings[]; + bindings?: BaseBindings[]; } export interface ServerVariables { @@ -108,10 +112,6 @@ export interface SecurityRequirement { [key: string]: string[]; } -export interface ServerBindings { - [key: string]: any; -} - export interface Channels { [key: string]: Channel; } @@ -123,6 +123,7 @@ export interface Channel { subscribe?: Operation; deprecated?: boolean; protocolInfo?: ProtocolInfo; + bindings?: BaseBindings; } export interface OperationTrait { @@ -145,6 +146,7 @@ export interface Operation { operationId?: string; protocolInfo?: ProtocolInfo; message?: Message; + bindings?: BaseBindings[]; } export interface ProtocolInfo { @@ -212,6 +214,7 @@ export interface RawMessage { examples?: Example[]; protocolInfo?: any; traits?: MessageTrait | [MessageTrait, any]; + bindings?: BaseBindings; } export interface Tag { diff --git a/playground/src/specs/streetlights.ts b/playground/src/specs/streetlights.ts index 6e6cc77cd..aeabb1e4a 100644 --- a/playground/src/specs/streetlights.ts +++ b/playground/src/specs/streetlights.ts @@ -35,6 +35,17 @@ servers: - streetlights:off - streetlights:dim - openIdConnectWellKnown: [] + bindings: + mqtt: + clientId: guest + cleanSession: false + keepAlive: 60 + bindingVersion: 0.1.0 + lastWill: + topic: smartylighting/streetlights/1/0/lastwill + qos: 1 + message: so long and thanks for all the fish + retain: false defaultContentType: application/json @@ -44,13 +55,42 @@ channels: parameters: streetlightId: $ref: '#/components/parameters/streetlightId' + bindings: + ws: + method: 'POST' + bindingVersion: '0.1.0' subscribe: - summary: Receive information about environmental lighting conditions of a particular streetlight. + summary: Receive lighting conditions of a streetlight. + description: Receive information about environmental lighting conditions of a particular streetlight. operationId: receiveLightMeasurement traits: - $ref: '#/components/operationTraits/kafka' message: $ref: '#/components/messages/lightMeasured' + bindings: + mqtt: + qos: 1 + bindingVersion: 0.1.0 + http: + type: request + method: GET + query: + type: object + required: + - companyId + properties: + companyId: + type: number + minimum: 1 + description: The Id of the company. + additionalProperties: false + publish: + summary: Send information about environmental lighting conditions of a particular streetlight. + operationId: sendLightMeasurement + traits: + - $ref: '#/components/operationTraits/kafka' + message: + $ref: '#/components/messages/lightMeasured' smartylighting/streetlights/1/0/action/{streetlightId}/turn/on: parameters: @@ -62,6 +102,10 @@ channels: - $ref: '#/components/operationTraits/kafka' message: $ref: '#/components/messages/turnOnOff' + bindings: + mqtt: + qos: 1 + bindingVersion: 0.1.0 smartylighting/streetlights/1/0/action/{streetlightId}/turn/off: parameters: @@ -90,8 +134,12 @@ components: lightMeasured: name: lightMeasured title: Light measured - summary: Inform about environmental lighting conditions for a particular streetlight. + summary: Lighting conditions for a streetlight. + description: Inform about environmental lighting conditions for a particular streetlight. contentType: application/json + bindings: + mqtt: + bindingVersion: 0.1.0 traits: - $ref: '#/components/messageTraits/commonHeaders' payload: @@ -223,5 +271,8 @@ components: kafka: bindings: kafka: - clientId: my-app-id + clientId: + type: string + enum: ['my-app-id'] + bindingVersion: '0.1.0' `;