= {};
+ let hasSupportedProto = true;
+
+ const servers = document?.servers();
+ const availableServers: string[] = [];
+ Object.entries(servers || {}).forEach(([serverName, server]) => {
+ if (server.protocol && supportedProtocols.includes(server.protocol())) availableServers.push(serverName);
+ });
+
+ if (supportedProtocols.length && availableServers.length === 0) {
+ hasSupportedProto = false;
+ setConfirmDisabled(true);
+ } else {
+ Object.keys(properties).forEach(propKey => {
+ if (propKey === 'server') {
+ // @ts-ignore
+ const jsonProperty = { ...properties[String(propKey)] };
+ jsonProperty.enum = availableServers;
+ requiredProperties[String(propKey)] = jsonProperty;
+ } else if (required.includes(propKey)) {
+ // @ts-ignore
+ requiredProperties[String(propKey)] = properties[String(propKey)];
+ } else {
+ // @ts-ignore
+ optionalProperties[String(propKey)] = properties[String(propKey)];
+ }
+ });
+ }
+
+ return { requiredProps: requiredProperties, optionalProps: optionalProperties, hasSupportedProtocols: hasSupportedProto };
+ }, [properties, required, document]);
+
+ useEffect(() => {
+ setValues({});
+ setShowOptionals(false);
+ }, [templateName, setValues, setShowOptionals]);
+
+ useImperativeHandle(templateParamsRef, () => ({
+ getValues() {
+ return values;
+ }
+ }));
+
+ const setValue = useCallback((propertyName: string, value: any, isRequired: boolean) => {
+ setValues(oldValues => {
+ oldValues[String(propertyName)] = String(value);
+ if (isRequired) {
+ const disableConfirm = required.some(r => !oldValues[String(r)]);
+ setConfirmDisabled(disableConfirm);
+ }
+ return oldValues;
+ });
+ }, [required]);
+
+ const renderFields = useCallback((propertyName: string, property: JSONSchema7, isRequired: boolean, isFirst: boolean) => {
+ return (
+
+
+
+
+ {propertyName}
+
+
+
+
+
+ {property.description}
+
+
+ );
+ }, [templateName]);
+
+ if (document === null) {
+ return null;
+ }
+
+ if (!templateName) {
+ return null;
+ }
+
+ if (!hasSupportedProtocols) {
+ return (
+
+ AsyncAPI document doesn't have at least one server with supported protocols. For the selected generation, these are supported: {supportedProtocols.join(', ')}
+
+ );
+ }
+
+ if (!properties || !Object.keys(properties).length) {
+ return (
+
+ {'The given generation hasn\'t parameters to pass'}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+const TemplateParameters = forwardRef(TemplateParametersSans);
+export { TemplateParameters };
diff --git a/apps/studio-next/src/components/Modals/Generator/template-parameters.json b/apps/studio-next/src/components/Modals/Generator/template-parameters.json
new file mode 100644
index 000000000..cd065e7eb
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/Generator/template-parameters.json
@@ -0,0 +1 @@
+{"@asyncapi/dotnet-nats-template":{"title":".NET Nats Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"version":{"description":"Version of the generated library","type":"string","default":"0.0.1"},"projectName":{"description":"Name of the generated library","type":"string","default":"AsyncapiNatsClient"},"repositoryUrl":{"description":"Repository url for the project file, often needed for release pipelines","type":"string"},"targetFramework":{"description":"The project target framework","type":"string","default":"netstandard2.0;netstandard2.1"},"packageVersion":{"description":"PackageVersion of the generated library","type":"string"},"assemblyVersion":{"description":"AssemblyVersion of the generated library","type":"string"},"fileVersion":{"description":"FileVersion of the generated library","type":"string"},"serializationLibrary":{"description":"Which serialization library should the models use? `newtonsoft` or `json` (system.text.json)","type":"string","default":"json"}},"required":[],"additionalProperties":false},"supportedProtocols":["nats"]},"@asyncapi/go-watermill-template":{"title":"GO Lang Watermill Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"moduleName":{"description":"name of the go module to be generated","type":"string","default":"go-async-api"}},"required":[],"additionalProperties":false},"supportedProtocols":["amqp"]},"@asyncapi/html-template":{"title":"HTML website","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"sidebarOrganization":{"description":"Defines how the sidebar should be organized. 'byTags' to categorize operations by tags in the root of the document, `byTagsNoRoot` does the same but for pub/sub tags.","type":"string"},"baseHref":{"description":"Sets the base URL for links and forms.","type":"string"},"version":{"description":"Override the version of your application provided under `info.version` location in the specification file.","type":"string"},"singleFile":{"description":"If set this parameter to true generate single html file with scripts and styles inside","type":"boolean","default":false},"outFilename":{"description":"The name of the output HTML file","type":"string","default":"index.html"},"pdf":{"description":"Set to `true` to get index.pdf generated next to your index.html","type":"boolean","default":false},"pdfTimeout":{"description":"The timeout (in ms) used to generate the pdf","type":"number","default":30000},"favicon":{"description":"URL/Path of the favicon","type":"string","default":""},"config":{"description":"Stringified JSON or a path to a JSON file to override the default React component config. The config override is merged with the default config using the [JSON Merge Patch](https://tools.ietf.org/html/rfc7386) algorithm.","type":"string","default":""}},"required":[],"additionalProperties":false}},"@asyncapi/java-spring-cloud-stream-template":{"title":"Java Spring Cloud Stream Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"actuator":{"description":"If present, it adds the dependencies for spring-boot-starter-web, spring-boot-starter-actuator and micrometer-registry-prometheus.","type":"boolean","default":false},"artifactId":{"description":"The Maven artifact id. Alternatively you can set the specification extension info.x-artifact-id","type":"string","default":"project-name"},"artifactType":{"description":"The type of project to generate, application or library. The default is application. When generating an application, the pom.xml file will contain the complete set of dependencies required to run an app, and it will contain an Application class with a main function. Otherwise the pom file will include only the dependencies required to compile a library.","type":"string","default":"application"},"binder":{"description":"The name of the binder implementation, one of kafka, rabbit or solace. Default: kafka. If you need other binders to be supported, please let us know!","type":"string","default":"kafka"},"dynamicType":{"description":"When using channels with parameters, i.e. dynamic topics where the topic could be different for each message, this determines whether to use the StreamBridge or a message header. StreamBridge can be used with all binders, but some binders such as Solace can use the topic set in a header for better performance. Possible values are streamBridge and header. Default is streamBridge.","type":"string","default":"streamBridge"},"groupId":{"description":"The Maven group id. Alternatively you can set the specification extension info.x-group-id","type":"string","default":"com.company"},"host":{"description":"The host connection property. Currently this only works with the Solace binder. Example: tcp://myhost.com:55555.","type":"string","default":"tcp://localhost:55555"},"javaPackage":{"description":"The Java package of the generated classes. Alternatively you can set the specification extension info.x-java-package","type":"string"},"msgVpn":{"description":"The message vpn connection property. Currently this only works with the Solace binder.","type":"string","default":"default"},"parametersToHeaders":{"description":"If true, this will create headers on the incoming messages for each channel parameter. Currently this only works with messages originating from Solace (using the solace_destination header) and RabbitMQ (using the amqp_receivedRoutingKey header.)","type":"boolean","default":false},"password":{"description":"The client password connection property. Currently this only works with the Solace binder.","type":"string","default":"default"},"reactive":{"description":"If true, this will generate reactive style functions using the Flux class. Defalt: false.","type":"boolean","default":false},"solaceSpringCloudVersion":{"description":"The version of the solace-spring-cloud-bom dependency used when generating an application. Alternatively you can set the specification extension info.x-solace-spring-cloud-version.","type":"string","default":"2.1.0"},"springBootVersion":{"description":"The version of Spring Boot used when generating an application. Alternatively you can set the specification extension info.x-spring-booot-version. Example: 2.2.6.RELEASE.","type":"string","default":"2.4.7"},"springCloudVersion":{"description":"The version of the spring-cloud-dependencies BOM dependency used when generating an application. Alternatively you can set the specification extension info.x-spring-cloud-version. Example: Hoxton.RELEASE.","type":"string","default":"2020.0.3"},"springCloudStreamVersion":{"description":"The version of the spring-cloud-stream dependency specified in the Maven file, when generating a library. When generating an application, the spring-cloud-dependencies BOM is used instead. Example: 3.0.1.RELEASE","type":"string","default":"3.1.3"},"username":{"description":"The client username connection property. Currently this only works with the Solace binder","type":"string","default":"default"},"view":{"description":"The view that the template uses. By default it is the client view, which means that when the document says publish, we subscribe. In the case of the provider view, when the document says publish, we publish. Values are client or provider. The default is client.","type":"string","default":"client"},"useServers":{"description":"This option works when binder is kafka. By default it is set to false. When set to true, it will concatenate all the urls in the servers section as a list of brokers for kafka.","type":"string"}},"required":[],"additionalProperties":false}},"@asyncapi/java-spring-template":{"title":"Java Spring Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"inverseOperations":{"description":"Generate application that will publish messages to `publish` operation of channels and read messages from `subscribe` operation of channels. Literally this flag just swap `publish` and `subscribe` operations in channels.","type":"boolean","default":false},"disableEqualsHashCode":{"description":"Disable generation of equals and hashCode methods for model classes.","type":"boolean","default":false},"listenerPollTimeout":{"description":"Only for Kafka. Timeout to use when polling the consumer.","type":"number","default":3000},"listenerConcurrency":{"description":"Only for Kafka. Number of threads to run in the listener containers.","type":"number","default":3},"connectionTimeout":{"description":"Only for MQTT. This value, measured in seconds, defines the maximum time interval the client will wait for the network connection to the MQTT server to be established. The default timeout is 30 seconds. A value of 0 disables timeout processing meaning the client will wait until the network connection is made successfully or fails.","type":"number","default":30},"disconnectionTimeout":{"description":"Only for MQTT. The completion timeout in milliseconds when disconnecting. The default disconnect completion timeout is 5000 milliseconds.","type":"number","default":5000},"completionTimeout":{"description":"Only for MQTT. The completion timeout in milliseconds for operations. The default completion timeout is 30000 milliseconds.","type":"number","default":30000},"mqttClientId":{"description":"Only for MQTT. Provides the client identifier for the MQTT server. This parameter overrides the value of the clientId if it's set in the AsyncAPI file.","type":"string"},"asyncapiFileDir":{"description":"Parameter of @asyncapi/generator-hooks#createAsyncapiFile, allows to specify where original AsyncAPI file will be stored.","type":"string","default":"src/main/resources/api/"},"javaPackage":{"description":"The Java package of the generated classes. Alternatively you can set the specification extension info.x-java-package","type":"string","default":"com.asyncapi"},"addTypeInfoHeader":{"description":"Only for Kafka. Add type information to the message header","type":"boolean","default":true},"springBoot2":{"description":"Generate template files for the Spring Boot version 2. For kafka protocol it will also force to use spring-kafka 2.9.9","type":"boolean","default":false},"maven":{"description":"Generate pom.xml Maven build file instead of Gradle build","type":"boolean","default":false}},"required":[],"additionalProperties":false},"supportedProtocols":["kafka","amqp","mqtt"]},"@asyncapi/java-template":{"title":"Java Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"server":{"description":"The server you want to use in the code.","type":"string"},"asyncapiFileDir":{"description":"Custom location of the AsyncAPI file that you provided as an input in generation. By default it is located in the root of the output directory","type":"string"},"user":{"description":"Username for the server to generate code for","type":"string","default":"app"},"password":{"description":"Password for the server to generate code for","type":"string","default":"passw0rd"},"package":{"description":"Java package name for generated code","type":"string","default":"com.asyncapi"},"mqTopicPrefix":{"description":"MQ topic prefix. Used for ibmmq protocols. Default will work with dev MQ instance","type":"string","default":"dev//"}},"required":["server"],"additionalProperties":false},"supportedProtocols":["ibmmq","ibmmq-secure","kafka","kafka-secure"]},"@asyncapi/markdown-template":{"title":"Markdown Documentation","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"frontMatter":{"description":"The name of a JSON or YAML formatted file containing values to provide the YAML frontmatter for static-site or documentation generators. The file may contain {{title}} and {{version}} replaceable tags.","type":"string"},"outFilename":{"description":"The name of the output markdown file","type":"string","default":"asyncapi.md"},"toc":{"description":"Include a Table-of-Contents in the output markdown.","type":"boolean","default":true},"version":{"description":"Override the version of your application provided under `info.version` location in the specification file.","type":"string"}},"required":[],"additionalProperties":false}},"@asyncapi/nodejs-template":{"title":"NodeJS Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"server":{"description":"The server you want to use in the code.","type":"string"},"asyncapiFileDir":{"description":"Custom location of the AsyncAPI file that you provided as an input in generation. By default it is located in the root of the output directory","type":"string"},"securityScheme":{"description":"Name of the security scheme. Only scheme with X509 and Kafka protocol is supported for now.","type":"string"},"certFilesDir":{"description":"Directory where application certificates are located. This parameter is needed when you use X509 security scheme and your cert files are not located in the root of your application.","type":"string","default":"./"}},"required":["server"],"additionalProperties":false},"supportedProtocols":["amqp","mqtt","mqtts","kafka","kafka-secure","ws"]},"@asyncapi/nodejs-ws-template":{"title":"NodeJS WebSocket Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"server":{"description":"The server you want to use in the code.","type":"string"},"asyncapiFileDir":{"description":"Custom location of the AsyncAPI file that you provided as an input in generation. By default it is located in the root of the output directory","type":"string"}},"required":["server"],"additionalProperties":false},"supportedProtocols":["ws"]},"@asyncapi/python-paho-template":{"title":"Python Paho Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"view":{"description":"The view that the template uses. By default it is the client view, which means that when the document says publish, we subscribe. In the case of the provider view, when the document says publish, we publish. Values are client or provider. The default is client.","type":"string"}},"required":[],"additionalProperties":false}},"@asyncapi/ts-nats-template":{"title":"Typescript Nats Project","schema":{"$schema":"http://json-schema.org/draft-07/schema#","type":"object","properties":{"generateTestClient":{"description":"Generate the test client","type":"boolean","default":false},"promisifyReplyCallback":{"description":"Use promises as callbacks for reply operation","type":"boolean","default":false}},"required":[],"additionalProperties":false},"supportedProtocols":["nats"]}}
\ No newline at end of file
diff --git a/apps/studio-next/src/components/Modals/ImportBase64Modal.tsx b/apps/studio-next/src/components/Modals/ImportBase64Modal.tsx
new file mode 100644
index 000000000..64ee1d22a
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/ImportBase64Modal.tsx
@@ -0,0 +1,57 @@
+import { useState } from 'react';
+import toast from 'react-hot-toast';
+import { create } from '@ebay/nice-modal-react';
+
+import { ConfirmModal } from './index';
+
+import { useServices } from '../../services';
+
+export const ImportBase64Modal = create(() => {
+ const [base64, setBase64] = useState('');
+ const { editorSvc } = useServices();
+
+ const onSubmit = () => {
+ toast.promise(editorSvc.importBase64(base64), {
+ loading: 'Importing...',
+ success: (
+
+
+ Document succesfully imported!
+
+
+ ),
+ error: (
+
+
+ Failed to import document.
+
+
+ ),
+ });
+ };
+
+ return (
+
+
+
+ Base64 content
+
+
+
+ );
+});
diff --git a/apps/studio-next/src/components/Modals/ImportURLModal.tsx b/apps/studio-next/src/components/Modals/ImportURLModal.tsx
new file mode 100644
index 000000000..3362e501e
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/ImportURLModal.tsx
@@ -0,0 +1,57 @@
+import { useState } from 'react';
+import toast from 'react-hot-toast';
+import { create } from '@ebay/nice-modal-react';
+
+import { ConfirmModal } from './index';
+
+import { useServices } from '../../services';
+
+export const ImportURLModal = create(() => {
+ const [url, setUrl] = useState('');
+ const { editorSvc } = useServices();
+
+ const onSubmit = () => {
+ toast.promise(editorSvc.importFromURL(url), {
+ loading: 'Importing...',
+ success: (
+
+
+ Document succesfully imported!
+
+
+ ),
+ error: (
+
+
+ Failed to import document.
+
+
+ ),
+ });
+ };
+
+ return (
+
+
+
+ Document URL
+
+ setUrl(e.target.value)}
+ />
+
+
+ );
+});
diff --git a/apps/studio-next/src/components/Modals/NewFileModal.tsx b/apps/studio-next/src/components/Modals/NewFileModal.tsx
new file mode 100644
index 000000000..a2bc2d89b
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/NewFileModal.tsx
@@ -0,0 +1,109 @@
+import { useState } from 'react';
+import { BsFillCheckCircleFill } from 'react-icons/bs';
+import toast from 'react-hot-toast';
+import { create } from '@ebay/nice-modal-react';
+
+import examples from '@/examples';
+
+import { ConfirmModal } from './ConfirmModal';
+import { useServices } from '../../services';
+
+import type { ComponentType, MouseEventHandler, FunctionComponent } from 'react';
+
+interface TemplateListItemProps {
+ title: string;
+ description: ComponentType;
+ isSelected: boolean;
+ onClick: MouseEventHandler;
+ key: string;
+}
+
+const TemplateListItem: FunctionComponent = ({ title, description: Description, onClick, isSelected }) => {
+ const containerStyles = isSelected ? 'border-pink-500' : 'border-gray-200';
+ const textStyles = isSelected ? 'text-pink-600' : 'text-gray-600';
+
+ return (
+
+
+ {title}
+ {isSelected && }
+
+
+
+
+
+ );
+};
+
+export const NewFileModal = create(() => {
+ const { editorSvc } = useServices();
+ const [selectedTemplate, setSelectedTemplate] = useState({ title: '', template: '' });
+
+ const onSubmit = () => {
+ editorSvc.updateState({ content: selectedTemplate.template, updateModel: true });
+ setSelectedTemplate({ title: '', template: '' });
+
+ toast.success(
+
+ Successfully reused the {`"${selectedTemplate.title}"`} template.
+
+ );
+ };
+
+ const realLifeExamples = examples.filter((template) => template.type === 'real-example');
+ const templates = examples.filter((template) => template.type === 'protocol-example');
+ const tutorials = examples.filter((template) => template.type === 'tutorial-example');
+
+ return (
+
+
+
+
+
Templates
+
+ {templates.map(({ title, description, template }) => {
+ const isSelected = selectedTemplate.title === title;
+ return setSelectedTemplate({ title, template })} />;
+ })}
+
+
+
+
Real world Examples
+
+ {realLifeExamples.map(({ title, description, template }) => {
+ const isSelected = selectedTemplate.title === title;
+ return setSelectedTemplate({ title, template })} />;
+ })}
+
+
+
+
Tutorials
+
+ {tutorials.map(({ title, description, template }) => {
+ const isSelected = selectedTemplate.title === title;
+ return setSelectedTemplate({ title, template })} />;
+ })}
+
+
+
+ Don't see what you're looking for?
+
+ Request a template or add one to the list →
+
+
+
+
+
+ );
+});
diff --git a/apps/studio-next/src/components/Modals/RedirectedModal.tsx b/apps/studio-next/src/components/Modals/RedirectedModal.tsx
new file mode 100644
index 000000000..6846d09a4
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/RedirectedModal.tsx
@@ -0,0 +1,62 @@
+import { useState } from 'react';
+import { create } from '@ebay/nice-modal-react';
+
+import { ConfirmModal } from './ConfirmModal';
+import { Markdown } from '../common';
+
+const CHANGES = `
+Below are the changes compared to the old AsyncAPI Playground:
+
+- There is no preview for markdown.
+- Studio supports the same query parameters except **template**.
+- To download an AsyncAPI document from an external source use the editor menu and select **Import from URL**. There is also an option to use a local file, base64 saved file, convert a given version of AsyncAPI document to a newer one as well as change the format from YAML to JSON and vice versa. There is also option to download AsyncAPI document as file.
+- To generate the template, please click on the **Generate code/docs** item in the menu at the top right corner of the editor, enter (needed) the parameters and click **Generate**.
+- The left navigation is used to open/close the panels.
+- Errors in the AsyncAPI document are shown in a panel at the bottom of the editor. The panel is expandable.
+- To see the data flow in AsyncAPI document click the 4th node in the left navigation.
+- To select a sample template file click on the 5th item in the left navigation.
+- Studio settings can be changed by clicking on the settings icon in the lower left corner.
+- Panels can be stretched.
+`;
+
+function onCancel() {
+ if (typeof window.history.replaceState === 'function') {
+ const url = new URL(window.location.href);
+ url.searchParams.delete('redirectedFrom');
+ window.history.replaceState({}, window.location.href, url.toString());
+ }
+}
+
+export const RedirectedModal = create(() => {
+ const [showMore, setShowMore] = useState(false);
+
+ return (
+
+
+
+
+ {CHANGES}
+
+ {!showMore && (
+ <>
+
+
+ setShowMore(true)}
+ >
+ Show what's changed
+
+
+ >
+ )}
+
+
+
+ );
+});
diff --git a/apps/studio-next/src/components/Modals/Settings/SettingsModal.tsx b/apps/studio-next/src/components/Modals/Settings/SettingsModal.tsx
new file mode 100644
index 000000000..5ee4a9338
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/Settings/SettingsModal.tsx
@@ -0,0 +1,211 @@
+import { useState, useEffect, useCallback } from 'react';
+import toast from 'react-hot-toast';
+import { create, useModal } from '@ebay/nice-modal-react';
+
+import { SettingsTabs, SettingTab } from './SettingsTabs';
+
+import { ConfirmModal } from '../index';
+import { Switch } from '../../common';
+
+import { useServices } from '@/services';
+
+import type { Dispatch, SetStateAction, FunctionComponent } from 'react';
+import type { SettingsState } from '@/state/settings.state';
+
+interface ShowGovernanceOptionProps {
+ label: 'warning' | 'information' | 'hint';
+ state: boolean;
+ setState: Dispatch>;
+}
+
+const ShowGovernanceOption: FunctionComponent = ({
+ label,
+ state,
+ setState
+}) => {
+ return (
+
+
+
+
+ Show {label} governance issues
+
+
+
+
+ Show {label} governance issues in the editor's Diagnostics tab.
+
+
+
+ );
+};
+
+interface SettingsModalProps {
+ activeTab?: 'editor' | 'governance' | 'template';
+}
+
+export const SettingsModal = create(({ activeTab = 'editor' }) => {
+ const { settingsSvc } = useServices();
+ const settings = settingsSvc.get();
+ const modal = useModal();
+
+ const [autoSaving, setAutoSaving] = useState(settings.editor.autoSaving);
+ const [savingDelay, setSavingDelay] = useState(settings.editor.savingDelay);
+ const [governanceWarnings, setGovernanceWarnings] = useState(settings.governance.show.warnings);
+ const [governanceInformations, setGovernanceInformations] = useState(settings.governance.show.informations);
+ const [governanceHints, setGovernanceHints] = useState(settings.governance.show.hints);
+ const [autoRendering, setAutoRendering] = useState(settings.templates.autoRendering);
+ const [confirmDisabled, setConfirmDisabled] = useState(true);
+
+ const createNewState = (): SettingsState => {
+ return {
+ editor: {
+ autoSaving,
+ savingDelay,
+ },
+ governance: {
+ show: {
+ warnings: governanceWarnings,
+ informations: governanceInformations,
+ hints: governanceHints,
+ },
+ },
+ templates: {
+ autoRendering,
+ }
+ };
+ };
+
+ useEffect(() => {
+ const newState = createNewState();
+ const isThisSameObjects = settingsSvc.isEqual(newState);
+ setConfirmDisabled(isThisSameObjects);
+ }, [autoSaving, savingDelay, autoRendering, governanceWarnings, governanceInformations, governanceHints]);
+
+ const onCancel = useCallback(() => {
+ modal.hide();
+ }, []);
+
+ const onSubmit = () => {
+ const newState = createNewState();
+ settingsSvc.set(newState);
+
+ toast.success(
+
+
+ Settings succesfully saved!
+
+
+ );
+ onCancel();
+ };
+
+ const tabs: Array = [
+ {
+ name: 'editor',
+ tab: Editor ,
+ content: (
+
+
+
+
+ Auto saving
+
+ setAutoSaving(v)}
+ />
+
+
+ Save automatically after each change in the document or manually.
+
+
+
+
+
+ Delay (in miliseconds)
+
+ setSavingDelay(JSON.parse(e.target.value))}
+ value={autoSaving ? savingDelay : ''}
+ disabled={!autoSaving}
+ >
+ Please Select
+ {[250, 500, 625, 750, 875, 1000].map(v => (
+
+ {v}
+
+ ))}
+
+
+
+ Delay in saving the modified document.
+
+
+
+ ),
+ },
+ {
+ name: 'governance',
+ tab: Governance ,
+ content: (
+ <>
+
+
+
+ >
+ ),
+ },
+ {
+ name: 'templates',
+ tab: Templates ,
+ content: (
+
+
+
+
+ Auto rendering
+
+ setAutoRendering(v)}
+ />
+
+
+
+ Automatic rendering after each change in the document or manually.
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ );
+});
diff --git a/apps/studio-next/src/components/Modals/Settings/SettingsTabs.tsx b/apps/studio-next/src/components/Modals/Settings/SettingsTabs.tsx
new file mode 100644
index 000000000..2127ef2cd
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/Settings/SettingsTabs.tsx
@@ -0,0 +1,73 @@
+import { useState } from 'react';
+
+import type { ReactNode, FunctionComponent } from 'react';
+
+export interface SettingTab {
+ name: string;
+ tab: ReactNode;
+ content: ReactNode;
+}
+
+interface SettingTabsProps {
+ active: string;
+ tabs: Array;
+}
+
+export const SettingsTabs: FunctionComponent = ({
+ active,
+ tabs = [],
+}) => {
+ const [activeTab, setActiveTab] = useState(
+ tabs.some(tab => tab.name === active) ? active : tabs[0]?.name
+ );
+
+ if (tabs.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ {tabs.map(tab => (
+ setActiveTab(tab.name)}
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') setActiveTab(tab.name);
+ }}
+ >
+
+ {tab.tab}
+
+
+ ))}
+
+
+
+
+ {tabs.map(tab => (
+
+ {tab.content}
+
+ ))}
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Modals/index.tsx b/apps/studio-next/src/components/Modals/index.tsx
new file mode 100644
index 000000000..b89162281
--- /dev/null
+++ b/apps/studio-next/src/components/Modals/index.tsx
@@ -0,0 +1,11 @@
+export * from './Generator/GeneratorModal';
+export * from './Settings/SettingsModal';
+
+export * from './ConfirmModal';
+export * from './ConvertModal';
+export * from './ConvertToLatestModal';
+export * from './ImportBase64Modal';
+export * from './ImportURLModal';
+export * from './NewFileModal';
+export * from './RedirectedModal';
+export * from './ConfirmNewFileModal';
\ No newline at end of file
diff --git a/apps/studio-next/src/components/Navigation.tsx b/apps/studio-next/src/components/Navigation.tsx
new file mode 100644
index 000000000..53537e2c2
--- /dev/null
+++ b/apps/studio-next/src/components/Navigation.tsx
@@ -0,0 +1,420 @@
+/* eslint-disable sonarjs/no-nested-template-literals, sonarjs/no-duplicate-string */
+
+import React, { useEffect, useState } from 'react';
+
+import { useServices } from '@/services';
+import { useDocumentsState, useFilesState } from '@/state';
+import { NAVIGATION_SECTION_STYLE, NAVIGATION_SUB_SECTION_STYLE } from './Navigationv3';
+import type { AsyncAPIDocumentInterface } from '@asyncapi/parser';
+
+interface NavigationProps {
+ className?: string;
+}
+
+interface NavigationSectionProps {
+ document: AsyncAPIDocumentInterface;
+ rawSpec: string;
+ hash: string;
+}
+
+const ServersNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ return (
+ <>
+
+ navigationSvc.scrollTo('/servers', 'servers')
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo('/servers', 'servers');
+ }}
+ >
+ Servers
+
+
+ {document.servers().all().map((server) => {
+ const serverName = server.id();
+ return
+ navigationSvc.scrollTo(
+ `/servers/${serverName.replace(/\//g, '~1')}`,
+ `server-${serverName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/servers/${serverName.replace(/\//g, '~1')}`,
+ `server-${serverName}`,
+ );
+ }}
+ >
+
+
+
+ {server.protocolVersion()
+ ? `${server.protocol()} ${server.protocolVersion()}`
+ : server.protocol()}
+
+
+
{serverName}
+
+
+ })}
+
+ >
+ );
+};
+
+const OperationsNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const operations = document.operations().all().map(
+ (operation) => {
+ const channels: React.ReactNode[] = [];
+ // only has one channel per operation
+ let channelName = 'Unknown';
+ if (!operation.channels().isEmpty()) {
+ channelName = operation.channels().all()[0].address() ?? 'Unknown';
+ }
+ if (operation.isReceive()) {
+ channels.push(
+
+ navigationSvc.scrollTo(
+ `/channels/${channelName.replace(/\//g, '~1')}`,
+ `operation-publish-${channelName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/channels/${channelName.replace(/\//g, '~1')}`,
+ `operation-publish-${channelName}`,
+ );
+ }}
+ >
+
+
+
+ Pub
+
+
+
{channelName}
+
+ ,
+ );
+ }
+ if (operation.isSend()) {
+ channels.push(
+
+ navigationSvc.scrollTo(
+ `/channels/${channelName.replace(/\//g, '~1')}`,
+ `operation-subscribe-${channelName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/channels/${channelName.replace(/\//g, '~1')}`,
+ `operation-subscribe-${channelName}`,
+ );
+ }}
+ >
+
+
+
+ Sub
+
+
+
{channelName}
+
+ ,
+ );
+ }
+
+ return channels;
+ },
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/channels',
+ 'operations',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/channels',
+ 'operations',
+ );
+ }}
+ >
+ Operations
+
+
+ >
+ );
+};
+
+const MessagesNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const messages = document.components().messages().all().map(
+ message => {
+ const messageName = message.id();
+ return
+ navigationSvc.scrollTo(
+ `/components/messages/${messageName.replace(/\//g, '~1')}`,
+ `message-${messageName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/components/messages/${messageName.replace(/\//g, '~1')}`,
+ `message-${messageName}`,
+ );
+ }}
+ >
+ {messageName}
+
+ },
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/components/messages',
+ 'messages',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/components/messages',
+ 'messages',
+ );
+ }}
+ >
+ Messages
+
+
+ >
+ );
+};
+
+const SchemasNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const schemas = document.components().schemas().all().map(
+ schema => {
+ const schemaName = schema.id();
+ return
+ navigationSvc.scrollTo(
+ `/components/schemas/${schemaName.replace(/\//g, '~1')}`,
+ `schema-${schemaName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/components/schemas/${schemaName.replace(/\//g, '~1')}`,
+ `schema-${schemaName}`,
+ );
+ }}
+ >
+ {schemaName}
+
+ }
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/components/schemas',
+ 'schemas',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/components/schemas',
+ 'schemas',
+ );
+ }}
+ >
+ Schemas
+
+
+ >
+ );
+};
+
+export const Navigation: React.FunctionComponent = ({
+ className = '',
+}) => {
+ const [hash, setHash] = useState(window.location.hash);
+ const [loading, setloading] = useState(false);
+
+ const { navigationSvc } = useServices();
+ const rawSpec = useFilesState(state => state.files['asyncapi']?.content);
+ const document = useDocumentsState(state => state.documents['asyncapi']?.document);
+
+ useEffect(() => {
+ const fn = () => {
+ // remove `#` char
+ const h = window.location.hash.startsWith('#') ? window.location.hash.substring(1) : window.location.hash;
+ setHash(h);
+ };
+ fn();
+ window.addEventListener('hashchange', fn);
+ return () => {
+ window.removeEventListener('hashchange', fn);
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!document) {
+ setloading(true);
+ const timer = setTimeout(() => {
+ setloading(false);
+ }, 1000);
+ return () => clearTimeout(timer);
+ }
+ },[document])
+
+ if (!rawSpec || !document) {
+ return (
+
+ {loading ?(
+
+ ) : (
+
Empty or invalid document. Please fix errors/define AsyncAPI document.
+ )
+ }
+
+ );
+ }
+
+ const components = document.components();
+ return (
+
+
+
+
+ navigationSvc.scrollTo(
+ '/info',
+ 'introduction',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/info',
+ 'introduction',
+ );
+ }}
+ >
+ Information
+
+
+ {!document.servers().isEmpty() && (
+
+
+
+ )}
+
+
+
+ {!components.messages().isEmpty() && (
+
+
+
+ )}
+ {!components.schemas().isEmpty() && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Navigationv3.tsx b/apps/studio-next/src/components/Navigationv3.tsx
new file mode 100644
index 000000000..701a0bdba
--- /dev/null
+++ b/apps/studio-next/src/components/Navigationv3.tsx
@@ -0,0 +1,477 @@
+/* eslint-disable sonarjs/no-nested-template-literals, sonarjs/no-duplicate-string */
+
+import React, { useEffect, useState } from 'react';
+
+import { useServices } from '@/services';
+import { useDocumentsState, useFilesState } from '@/state';
+
+import type { AsyncAPIDocumentInterface } from '@asyncapi/parser';
+
+interface NavigationProps {
+ className?: string;
+}
+
+interface NavigationSectionProps {
+ document: AsyncAPIDocumentInterface;
+ rawSpec: string;
+ hash: string;
+}
+
+export const NAVIGATION_SUB_SECTION_STYLE = 'p-2 pl-6 text-white cursor-pointer text-xs border-t border-gray-700 hover:bg-gray-900';
+export const NAVIGATION_SECTION_STYLE = 'p-2 pl-3 text-white cursor-pointer hover:bg-gray-900'
+
+const ServersNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ return (
+ <>
+
+ navigationSvc.scrollTo('/servers', 'servers')
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo('/servers', 'servers');
+ }}
+ >
+ Servers
+
+
+ {document.servers().all().map((server) => {
+ const serverName = server.id();
+ return
+ navigationSvc.scrollTo(
+ `/servers/${serverName.replace(/\//g, '~1')}`,
+ `server-${serverName}`,
+ )
+ }
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/servers/${serverName.replace(/\//g, '~1')}`,
+ `server-${serverName}`,
+ );
+ }}
+ >
+
+
+
+ {server.protocolVersion()
+ ? `${server.protocol()} ${server.protocolVersion()}`
+ : server.protocol()}
+
+
+
{serverName}
+
+
+ })}
+
+ >
+ );
+};
+
+const ChannelsNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const channels = document.channels().all().map(
+ (channel) => {
+ return
+ navigationSvc.scrollTo(
+ `/channels/${(channel.id() ?? '').replace(/\//g, '~1')}`,
+ `channels-${channel.id()}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/channels/${(channel.id() ?? '').replace(/\//g, '~1')}`,
+ `channels-${channel.id()}`,
+ );
+ }}
+
+ >
+
+
+
+ {channel.id()}
+
+
+
{channel.address()}
+
+
+ },
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/channels',
+ 'channels',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/channels',
+ 'channels',
+ );
+ }}
+
+ >
+ Channels
+
+
+ >
+ );
+};
+
+const OperationsNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const operations = document.operations().all().map(
+ (operation) => {
+ const operations: React.ReactNode[] = [];
+ if (operation.isReceive()) {
+ operations.push(
+
+ navigationSvc.scrollTo(
+ `/operations/${(operation.id() ?? '').replace(/\//g, '~1')}`,
+ `operation-receive-${operation.id()}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/operations/${(operation.id() ?? '').replace(/\//g, '~1')}`,
+ `operation-receive-${operation.id()}`,
+ );
+ }}
+ >
+
+
+
+ Receive
+
+
+
{operation.id()}
+
+
+ );
+ }
+ if (operation.isSend()) {
+ operations.push(
+
+ navigationSvc.scrollTo(
+ `/operations/${(operation.id() ?? '').replace(/\//g, '~1')}`,
+ `operation-send-${operation.id()}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/operations/${(operation.id() ?? '').replace(/\//g, '~1')}`,
+ `operation-send-${operation.id()}`,
+ );
+ }}
+ >
+
+
+
+ Send
+
+
+
{operation.id()}
+
+ ,
+ );
+ }
+
+ return operations;
+ },
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/operations',
+ 'operations',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/operations',
+ 'operations',
+ );
+ }}
+ >
+ Operations
+
+
+ >
+ );
+};
+
+const MessagesNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const messages = document.components().messages().all().map(
+ message => {
+ const messageName = message.id();
+ return
+ navigationSvc.scrollTo(
+ `/components/messages/${messageName.replace(/\//g, '~1')}`,
+ `message-${messageName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/components/messages/${messageName.replace(/\//g, '~1')}`,
+ `message-${messageName}`,
+ );
+ }}
+ >
+ {messageName}
+
+ },
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/components/messages',
+ 'messages',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/components/messages',
+ 'messages',
+ );
+ }}
+ >
+ Messages
+
+
+ >
+ );
+};
+
+const SchemasNavigation: React.FunctionComponent = ({
+ document,
+ hash,
+}) => {
+ const { navigationSvc } = useServices();
+
+ const schemas = document.components().schemas().all().map(
+ schema => {
+ const schemaName = schema.id();
+ return
+ navigationSvc.scrollTo(
+ `/components/schemas/${schemaName.replace(/\//g, '~1')}`,
+ `schema-${schemaName}`,
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ `/components/schemas/${schemaName.replace(/\//g, '~1')}`,
+ `schema-${schemaName}`,
+ );
+ }}
+ >
+ {schemaName}
+
+ }
+ );
+
+ return (
+ <>
+
+ navigationSvc.scrollTo(
+ '/components/schemas',
+ 'schemas',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/components/schemas',
+ 'schemas',
+ );
+ }}
+ >
+ Schemas
+
+
+ >
+ );
+};
+
+export const Navigationv3: React.FunctionComponent = ({
+ className = '',
+}) => {
+ const [hash, setHash] = useState(window.location.hash);
+
+ const { navigationSvc } = useServices();
+ const rawSpec = useFilesState(state => state.files['asyncapi']?.content);
+ const document = useDocumentsState(state => state.documents['asyncapi']?.document);
+
+ useEffect(() => {
+ const fn = () => {
+ // remove `#` char
+ const h = window.location.hash.startsWith('#') ? window.location.hash.substring(1) : window.location.hash;
+ setHash(h);
+ };
+ fn();
+ window.addEventListener('hashchange', fn);
+ return () => {
+ window.removeEventListener('hashchange', fn);
+ };
+ }, []);
+
+ if (!rawSpec || !document) {
+ return (
+
+ Empty or invalid document. Please fix errors/define AsyncAPI document.
+
+ );
+ }
+
+ const components = document.components();
+ return (
+
+
+
+
+ navigationSvc.scrollTo(
+ '/info',
+ 'introduction',
+ )
+ }
+ tabIndex={0}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter' || event.key === ' ') navigationSvc.scrollTo(
+ '/info',
+ 'introduction',
+ );
+ }}
+ >
+ Information
+
+
+ {!document.servers().isEmpty() && (
+
+
+
+ )}
+
+
+
+
+
+
+ {!components.messages().isEmpty() && (
+
+
+
+ )}
+ {!components.schemas().isEmpty() && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Popovers/SurveyPopover.tsx b/apps/studio-next/src/components/Popovers/SurveyPopover.tsx
new file mode 100644
index 000000000..09ea423c2
--- /dev/null
+++ b/apps/studio-next/src/components/Popovers/SurveyPopover.tsx
@@ -0,0 +1,83 @@
+import { FunctionComponent } from 'react';
+
+interface SurveyPopoverProps {}
+
+export const SurveyPopover: FunctionComponent = () => {
+ return null;
+
+ // const editorState = state.useEditorState();
+ // const editorLoaded = editorState.editorLoaded.get();
+ // const [show, setShow] = useState(false);
+
+ // useEffect(() => {
+ // if (localStorage.getItem('show:survey') === 'false') return;
+ // if (editorLoaded) {
+ // setTimeout(() => {
+ // setShow(true);
+ // }, 3000);
+ // }
+ // }, [editorLoaded]);
+
+ // const closePopover = () => {
+ // localStorage.setItem('show:survey', 'false');
+ // setShow(false);
+ // };
+
+ // return (
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
Help us improve AsyncAPI Studio
+ //
We know that the best way to improve our tools is to understand our users better. Help us define your needs by completing this short survey!
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // );
+};
diff --git a/apps/studio-next/src/components/Popovers/index.ts b/apps/studio-next/src/components/Popovers/index.ts
new file mode 100644
index 000000000..61169847d
--- /dev/null
+++ b/apps/studio-next/src/components/Popovers/index.ts
@@ -0,0 +1 @@
+export * from './SurveyPopover';
\ No newline at end of file
diff --git a/apps/studio-next/src/components/Preloader.tsx b/apps/studio-next/src/components/Preloader.tsx
new file mode 100644
index 000000000..bd3a979cb
--- /dev/null
+++ b/apps/studio-next/src/components/Preloader.tsx
@@ -0,0 +1,13 @@
+import React from 'react'
+
+export default function Preloader() {
+ return (
+
+ )
+}
diff --git a/apps/studio-next/src/components/Sidebar.tsx b/apps/studio-next/src/components/Sidebar.tsx
new file mode 100644
index 000000000..00cb27fb6
--- /dev/null
+++ b/apps/studio-next/src/components/Sidebar.tsx
@@ -0,0 +1,149 @@
+import { VscListSelection, VscCode, VscOpenPreview, VscGraph, VscNewFile, VscSettingsGear } from 'react-icons/vsc';
+import { show as showModal } from '@ebay/nice-modal-react';
+
+import { Tooltip } from './common';
+import { SettingsModal, ConfirmNewFileModal } from './Modals';
+
+import { usePanelsState, panelsState, useDocumentsState } from '@/state';
+
+import type { FunctionComponent, ReactNode } from 'react';
+import type { PanelsState } from '@/state/panels.state';
+
+function updateState(panelName: keyof PanelsState['show'], type?: PanelsState['secondaryPanelType']) {
+ const settingsState = panelsState.getState();
+ let secondaryPanelType = settingsState.secondaryPanelType;
+ const newShow = { ...settingsState.show };
+
+ if (type === 'template' || type === 'visualiser') {
+ // on current type
+ if (secondaryPanelType === type) {
+ newShow[`${panelName}`] = !newShow[`${panelName}`];
+ } else {
+ secondaryPanelType = type;
+ if (newShow[`${panelName}`] === false) {
+ newShow[`${panelName}`] = true;
+ }
+ }
+ } else {
+ newShow[`${panelName}`] = !newShow[`${panelName}`];
+ }
+
+ if (!newShow.primaryPanel && !newShow.secondaryPanel) {
+ newShow.secondaryPanel = true;
+ }
+
+ panelsState.setState({
+ show: newShow,
+ secondaryPanelType,
+ });
+}
+
+interface NavItem {
+ name: string;
+ title: string;
+ isActive: boolean;
+ onClick: () => void;
+ icon: ReactNode;
+ tooltip: ReactNode;
+ enabled: boolean;
+}
+
+interface SidebarProps {}
+
+export const Sidebar: FunctionComponent = () => {
+ const { show, secondaryPanelType } = usePanelsState();
+ const document = useDocumentsState(state => state.documents['asyncapi']?.document) || null;
+ const isV3 = document?.version() === '3.0.0';
+
+ if (show.activityBar === false) {
+ return null;
+ }
+
+ let navigation: NavItem[] = [
+ // navigation
+ {
+ name: 'primarySidebar',
+ title: 'Navigation',
+ isActive: show.primarySidebar,
+ onClick: () => updateState('primarySidebar'),
+ icon: ,
+ tooltip: 'Navigation',
+ enabled: true
+ },
+ // editor
+ {
+ name: 'primaryPanel',
+ title: 'Editor',
+ isActive: show.primaryPanel,
+ onClick: () => updateState('primaryPanel'),
+ icon: ,
+ tooltip: 'Editor',
+ enabled: true
+ },
+ // template
+ {
+ name: 'template',
+ title: 'Template',
+ isActive: show.secondaryPanel && secondaryPanelType === 'template',
+ onClick: () => updateState('secondaryPanel', 'template'),
+ icon: ,
+ tooltip: 'HTML preview',
+ enabled: true
+ },
+ // visuliser
+ {
+ name: 'visualiser',
+ title: 'Visualiser',
+ isActive: show.secondaryPanel && secondaryPanelType === 'visualiser',
+ onClick: () => updateState('secondaryPanel', 'visualiser'),
+ icon: ,
+ tooltip: 'Blocks visualiser',
+ enabled: !isV3
+ },
+ // newFile
+ {
+ name: 'newFile',
+ title: 'New file',
+ isActive: false,
+ onClick: () => showModal(ConfirmNewFileModal),
+ icon: ,
+ tooltip: 'New file',
+ enabled: true
+ },
+ ];
+
+ navigation = navigation.filter(item => item.enabled);
+
+ return (
+
+
+ {navigation.map(item => (
+
+ item.onClick()}
+ className={'flex text-sm focus:outline-none border-box p-2'}
+ type="button"
+ >
+
+ {item.icon}
+
+
+
+ ))}
+
+
+
+ showModal(SettingsModal)}
+ >
+
+
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/SplitPane/Pane.tsx b/apps/studio-next/src/components/SplitPane/Pane.tsx
new file mode 100644
index 000000000..7b6bbf14e
--- /dev/null
+++ b/apps/studio-next/src/components/SplitPane/Pane.tsx
@@ -0,0 +1,34 @@
+/* eslint-disable */
+// @ts-nocheck
+
+function Pane(props) {
+ const { children, className, split, style: styleProps, size, eleRef } = props;
+
+ const classes = ['Pane', split, className];
+
+ let style = {
+ flex: 1,
+ position: 'relative',
+ outline: 'none',
+ };
+
+ if (size !== undefined) {
+ if (split === 'vertical') {
+ style.width = size;
+ } else {
+ style.height = size;
+ style.display = 'flex';
+ }
+ style.flex = 'none';
+ }
+
+ style = Object.assign({}, style, styleProps || {});
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default Pane;
\ No newline at end of file
diff --git a/apps/studio-next/src/components/SplitPane/Readme.md b/apps/studio-next/src/components/SplitPane/Readme.md
new file mode 100644
index 000000000..bef5d6984
--- /dev/null
+++ b/apps/studio-next/src/components/SplitPane/Readme.md
@@ -0,0 +1,9 @@
+MIT License
+
+Copyright (c) 2022 Jeremy Grieshop
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/apps/studio-next/src/components/SplitPane/Resizer.tsx b/apps/studio-next/src/components/SplitPane/Resizer.tsx
new file mode 100644
index 000000000..d99d9c29e
--- /dev/null
+++ b/apps/studio-next/src/components/SplitPane/Resizer.tsx
@@ -0,0 +1,50 @@
+/* eslint-disable */
+// @ts-nocheck
+
+export const RESIZER_DEFAULT_CLASSNAME = 'Resizer';
+
+function Resizer(props) {
+ const {
+ className,
+ onClick,
+ onDoubleClick,
+ onMouseDown,
+ onTouchEnd,
+ onTouchStart,
+ resizerClassName = RESIZER_DEFAULT_CLASSNAME,
+ split,
+ style,
+ } = props;
+ const classes = [resizerClassName, split, className];
+
+ return (
+ onMouseDown(event)}
+ onTouchStart={event => {
+ event.preventDefault();
+ onTouchStart(event);
+ }}
+ onTouchEnd={event => {
+ event.preventDefault();
+ onTouchEnd(event);
+ }}
+ onClick={event => {
+ if (onClick) {
+ event.preventDefault();
+ onClick(event);
+ }
+ }}
+ onDoubleClick={event => {
+ if (onDoubleClick) {
+ event.preventDefault();
+ onDoubleClick(event);
+ }
+ }}
+ />
+ );
+}
+
+export default Resizer;
diff --git a/apps/studio-next/src/components/SplitPane/SplitPane.tsx b/apps/studio-next/src/components/SplitPane/SplitPane.tsx
new file mode 100644
index 000000000..526a80686
--- /dev/null
+++ b/apps/studio-next/src/components/SplitPane/SplitPane.tsx
@@ -0,0 +1,322 @@
+/* eslint-disable */
+// @ts-nocheck
+
+import React, { useEffect, useState, useCallback, useRef } from 'react';
+
+import Pane from './Pane';
+import Resizer, { RESIZER_DEFAULT_CLASSNAME } from './Resizer';
+
+function unFocus(document, window) {
+ if (document.selection) {
+ document.selection.empty();
+ } else {
+ try {
+ window.getSelection().removeAllRanges();
+ // eslint-disable-next-line no-empty
+ } catch (e) {}
+ }
+}
+
+function getDefaultSize(defaultSize, minSize, maxSize, draggedSize) {
+ if (typeof draggedSize === 'number') {
+ const min = typeof minSize === 'number' ? minSize : 0;
+ const max = typeof maxSize === 'number' && maxSize >= 0 ? maxSize : Infinity;
+ return Math.max(min, Math.min(max, draggedSize));
+ }
+ if (defaultSize !== undefined) {
+ return defaultSize;
+ }
+ return minSize;
+}
+
+function removeNullChildren(children) {
+ return React.Children.toArray(children).filter(c => c);
+}
+
+function SplitPane(props) {
+ const {
+ allowResize = true,
+ children,
+ className,
+ defaultSize,
+ minSize = 50,
+ maxSize,
+ onChange,
+ onDragFinished,
+ onDragStarted,
+ onResizerClick,
+ onResizerDoubleClick,
+ paneClassName = '',
+ pane1ClassName = '',
+ pane2ClassName = '',
+ paneStyle,
+ primary = 'first',
+ pane1Style: pane1StyleProps,
+ pane2Style: pane2StyleProps,
+ resizerClassName,
+ resizerStyle,
+ size,
+ split = 'vertical',
+ step,
+ style: styleProps,
+ } = props;
+
+ const initialSize =
+ size !== undefined ? size : getDefaultSize(defaultSize, minSize, maxSize, null);
+
+ const [active, setActive] = useState(false);
+ const [, setResized] = useState(false);
+ const [pane1Size, setPane1Size] = useState(primary === 'first' ? initialSize : undefined);
+ const [pane2Size, setPane2Size] = useState(primary === 'second' ? initialSize : undefined);
+ const [draggedSize, setDraggedSize] = useState();
+ const [position, setPosition] = useState();
+
+ const splitPane = useRef();
+ const pane1 = useRef();
+ const pane2 = useRef();
+ const instanceProps = useRef({ size });
+
+ const getSizeUpdate = useCallback(() => {
+ if (instanceProps.current.size === size && size !== undefined) {
+ return undefined;
+ }
+
+ const newSize =
+ size !== undefined ? size : getDefaultSize(defaultSize, minSize, maxSize, draggedSize);
+
+ if (size !== undefined) {
+ setDraggedSize(newSize);
+ }
+
+ const isPanel1Primary = primary === 'first';
+ if (isPanel1Primary) {
+ setPane1Size(newSize);
+ setPane2Size(undefined);
+ } else {
+ setPane2Size(newSize);
+ setPane1Size(undefined);
+ }
+
+ instanceProps.current.size = newSize;
+ }, [defaultSize, draggedSize, maxSize, minSize, primary, size]);
+
+ const onMouseUp = useCallback(() => {
+ if (allowResize && active) {
+ if (typeof onDragFinished === 'function') {
+ onDragFinished(draggedSize);
+ }
+ setActive(false);
+ }
+ }, [active, allowResize, draggedSize, onDragFinished]);
+
+ const onTouchMove = useCallback(
+ event => {
+ if (allowResize && active) {
+ unFocus(document, window);
+ const isPrimaryFirst = primary === 'first';
+ const ref = isPrimaryFirst ? pane1.current : pane2.current;
+ const ref2 = isPrimaryFirst ? pane2.current : pane1.current;
+ if (ref) {
+ const node = ref;
+ const node2 = ref2;
+
+ if (node.getBoundingClientRect) {
+ const width = node.getBoundingClientRect().width;
+ const height = node.getBoundingClientRect().height;
+ const current =
+ split === 'vertical' ? event.touches[0].clientX : event.touches[0].clientY;
+ const size = split === 'vertical' ? width : height;
+ let positionDelta = position - current;
+ if (step) {
+ if (Math.abs(positionDelta) < step) {
+ return;
+ }
+ // Integer division
+ // eslint-disable-next-line no-bitwise
+ positionDelta = ~~(positionDelta / step) * step;
+ }
+ let sizeDelta = isPrimaryFirst ? positionDelta : -positionDelta;
+
+ const pane1Order = parseInt(window.getComputedStyle(node).order);
+ const pane2Order = parseInt(window.getComputedStyle(node2).order);
+ if (pane1Order > pane2Order) {
+ sizeDelta = -sizeDelta;
+ }
+
+ let newMaxSize = maxSize;
+ if (maxSize !== undefined && maxSize <= 0) {
+ if (split === 'vertical') {
+ newMaxSize = splitPane.current.getBoundingClientRect().width + maxSize;
+ } else {
+ newMaxSize = splitPane.current.getBoundingClientRect().height + maxSize;
+ }
+ }
+
+ let newSize = size - sizeDelta;
+ const newPosition = position - positionDelta;
+
+ if (newSize < minSize) {
+ newSize = minSize;
+ } else if (maxSize !== undefined && newSize > newMaxSize) {
+ newSize = newMaxSize;
+ } else {
+ setPosition(newPosition);
+ setResized(true);
+ }
+
+ if (onChange) onChange(newSize);
+
+ setDraggedSize(newSize);
+ if (isPrimaryFirst) setPane1Size(newSize);
+ else setPane2Size(newSize);
+ }
+ }
+ }
+ },
+ [active, allowResize, maxSize, minSize, onChange, position, primary, split, step],
+ );
+
+ const onMouseMove = useCallback(
+ event => {
+ const eventWithTouches = Object.assign({}, event, {
+ touches: [{ clientX: event.clientX, clientY: event.clientY }],
+ });
+ onTouchMove(eventWithTouches);
+ },
+ [onTouchMove],
+ );
+
+ const onTouchStart = useCallback(
+ event => {
+ if (allowResize) {
+ unFocus(document, window);
+ const position = split === 'vertical' ? event.touches[0].clientX : event.touches[0].clientY;
+
+ if (typeof onDragStarted === 'function') {
+ onDragStarted();
+ }
+ setActive(true);
+ setPosition(position);
+ }
+ },
+ [allowResize, onDragStarted, split],
+ );
+
+ const onMouseDown = useCallback(
+ event => {
+ const eventWithTouches = Object.assign({}, event, {
+ touches: [{ clientX: event.clientX, clientY: event.clientY }],
+ });
+ onTouchStart(eventWithTouches);
+ },
+ [onTouchStart],
+ );
+
+ useEffect(() => {
+ document.addEventListener('mouseup', onMouseUp);
+ document.addEventListener('mousemove', onMouseMove);
+ document.addEventListener('touchmove', onTouchMove);
+
+ getSizeUpdate();
+
+ return () => {
+ document.removeEventListener('mouseup', onMouseUp);
+ document.removeEventListener('mousemove', onMouseMove);
+ document.removeEventListener('touchmove', onTouchMove);
+ };
+ }, [onMouseUp, onMouseMove, onTouchMove, getSizeUpdate]);
+
+ const disabledClass = allowResize ? '' : 'disabled';
+ const resizerClassNamesIncludingDefault = resizerClassName
+ ? `${resizerClassName} ${RESIZER_DEFAULT_CLASSNAME}`
+ : resizerClassName;
+
+ const notNullChildren = removeNullChildren(children);
+
+ const style = {
+ display: 'flex',
+ flex: 1,
+ height: '100%',
+ position: 'absolute',
+ outline: 'none',
+ overflow: 'hidden',
+ MozUserSelect: 'text',
+ WebkitUserSelect: 'text',
+ msUserSelect: 'text',
+ userSelect: 'text',
+ ...styleProps,
+ };
+
+ if (split === 'vertical') {
+ Object.assign(style, {
+ flexDirection: 'row',
+ left: 0,
+ right: 0,
+ });
+ } else {
+ Object.assign(style, {
+ bottom: 0,
+ flexDirection: 'column',
+ minHeight: '100%',
+ top: 0,
+ width: '100%',
+ });
+ }
+
+ const classes = ['SplitPane', className, split, disabledClass];
+
+ const pane1Style = { ...paneStyle, ...pane1StyleProps };
+ const pane2Style = { ...paneStyle, ...pane2StyleProps };
+
+ const pane1Classes = ['Pane1', paneClassName, pane1ClassName].join(' ');
+ const pane2Classes = ['Pane2', paneClassName, pane2ClassName].join(' ');
+
+ return (
+ {
+ splitPane.current = node;
+ }}
+ style={style}
+ >
+
{
+ pane1.current = node;
+ }}
+ size={pane1Size}
+ split={split}
+ style={pane1Style}
+ >
+ {notNullChildren[0]}
+
+
+
{
+ pane2.current = node;
+ }}
+ size={pane2Size}
+ split={split}
+ style={pane2Style}
+ >
+ {notNullChildren[1]}
+
+
+ );
+}
+
+export default SplitPane;
\ No newline at end of file
diff --git a/apps/studio-next/src/components/SplitPane/index.tsx b/apps/studio-next/src/components/SplitPane/index.tsx
new file mode 100644
index 000000000..18bcb3786
--- /dev/null
+++ b/apps/studio-next/src/components/SplitPane/index.tsx
@@ -0,0 +1,7 @@
+import ReactSplitPane from './SplitPane';
+import Pane from './Pane';
+/**
+ * Implementation from https://github.com/JeremyGrieshop/react-split-pane/blob/main/SplitPane.js
+ */
+export default ReactSplitPane;
+export { Pane };
\ No newline at end of file
diff --git a/apps/studio-next/src/components/StudioWrapper.tsx b/apps/studio-next/src/components/StudioWrapper.tsx
new file mode 100644
index 000000000..97216e033
--- /dev/null
+++ b/apps/studio-next/src/components/StudioWrapper.tsx
@@ -0,0 +1,54 @@
+'use client'
+import { StrictMode, useEffect, useState } from 'react';
+import { Provider as ModalsProvider } from '@ebay/nice-modal-react';
+
+import { createServices, Services, ServicesProvider } from '@/services';
+
+import { AsyncAPIStudio } from './CodeEditor';
+import Preloader from './Preloader';
+
+function configureMonacoEnvironment() {
+ if (typeof window !== 'undefined') {
+ window.MonacoEnvironment = {
+ getWorker(_, label) {
+ switch (label) {
+ case 'editorWorkerService':
+ return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url));
+ case 'json':
+ return new Worker(
+ new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url),
+ );
+ case 'yaml':
+ case 'yml':
+ return new Worker(new URL('monaco-yaml/yaml.worker', import.meta.url));
+ default:
+ throw new Error(`Unknown worker ${label}`);
+ }
+ },
+ };
+ }
+}
+
+export default function StudioWrapper() {
+ const [services, setServices] = useState();
+ useEffect(() => {
+ const fetchData = async () => {
+ const servicess = await createServices();
+ setServices(servicess)
+ configureMonacoEnvironment();
+ };
+
+ fetchData();
+ }, []);
+
+ if (!services) return
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/studio-next/src/components/Template/HTMLWrapper.tsx b/apps/studio-next/src/components/Template/HTMLWrapper.tsx
new file mode 100644
index 000000000..cafc659e5
--- /dev/null
+++ b/apps/studio-next/src/components/Template/HTMLWrapper.tsx
@@ -0,0 +1,76 @@
+import React, { useState, useEffect } from 'react';
+import { AsyncApiComponentWP } from '@asyncapi/react-component';
+
+import { useServices } from '../../services';
+import { appState, useDocumentsState, useSettingsState, useOtherState, otherState } from '../../state';
+
+import { AsyncAPIDocumentInterface } from '@asyncapi/parser';
+
+interface HTMLWrapperProps {}
+
+export const HTMLWrapper: React.FunctionComponent = () => {
+ const [parsedSpec, setParsedSpec] = useState(null);
+ const { navigationSvc } = useServices();
+ const document = useDocumentsState(state => state.documents['asyncapi']?.document) || null;
+ const [loading, setloading] = useState(false);
+
+ const autoRendering = useSettingsState(state => state.templates.autoRendering);
+ const templateRerender = useOtherState(state => state.templateRerender);
+
+ useEffect(() => {
+ navigationSvc.scrollToHash();
+ }, []); // eslint-disable-line
+
+ useEffect(() => {
+ if (autoRendering || parsedSpec === null) {
+ setParsedSpec(document);
+ }
+ }, [document]); // eslint-disable-line
+
+ useEffect(() => {
+ if (templateRerender) {
+ setParsedSpec(document);
+ otherState.setState({ templateRerender: false });
+ }
+ }, [templateRerender]); // eslint-disable-line
+
+ useEffect(() => {
+ if (!document) {
+ setloading(true);
+ const timer = setTimeout(() => {
+ setloading(false);
+ }, 1000);
+ return () => clearTimeout(timer);
+ }
+ },[document])
+ if (!document) {
+ return (
+
+ {loading ?(
+
+ ) : (
+
Empty or invalid document. Please fix errors/define AsyncAPI document.
+ )
+ }
+
+ );
+ }
+
+ return (
+ parsedSpec && (
+
+ )
+ );
+};
diff --git a/apps/studio-next/src/components/Template/Template.tsx b/apps/studio-next/src/components/Template/Template.tsx
new file mode 100644
index 000000000..912a38527
--- /dev/null
+++ b/apps/studio-next/src/components/Template/Template.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+
+import { TemplateSidebar } from './TemplateSidebar';
+import { HTMLWrapper } from './HTMLWrapper';
+
+import { appState } from '../../state';
+
+interface TemplateProps {}
+
+export const Template: React.FunctionComponent = () => {
+ return (
+
+ {!appState.getState().readOnly && }
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Template/TemplateSidebar.tsx b/apps/studio-next/src/components/Template/TemplateSidebar.tsx
new file mode 100644
index 000000000..bfd9a6e95
--- /dev/null
+++ b/apps/studio-next/src/components/Template/TemplateSidebar.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { VscRefresh } from 'react-icons/vsc';
+
+import { useSettingsState, otherState } from '../../state';
+
+interface TemplateSidebarProps {}
+
+export const TemplateSidebar: React.FunctionComponent = () => {
+ const autoRendering = useSettingsState(state => state.templates.autoRendering);
+
+ return (
+
+ {autoRendering ? (
+
+ ) : (
+
+
otherState.setState({ templateRerender: true })}>
+
+
+
+
+
+ Rerender
+
+
+ )}
+
+ );
+};
diff --git a/apps/studio-next/src/components/Template/index.ts b/apps/studio-next/src/components/Template/index.ts
new file mode 100644
index 000000000..1972b01e5
--- /dev/null
+++ b/apps/studio-next/src/components/Template/index.ts
@@ -0,0 +1,2 @@
+export * from './Template';
+export * from './HTMLWrapper';
diff --git a/apps/studio-next/src/components/Terminal/ProblemsTab.tsx b/apps/studio-next/src/components/Terminal/ProblemsTab.tsx
new file mode 100644
index 000000000..58005aa8d
--- /dev/null
+++ b/apps/studio-next/src/components/Terminal/ProblemsTab.tsx
@@ -0,0 +1,306 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react';
+import { VscError, VscWarning, VscInfo, VscLightbulb, VscSearch, VscClose, VscSettingsGear } from 'react-icons/vsc';
+import { useModal } from '@ebay/nice-modal-react';
+import { DiagnosticSeverity } from '@asyncapi/parser';
+
+import { SettingsModal } from '../Modals/Settings/SettingsModal';
+
+import { Tooltip } from '../common';
+import { useServices } from '../../services';
+import { debounce } from '../../helpers';
+import { useDocumentsState, useSettingsState } from '../../state';
+
+import { FunctionComponent } from 'react';
+import type { Diagnostic } from '@asyncapi/parser';
+
+interface ProblemsTabProps {}
+
+export const ProblemsTab: FunctionComponent = () => {
+ const diagnostics = useDocumentsState(state => state.documents['asyncapi'].diagnostics);
+
+ const errorDiagnosticsLength = diagnostics.errors.length;
+ const warningDiagnosticsLength = diagnostics.warnings.length;
+ const informationDiagnosticsLength = diagnostics.informations.length;
+ const hintDiagnosticsLength = diagnostics.hints.length;
+
+ return (
+
+
Diagnostics
+
+ {errorDiagnosticsLength > 0 && (
+
+
+ {errorDiagnosticsLength}
+
+
+ )}
+ {warningDiagnosticsLength > 0 && (
+
+
+ {warningDiagnosticsLength}
+
+
+ )}
+ {informationDiagnosticsLength > 0 && (
+
+
+ {informationDiagnosticsLength}
+
+
+ )}
+ {hintDiagnosticsLength > 0 && (
+
+
+ {hintDiagnosticsLength}
+
+
+ )}
+
+
+ );
+};
+
+interface SeverityIconProps {
+ severity: Diagnostic['severity']
+}
+
+const SeverityIcon: React.FunctionComponent = ({ severity }) => {
+ switch (severity) {
+ case 1: return (
+
+
+
+ );
+ case 2: return (
+
+
+
+ );
+ case 3: return (
+
+
+
+ );
+ default: return (
+
+
+
+ );
+ }
+};
+
+function createProperMessage(
+ disabled: boolean,
+ active: DiagnosticSeverity[],
+ severity: DiagnosticSeverity,
+ showMessage: string,
+ hideMessage: string,
+ firstMessage: string,
+) {
+ if (disabled) {
+ return 'Disabled. Enable it in the settings.';
+ }
+ if (active.some(s => s === severity)) {
+ if (active.length === 1) {
+ return 'Show all diagnostics';
+ }
+ return hideMessage;
+ }
+ if (active.length === 0) {
+ return firstMessage;
+ }
+ return showMessage;
+}
+
+interface SeverityButtonsProps {
+ active: DiagnosticSeverity[];
+ setActive: (severity: DiagnosticSeverity) => void;
+}
+
+const SeverityButtons: FunctionComponent = ({ active, setActive }) => {
+ const diagnostics = useDocumentsState(state => state.documents['asyncapi'].diagnostics);
+ const governanceShowState = useSettingsState(state => state.governance.show);
+
+ const errorDiagnostics = diagnostics.errors;
+ const warningDiagnostics = diagnostics.warnings;
+ const infoDiagnostics = diagnostics.informations;
+ const hintDiagnostics = diagnostics.hints;
+
+ const errorsTooltip = createProperMessage(false, active, DiagnosticSeverity.Error, 'Show errors', 'Hide errors', 'Show only errors');
+ const warningsTooltip = createProperMessage(!governanceShowState.warnings, active, DiagnosticSeverity.Warning, 'Show warnings', 'Hide warnings', 'Show only warnings');
+ const informationTooltip = createProperMessage(!governanceShowState.informations, active, DiagnosticSeverity.Information, 'Show information messages', 'Hide information messages', 'Show only information messages');
+ const hintsTooltip = createProperMessage(!governanceShowState.hints, active, DiagnosticSeverity.Hint, 'Show hints', 'Hide hints', 'Show only hints');
+
+ const activeBg = 'bg-gray-900';
+ const notActiveBg = 'bg-gray-700';
+
+ return (
+
+
+
+ s === DiagnosticSeverity.Error) ? activeBg : notActiveBg} text-xs font-medium text-white hover:bg-gray-900 disabled:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-700`}
+ onClick={() => setActive(DiagnosticSeverity.Error)}
+ >
+
+
+ {errorDiagnostics.length}
+
+
+
+
+
+
+ s === DiagnosticSeverity.Warning) ? activeBg : notActiveBg} text-xs font-medium text-white hover:bg-gray-900 disabled:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-700`}
+ onClick={() => setActive(DiagnosticSeverity.Warning)}
+ disabled={!governanceShowState.warnings}
+ >
+
+
+ {warningDiagnostics.length}
+
+
+
+
+
+
+ s === DiagnosticSeverity.Information) ? activeBg : notActiveBg} text-xs font-medium text-white hover:bg-gray-900 disabled:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-700`}
+ onClick={() => setActive(DiagnosticSeverity.Information)}
+ disabled={!governanceShowState.informations}
+ >
+
+
+ {infoDiagnostics.length}
+
+
+
+
+
+
+ s === DiagnosticSeverity.Hint) ? activeBg : notActiveBg} text-xs font-medium text-white hover:bg-gray-900 disabled:bg-gray-700 focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-700`}
+ onClick={() => setActive(DiagnosticSeverity.Hint)}
+ disabled={!governanceShowState.hints}
+ >
+
+
+ {hintDiagnostics.length}
+
+
+
+
+
+ );
+};
+
+export const ProblemsTabContent: FunctionComponent = () => {
+ const { navigationSvc } = useServices();
+ const diagnostics = useDocumentsState(state => state.documents['asyncapi'].diagnostics);
+ const modal = useModal(SettingsModal);
+
+ const [active, setActive] = useState>([]);
+ const [search, setSearch] = useState('');
+ const inputRef = useRef(null);
+
+ const setActiveFn = useCallback((severity: DiagnosticSeverity) => {
+ setActive(acc => {
+ if (acc.some(s => s === severity)) {
+ return acc.filter(s => s !== severity);
+ }
+ return [...acc, severity];
+ });
+ }, [setActive]);
+
+ const filteredDiagnostics = useMemo(() => {
+ return diagnostics.filtered.filter(diagnostic => {
+ const { severity, message } = diagnostic;
+
+ if (active.length && !active.some(s => s === severity)) {
+ return false;
+ }
+
+ const lowerCasingSearch = search.toLowerCase();
+ return !(lowerCasingSearch && !message.toLowerCase().includes(lowerCasingSearch));
+ });
+ }, [diagnostics, search, active]);
+
+ return (
+
+
+
+
+
+
+ setSearch(e.target.value), 250)} />
+ {
+ if (inputRef.current) {
+ inputRef.current.value = '';
+ }
+ setSearch('');
+ }}>
+
+
+
+
+ modal.show({ activeTab: 'governance' })}
+ >
+
+
+
+
+
+
+
+
+ Type
+ Line
+ Message
+
+
+
+ {filteredDiagnostics.map((diagnostic, id) => {
+ const { severity, message, range } = diagnostic;
+
+ return (
+
+
+
+ navigationSvc.scrollToEditorLine(
+ range.start.line + 1,
+ range.start.character + 1,
+ )
+ }
+ >
+ {range.start.line + 1}:{range.start.character + 1}
+
+ {message}
+
+ );
+ })}
+
+
+ {filteredDiagnostics.length === 0 && !search && (
+
+ No issues.
+
+ )}
+ {filteredDiagnostics.length === 0 && search && (
+
+ No results for "{search}".
+
+ )}
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Terminal/Terminal.tsx b/apps/studio-next/src/components/Terminal/Terminal.tsx
new file mode 100644
index 000000000..beb0afa9f
--- /dev/null
+++ b/apps/studio-next/src/components/Terminal/Terminal.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+
+import { TerminalTabs, TerminalTab } from './TerminalTabs';
+import { ProblemsTab, ProblemsTabContent } from './ProblemsTab';
+
+interface TerminalProps {}
+
+export const Terminal: React.FunctionComponent = () => {
+ const tabs: Array = [
+ {
+ name: 'problems',
+ tab: ,
+ content: ,
+ },
+ ];
+
+ return (
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Terminal/TerminalInfo.tsx b/apps/studio-next/src/components/Terminal/TerminalInfo.tsx
new file mode 100644
index 000000000..b19c17e98
--- /dev/null
+++ b/apps/studio-next/src/components/Terminal/TerminalInfo.tsx
@@ -0,0 +1,114 @@
+import { useCallback } from 'react';
+import { VscRadioTower } from 'react-icons/vsc';
+import { show } from '@ebay/nice-modal-react';
+
+import { ConvertToLatestModal } from '../Modals';
+
+import { useServices } from '../../services';
+import { useAppState, useDocumentsState, useFilesState, useSettingsState } from '../../state';
+
+import type { FunctionComponent } from 'react';
+
+interface TerminalInfoProps {}
+
+export const TerminalInfo: FunctionComponent = () => {
+ const { specificationSvc } = useServices();
+ const file = useFilesState(state => state.files['asyncapi']);
+ const document = useDocumentsState(state => state.documents['asyncapi']);
+ const autoSaving = useSettingsState(state => state.editor.autoSaving);
+
+ const liveServer = useAppState(state => state.liveServer);
+ const actualVersion = document.document?.version() || '2.0.0';
+ const latestVersion = specificationSvc.latestVersion;
+
+ const onNonLatestClick = useCallback((e: {stopPropagation: ()=>void}) => {
+ e.stopPropagation();
+ show(ConvertToLatestModal);
+ }, []);
+
+ return (
+
+ {liveServer && (
+
+
+
+
+ Live server
+
+ )}
+ {document.diagnostics.errors.length > 0 ? (
+
+ ) : (
+
+ )}
+ {!autoSaving && file.modified && (
+
+ )}
+
+
+
+
+
+
+
{autoSaving ? 'Autosave: On' : 'Autosave: Off'}
+
+ {actualVersion !== latestVersion && document.valid === true && (
+
{
+ if (event.key === 'Enter' || event.key === ' ') onNonLatestClick(event);
+ }}>
+
+
+
+
+
+
Not latest
+
+ )}
+
+ {file.language}
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Terminal/TerminalTabs.tsx b/apps/studio-next/src/components/Terminal/TerminalTabs.tsx
new file mode 100644
index 000000000..47296a5a7
--- /dev/null
+++ b/apps/studio-next/src/components/Terminal/TerminalTabs.tsx
@@ -0,0 +1,103 @@
+import React, { useState } from 'react';
+
+import { TerminalInfo } from './TerminalInfo';
+import { otherState, useDocumentsState } from '../../state';
+
+export interface TerminalTab {
+ name: string;
+ tab: React.ReactNode;
+ content: React.ReactNode;
+}
+
+interface TerminalTabsProps {
+ tabs: Array;
+ active?: string;
+}
+
+const onTerminalTabClickHandler = (e: {currentTarget: {parentElement: any}}) => {
+ const clientRects = e.currentTarget.parentElement?.parentElement?.getClientRects()[0];
+ if (!clientRects) return;
+
+ const height = clientRects.height;
+ const calc160px = 'calc(100% - 160px)';
+ const calc36px = 'calc(100% - 36px)';
+
+ const prevHeight = otherState.getState().editorHeight;
+ const newHeight =
+ height < 50 ? calc160px : calc36px;
+ if (
+ prevHeight === calc160px &&
+ newHeight === calc160px
+ ) {
+ return 'calc(100% - 161px)';
+ }
+ if (
+ prevHeight === calc36px &&
+ newHeight === calc36px
+ ) {
+ return 'calc(100% - 37px)';
+ }
+
+ otherState.setState({ editorHeight: newHeight });
+}
+
+export const TerminalTabs: React.FunctionComponent = ({
+ tabs = [],
+ active = 0,
+}) => {
+ const [activeTab, setActiveTab] = useState(active);
+ const document = useDocumentsState(state => state.documents['asyncapi']?.document) || null;
+ const isV3 = document?.version() === '3.0.0';
+ if (tabs.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+
+ {!isV3 && tabs.map(tab => (
+ setActiveTab(tab.name)}
+ tabIndex={0}
+ onKeyDown={() => setActiveTab(tab.name)}
+ >
+
+ {tab.tab}
+
+
+ ))}
+
+
+
+
+
+ {tabs.map(tab => (
+
+ {tab.content}
+
+ ))}
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Terminal/index.ts b/apps/studio-next/src/components/Terminal/index.ts
new file mode 100644
index 000000000..39f1492ee
--- /dev/null
+++ b/apps/studio-next/src/components/Terminal/index.ts
@@ -0,0 +1 @@
+export * from './Terminal';
diff --git a/apps/studio-next/src/components/Toolbar.tsx b/apps/studio-next/src/components/Toolbar.tsx
new file mode 100644
index 000000000..4c0f85a27
--- /dev/null
+++ b/apps/studio-next/src/components/Toolbar.tsx
@@ -0,0 +1,42 @@
+
+import { IoGlobeOutline, IoLogoGithub, IoLogoSlack } from 'react-icons/io5';
+
+export function Toolbar() {
+ return (
+
+
+
+
+
+
+
+ beta
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/studio-next/src/components/Visualiser/Controls.tsx b/apps/studio-next/src/components/Visualiser/Controls.tsx
new file mode 100644
index 000000000..238a4f2ff
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/Controls.tsx
@@ -0,0 +1,43 @@
+import { useState, useEffect } from 'react';
+import { useStore, useReactFlow, useNodes, useEdges } from 'reactflow';
+import { VscDebugStart, VscDebugPause, VscRefresh } from 'react-icons/vsc';
+
+import { calculateNodesForDynamicLayout } from './utils/node-calculator';
+
+import { FunctionComponent } from 'react';
+
+interface ControlsProps {}
+
+export const Controls: FunctionComponent = () => {
+ const [animateNodes, setAnimateNodes] = useState(false);
+
+ const { fitView } = useReactFlow();
+ const nodes = useNodes();
+ const edges = useEdges();
+ const setNodes = useStore(state => state.setNodes);
+ const setEdges = useStore(state => state.setEdges);
+
+ useEffect(() => {
+ if (nodes.length > 0) {
+ const newNodeEdges = edges.map(edge => ({ ...edge, animated: animateNodes }));
+ setEdges([...newNodeEdges]);
+ }
+ }, [animateNodes]);
+
+ const reloadInterface = () => {
+ setNodes(calculateNodesForDynamicLayout(nodes));
+ fitView();
+ };
+
+ return (
+
+ setAnimateNodes(!animateNodes)}>
+ {animateNodes && }
+ {!animateNodes && }
+
+
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Visualiser/FlowDiagram.tsx b/apps/studio-next/src/components/Visualiser/FlowDiagram.tsx
new file mode 100644
index 000000000..c25862486
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/FlowDiagram.tsx
@@ -0,0 +1,77 @@
+import { useEffect } from 'react';
+import ReactFlow, { Controls as FlowControls, Background, BackgroundVariant, useReactFlow, useStore, useNodesState, useEdgesState, useNodes } from 'reactflow';
+
+import NodeTypes from './Nodes';
+import { Controls } from './Controls';
+import { getElementsFromAsyncAPISpec } from './utils/node-factory';
+import { calculateNodesForDynamicLayout } from './utils/node-calculator';
+
+import type { OldAsyncAPIDocument as AsyncAPIDocument } from '@asyncapi/parser';
+import { FunctionComponent } from 'react';
+
+interface FlowDiagramProps {
+ parsedSpec: AsyncAPIDocument;
+}
+
+interface AutoLayoutProps {}
+
+const AutoLayout: FunctionComponent = () => {
+ const { fitView } = useReactFlow();
+ const nodes = useNodes();
+ const setNodes = useStore(state => state.setNodes);
+
+ useEffect(() => {
+ if (nodes.length === 0 || !nodes[0].width) {
+ return;
+ }
+
+ const nodesWithOrginalPosition = nodes.filter(node => node.position.x === 0 && node.position.y === 0);
+ if (nodesWithOrginalPosition.length > 1) {
+ const calculatedNodes = calculateNodesForDynamicLayout(nodes);
+ setNodes(calculatedNodes);
+ fitView();
+ }
+ }, [nodes]);
+
+ return null;
+};
+
+export const FlowDiagram: FunctionComponent = ({ parsedSpec }) => {
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+
+ useEffect(() => {
+ const elements = getElementsFromAsyncAPISpec(parsedSpec);
+ const newNodes = elements.map(el => el.node).filter(Boolean);
+ const newEdges = elements.map(el => el.edge).filter(Boolean);
+
+ setNodes(newNodes);
+ setEdges(newEdges);
+ }, [parsedSpec]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Event Visualiser
+ |
+ {parsedSpec.info().title()}
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Visualiser/Nodes/ApplicationNode.tsx b/apps/studio-next/src/components/Visualiser/Nodes/ApplicationNode.tsx
new file mode 100644
index 000000000..c7a0bb8b4
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/Nodes/ApplicationNode.tsx
@@ -0,0 +1,166 @@
+import { useState, useEffect } from 'react';
+import { Handle, Position } from 'reactflow';
+import { OldAsyncAPIDocument as AsyncAPIDocument } from '@asyncapi/parser';
+
+import { useServices } from '@/services';
+import { Markdown } from '../../common';
+
+import { FunctionComponent } from 'react';
+
+interface IData {
+ spec: AsyncAPIDocument
+}
+
+interface ApplicationNodeProps {
+ data: IData
+}
+
+const buildNodeData = (spec: AsyncAPIDocument) => {
+ const servers = spec.servers();
+
+ const mappedServers = Object.keys(servers).reduce((newMappedServers: any[], serverKey) => {
+ const server = servers[String(serverKey)];
+
+ newMappedServers.push({
+ name: serverKey,
+ url: server.url(),
+ description: server.description(),
+ protocol: server.protocol(),
+ protocolVersion: server.protocolVersion(),
+ });
+ return newMappedServers;
+ }, []);
+
+ const specInfo = spec.info();
+
+ return {
+ defaultContentType: spec.defaultContentType(),
+ description: specInfo.description(),
+ title: specInfo.title(),
+ version: specInfo.version(),
+ license: {
+ name: specInfo.license() && specInfo.license()?.name(),
+ url: specInfo.license() && specInfo.license()?.url(),
+ },
+ // @ts-ignore
+ externalDocs: spec.externalDocs() && spec.externalDocs().url(),
+ servers: mappedServers,
+ };
+};
+
+export const ApplicationNode: FunctionComponent = ({
+ data: { spec } = {},
+}) => {
+ const { navigationSvc } = useServices();
+ const [highlight, setHighlight] = useState(false);
+ const { description, title, version, license, externalDocs, servers, defaultContentType } = buildNodeData(spec as AsyncAPIDocument);
+
+ useEffect(() => {
+ return navigationSvc.highlightVisualiserNode('#server', setHighlight);
+ }, [navigationSvc, setHighlight]);
+
+ return (
+
+
+
+
+ In
+
+
+
+
+
+
+ application
+
+
+
+
+
{title}
+
+ v{version}
+
+
+ {description && (
+
+
+ {description}
+
+
+ )}
+ {defaultContentType && (
+
+ Default ContentType:{' '}
+
+ {defaultContentType}
+
+
+ )}
+
+
+ {servers.length > 0 && (
+
+
Servers
+
+ {servers.map((server) => {
+ return (
+
+
+ {server.name}
+
+ {server.protocolVersion
+ ? `${server.protocol} ${server.protocolVersion}`
+ : server.protocol}
+
+
+
+
+ {server.description}
+
+
+ url: {server.url}
+
+ );
+ })}
+
+
+ )}
+
+
+
+
+
+ Out
+
+
+
+
+ );
+};
+
+export default ApplicationNode;
diff --git a/apps/studio-next/src/components/Visualiser/Nodes/PublishNode.tsx b/apps/studio-next/src/components/Visualiser/Nodes/PublishNode.tsx
new file mode 100644
index 000000000..85eec2879
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/Nodes/PublishNode.tsx
@@ -0,0 +1,89 @@
+import { useState, useEffect } from 'react';
+import { Handle, Position } from 'reactflow';
+
+import { useServices } from '@/services';
+import getBackgroundColor from '../utils/random-background-color';
+
+// @ts-ignore
+import { Markdown } from '@asyncapi/react-component/lib/esm/components/Markdown';
+
+import type React from 'react';
+
+interface IData {
+ messages: any[];
+ channel: string
+ description: string
+}
+
+interface PublishNodeProps {
+ data: IData
+}
+
+export const PublishNode: React.FunctionComponent = ({
+ data: { messages = [], channel, description },
+}) => {
+ const { navigationSvc } = useServices();
+ const [highlight, setHighlight] = useState(false);
+
+ useEffect(() => {
+ return navigationSvc.highlightVisualiserNode(`#operation-publish-${channel}`, setHighlight);
+ }, [navigationSvc, setHighlight]);
+
+ return (
+
+
+
You can publish
+
+
{channel}
+ {description && (
+
+
+ {description}
+
+
+ )}
+
+
+
+
+ Messages
+
+
+ Payloads you can publish using this channel
+
+
+ {messages.map((message) => {
+ const theme = getBackgroundColor(message.title);
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+export default PublishNode;
diff --git a/apps/studio-next/src/components/Visualiser/Nodes/SubscribeNode.tsx b/apps/studio-next/src/components/Visualiser/Nodes/SubscribeNode.tsx
new file mode 100644
index 000000000..56cfbc459
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/Nodes/SubscribeNode.tsx
@@ -0,0 +1,94 @@
+import { useState, useEffect } from 'react';
+import { Handle, Position } from 'reactflow';
+
+import { useServices } from '@/services';
+import getBackgroundColor from '../utils/random-background-color';
+
+// @ts-ignore
+import { Markdown } from '@asyncapi/react-component/lib/esm/components/Markdown';
+
+import { FunctionComponent } from 'react';
+
+interface IData {
+ messages: any []
+ channel: string
+ description: string
+}
+
+interface PublishNodeProps {
+ data: IData
+}
+
+export const SubscribeNode: FunctionComponent = ({ data: { channel, description, messages } }) => {
+ const { navigationSvc } = useServices();
+ const [highlight, setHighlight] = useState(false);
+
+ useEffect(() => {
+ return navigationSvc.highlightVisualiserNode(`#operation-subscribe-${channel}`, setHighlight);
+ }, [navigationSvc, setHighlight]);
+
+ return (
+
+
+
+
+
+ You can subscribe
+
+
+
+
+
{channel}
+ {description && (
+
+
+ {description}
+
+
+ )}
+
+
+
+
+ Messages
+
+
+ Payloads to expect from listening to this channel
+
+
+ {messages.map((message) => {
+ return (
+
+
+
+
+ {message.title}
+
+
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+export default SubscribeNode;
diff --git a/apps/studio-next/src/components/Visualiser/Nodes/index.tsx b/apps/studio-next/src/components/Visualiser/Nodes/index.tsx
new file mode 100644
index 000000000..10475b57a
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/Nodes/index.tsx
@@ -0,0 +1,11 @@
+import PublishNode from './PublishNode';
+import ApplicationNode from './ApplicationNode';
+import SubscribeNode from './SubscribeNode';
+
+const nodeTypes = {
+ publishNode: PublishNode,
+ subscribeNode: SubscribeNode,
+ applicationNode: ApplicationNode
+};
+
+export default nodeTypes;
\ No newline at end of file
diff --git a/apps/studio-next/src/components/Visualiser/Visualiser.tsx b/apps/studio-next/src/components/Visualiser/Visualiser.tsx
new file mode 100644
index 000000000..085b74324
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/Visualiser.tsx
@@ -0,0 +1,52 @@
+import { useState, useEffect } from 'react';
+
+import { FlowDiagram } from './FlowDiagram';
+
+import { useDocumentsState, useSettingsState, useOtherState, otherState } from '@/state';
+
+import type { OldAsyncAPIDocument as AsyncAPIDocument } from '@asyncapi/parser';
+import { convertToOldAPI } from '@asyncapi/parser';
+
+import { FunctionComponent } from 'react';
+
+interface VisualiserProps {}
+
+export const Visualiser: FunctionComponent = () => {
+ const [parsedSpec, setParsedSpec] = useState(null);
+ const document = useDocumentsState(state => state.documents['asyncapi']?.document) || null;
+ const autoRendering = useSettingsState(state => state.templates.autoRendering);
+ const templateRerender = useOtherState(state => state.templateRerender);
+
+ useEffect(() => {
+ if (autoRendering || parsedSpec === null) {
+ const oldDocument = document !== null ? convertToOldAPI(document) : null;
+ setParsedSpec(oldDocument);
+ }
+ }, [document]); // eslint-disable-line
+
+ useEffect(() => {
+ if (templateRerender) {
+ const oldDocument = document !== null ? convertToOldAPI(document) : null;
+ setParsedSpec(oldDocument);
+ otherState.setState({ templateRerender: false });
+ }
+ }, [templateRerender]); // eslint-disable-line
+
+ if (!document) {
+ return (
+
+
Empty or invalid document. Please fix errors/define AsyncAPI document.
+
+ );
+ }
+
+ return (
+ parsedSpec && (
+
+ )
+ );
+};
diff --git a/apps/studio-next/src/components/Visualiser/VisualiserTemplate.tsx b/apps/studio-next/src/components/Visualiser/VisualiserTemplate.tsx
new file mode 100644
index 000000000..cc8f5e325
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/VisualiserTemplate.tsx
@@ -0,0 +1,15 @@
+import { Visualiser } from './Visualiser';
+import { TemplateSidebar } from '../Template/TemplateSidebar';
+
+import { FunctionComponent } from 'react';
+
+interface VisualiserTemplateProps {}
+
+export const VisualiserTemplate: FunctionComponent = () => {
+ return (
+
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/Visualiser/index.ts b/apps/studio-next/src/components/Visualiser/index.ts
new file mode 100644
index 000000000..02d60e00d
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/index.ts
@@ -0,0 +1,2 @@
+export * from './Visualiser';
+export * from './VisualiserTemplate';
diff --git a/apps/studio-next/src/components/Visualiser/utils/node-calculator.ts b/apps/studio-next/src/components/Visualiser/utils/node-calculator.ts
new file mode 100644
index 000000000..96a7f8166
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/utils/node-calculator.ts
@@ -0,0 +1,61 @@
+import { isNode, Node } from 'reactflow';
+
+const groupNodesByColumn = (elements: Node[]) => {
+ return elements.reduce((elementsGrouped: any, element: Node) => {
+ if (isNode(element)) {
+ if (elementsGrouped[element?.data.columnToRenderIn]) {
+ return {
+ ...elementsGrouped,
+ [element.data.columnToRenderIn]: elementsGrouped[element?.data.columnToRenderIn].concat([element])};
+ }
+
+ return {
+ ...elementsGrouped,
+ [element.data.columnToRenderIn]: (elementsGrouped[element?.data.groupId] = [element]),
+ };
+ }
+ return elementsGrouped;
+ }, {});
+};
+
+export const calculateNodesForDynamicLayout = (elements: Node[]) => {
+ const elementsGroupedByColumn = groupNodesByColumn(elements);
+
+ const newElements: { nodes: Node[], currentXPosition: number } = Object.keys(elementsGroupedByColumn).reduce(
+ (data: { nodes: Node[], currentXPosition: number }, group: string) => {
+ const groupNodes = elementsGroupedByColumn[String(group)];
+
+ // eslint-disable-next-line
+ const maxWidthOfColumn = Math.max.apply(
+ Math,
+ groupNodes.map((o: Node) => {
+ return o.width;
+ })
+ );
+
+ // For each group (column), render the nodes based on height they require (with some padding)
+ const { positionedNodes } = groupNodes.reduce(
+ (groupedNodes: { positionedNodes: Node[], currentYPosition: number }, currentNode: Node) => {
+ const verticalPadding = 40;
+
+ currentNode.position.x = data.currentXPosition;
+ currentNode.position.y = groupedNodes.currentYPosition;
+
+ return {
+ positionedNodes: groupedNodes.positionedNodes.concat([currentNode]),
+ currentYPosition: groupedNodes.currentYPosition + (currentNode.height || 0) + verticalPadding,
+ };
+ },
+ { positionedNodes: [], currentYPosition: 0 }
+ );
+
+ return {
+ nodes: [...data.nodes, ...positionedNodes],
+ currentXPosition: data.currentXPosition + maxWidthOfColumn + 100,
+ };
+ },
+ { nodes: [], currentXPosition: 0 }
+ );
+
+ return newElements.nodes;
+};
diff --git a/apps/studio-next/src/components/Visualiser/utils/node-factory.ts b/apps/studio-next/src/components/Visualiser/utils/node-factory.ts
new file mode 100644
index 000000000..4db581111
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/utils/node-factory.ts
@@ -0,0 +1,94 @@
+import type { OldAsyncAPIDocument as AsyncAPIDocument, OldChannel, OldOperation, OldMessage } from '@asyncapi/parser';
+import type { Node, Edge } from 'reactflow';
+
+interface FileredChannel {
+ channel: string;
+ channelModel: OldChannel;
+ operationModel: OldOperation;
+ messagesModel: OldMessage[];
+}
+
+function getChannelsByOperation(operation: string, spec: AsyncAPIDocument) {
+ const channels = spec.channels();
+ return Object.keys(channels).reduce((filteredChannels: FileredChannel[], channel) => {
+ const operationFn = operation === 'publish' ? 'hasPublish' : 'hasSubscribe';
+ // eslint-disable-next-line
+ if (channels[String(channel)][operationFn]()) {
+ const operationModel = (channels as any)[String(channel)][String(operation)]() as OldOperation;
+ filteredChannels.push({
+ channel,
+ channelModel: channels[String(channel)],
+ operationModel,
+ messagesModel: operationModel.messages(),
+ });
+ }
+ return filteredChannels;
+ }, []);
+}
+
+function buildFlowElementsForOperation({ operation, spec, applicationLinkType, data }: { operation: 'publish' | 'subscribe'; spec: AsyncAPIDocument; applicationLinkType: string, data: any }): Array<{ node: Node, edge: Edge }> {
+ return getChannelsByOperation(operation, spec).reduce((nodes: any, channel) => {
+ const { channelModel, operationModel, messagesModel } = channel;
+
+ const node: Node = {
+ id: `${operation}-${channel.channel}`,
+ type: `${operation}Node`,
+ data: {
+ title: operationModel.id(),
+ channel: channel.channel,
+ tags: operationModel.tags(),
+ messages: messagesModel.map((message) => ({
+ title: message.uid(),
+ description: message.description(),
+ })),
+
+ spec,
+ description: channelModel.description(),
+ operationId: operationModel.id(),
+ elementType: operation,
+ theme: operation === 'subscribe' ? 'green' : 'blue',
+ ...data
+ },
+ position: { x: 0, y: 0 },
+ };
+
+ const edge: Edge = {
+ id: `${operation}-${channel.channel}-to-application`,
+ // type: 'smoothstep',
+ // animated: true,
+ // label: messagesModel.map(message => message.uid()).join(','),
+ style: { stroke: applicationLinkType === 'target' ? '#00A5FA' : '#7ee3be', strokeWidth: 4 },
+ source: applicationLinkType === 'target' ? `${operation}-${channel.channel}` : 'application',
+ target: applicationLinkType === 'target' ? 'application' : `${operation}-${channel.channel}`,
+ };
+
+ return [...nodes, { node, edge }];
+ }, []);
+}
+
+export function getElementsFromAsyncAPISpec(spec: AsyncAPIDocument): Array<{ node: Node, edge: Edge }> {
+ const publishNodes = buildFlowElementsForOperation({
+ operation: 'publish',
+ spec,
+ applicationLinkType: 'target',
+ data: { columnToRenderIn: 'col-1' },
+ });
+ const subscribeNodes = buildFlowElementsForOperation({
+ operation: 'subscribe',
+ spec,
+ applicationLinkType: 'source',
+ data: { columnToRenderIn: 'col-3' },
+ });
+ const applicationNode = {
+ id: 'application',
+ type: 'applicationNode',
+ data: { spec, elementType: 'application', theme: 'indigo', columnToRenderIn: 'col-2' },
+ position: { x: 0, y: 0 },
+ };
+
+ return [
+ ...publishNodes,
+ { node: applicationNode } as { node: Node, edge: Edge },
+ ...subscribeNodes
+ ];
+}
diff --git a/apps/studio-next/src/components/Visualiser/utils/random-background-color.ts b/apps/studio-next/src/components/Visualiser/utils/random-background-color.ts
new file mode 100644
index 000000000..cc6a8435a
--- /dev/null
+++ b/apps/studio-next/src/components/Visualiser/utils/random-background-color.ts
@@ -0,0 +1,7 @@
+export default (stringInput: string) => {
+ //@ts-ignore
+ const stringUniqueHash = [...stringInput].reduce((acc, char) => {
+ return char.charCodeAt(0) + ((acc << 5) - acc);
+ }, 0);
+ return `hsla(${stringUniqueHash % 360}, 95%, 35%, 0.5)`;
+};
\ No newline at end of file
diff --git a/apps/studio-next/src/components/common/Dropdown.tsx b/apps/studio-next/src/components/common/Dropdown.tsx
new file mode 100644
index 000000000..826ab3569
--- /dev/null
+++ b/apps/studio-next/src/components/common/Dropdown.tsx
@@ -0,0 +1,55 @@
+import { useState, useRef } from 'react';
+
+import { useOutsideClickCallback } from '@/helpers';
+
+import type { FunctionComponent, PropsWithChildren, ReactNode } from 'react';
+
+interface DropdownProps extends PropsWithChildren {
+ opener: ReactNode;
+ className?: string;
+ buttonHoverClassName?: string;
+ align?: string;
+}
+
+export const Dropdown: FunctionComponent = ({
+ opener,
+ className = 'relative',
+ buttonHoverClassName,
+ align = 'right',
+ children,
+}) => {
+ const [open, setOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ useOutsideClickCallback(dropdownRef, () => setOpen(false));
+ buttonHoverClassName = buttonHoverClassName || 'hover:text-white';
+
+ return (
+
+
setOpen(!open)}
+ tabIndex={0}
+ onKeyDown={() => setOpen(!open)}
+ type="button"
+ className={`flex p-2 text-sm rounded-md ${buttonHoverClassName} focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo transition ease-in-out duration-150`}
+ >
+ {opener}
+
+
setOpen(false)}
+ tabIndex={0}
+ onKeyDown={() => setOpen(false)}
+ className={`${
+ open ? 'visible' : 'invisible'
+ } origin-top-right absolute ${align === 'right' &&
+ 'right-0'} ${align === 'left' &&
+ 'left-0'} mt-1 mr-3 w-64 rounded-md shadow-lg z-50`}
+ >
+
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/common/Markdown.tsx b/apps/studio-next/src/components/common/Markdown.tsx
new file mode 100644
index 000000000..fe6b16127
--- /dev/null
+++ b/apps/studio-next/src/components/common/Markdown.tsx
@@ -0,0 +1,16 @@
+// @ts-ignore
+import { Markdown as MarkdownComponent } from '@asyncapi/react-component/lib/esm/components/Markdown';
+
+import type { FunctionComponent, PropsWithChildren } from 'react';
+
+export const Markdown: FunctionComponent = ({
+ children,
+}) => {
+ return (
+
+
+ {children}
+
+
+ );
+};
diff --git a/apps/studio-next/src/components/common/Switch.tsx b/apps/studio-next/src/components/common/Switch.tsx
new file mode 100644
index 000000000..4483c4ba2
--- /dev/null
+++ b/apps/studio-next/src/components/common/Switch.tsx
@@ -0,0 +1,39 @@
+import React, { useState } from 'react';
+
+interface SwitchProps {
+ toggle?: boolean;
+ onChange: (toggle: boolean) => void;
+}
+
+export const Switch: React.FunctionComponent = ({
+ toggle: initToggle = false,
+ onChange,
+}) => {
+ const [toggle, setToggle] = useState(initToggle);
+
+ const onClickHandler = (e: any) => {
+ const newValue = !toggle;
+ setToggle(newValue);
+ onChange(newValue);
+ }
+
+ return (
+
+ );
+};
diff --git a/apps/studio-next/src/components/common/Tooltip.tsx b/apps/studio-next/src/components/common/Tooltip.tsx
new file mode 100644
index 000000000..c17f3e59c
--- /dev/null
+++ b/apps/studio-next/src/components/common/Tooltip.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import Tippy, { TippyProps } from '@tippyjs/react';
+
+export const Tooltip: React.FunctionComponent = ({
+ placement = 'bottom',
+ arrow = true,
+ animation = 'shift-away',
+ className = 'text-xs bg-gray-900 text-center',
+ hideOnClick = false,
+ children,
+ ...rest
+}) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/apps/studio-next/src/components/common/index.ts b/apps/studio-next/src/components/common/index.ts
new file mode 100644
index 000000000..ba9b3419b
--- /dev/null
+++ b/apps/studio-next/src/components/common/index.ts
@@ -0,0 +1,6 @@
+'use client'
+
+export * from './Dropdown';
+export * from './Markdown';
+export * from './Switch';
+export * from './Tooltip';
diff --git a/apps/studio-next/src/components/index.ts b/apps/studio-next/src/components/index.ts
new file mode 100644
index 000000000..8d3dad345
--- /dev/null
+++ b/apps/studio-next/src/components/index.ts
@@ -0,0 +1,5 @@
+export * from './Editor';
+export * from './Content';
+export * from './Navigation';
+export * from './Sidebar';
+export * from './Template';
diff --git a/apps/studio-next/src/examples/ibmmq.yml b/apps/studio-next/src/examples/ibmmq.yml
new file mode 100644
index 000000000..d9e8d09d2
--- /dev/null
+++ b/apps/studio-next/src/examples/ibmmq.yml
@@ -0,0 +1,57 @@
+asyncapi: 3.0.0
+info:
+ title: Record Label Service
+ version: 1.1.0
+ description: This service is in charge of processing music
+ license:
+ name: Apache License 2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0'
+servers:
+ production:
+ host: 'localhost:1414'
+ pathname: /QM1/DEV.APP.SVRCONN
+ protocol: ibmmq-secure
+ description: Production Instance 1
+ bindings:
+ ibmmq:
+ cipherSpec: ANY_TLS12
+channels:
+ songReleased:
+ address: song/released
+ messages:
+ Song:
+ $ref: '#/components/messages/Song'
+operations:
+ receiveSong:
+ action: receive
+ channel:
+ $ref: '#/channels/songReleased'
+ messages:
+ - $ref: '#/channels/songReleased/messages/Song'
+ sendSong:
+ action: send
+ channel:
+ $ref: '#/channels/songReleased'
+ messages:
+ - $ref: '#/channels/songReleased/messages/Song'
+components:
+ messages:
+ Song:
+ payload:
+ type: object
+ properties:
+ title:
+ type: string
+ description: Song title
+ artist:
+ type: string
+ description: Song artist
+ album:
+ type: string
+ description: Song album
+ genre:
+ type: string
+ description: Primary song genre
+ length:
+ type: integer
+ description: Track length in seconds
\ No newline at end of file
diff --git a/apps/studio-next/src/examples/index.tsx b/apps/studio-next/src/examples/index.tsx
new file mode 100644
index 000000000..b166ad96d
--- /dev/null
+++ b/apps/studio-next/src/examples/index.tsx
@@ -0,0 +1,85 @@
+// @ts-nocheck
+
+// protocol examples
+import kafka from './streetlights-kafka.yml';
+import websocket from './websocket-gemini.yml';
+import mqtt from './streetlights-mqtt.yml';
+import simple from './simple.yml';
+import ibmmq from './ibmmq.yml';
+
+// tutorial example
+import invalid from './tutorials/invalid.yml';
+
+// real world examples
+import slack from './real-world/slack-rtm.yml';
+import gitterStreaming from './real-world/gitter-streaming.yml';
+import kraken from './real-world/kraken-api-request-reply-filter.yml';
+
+const templateTypes = {
+ protocol: 'protocol-example',
+ realExample: 'real-example',
+ tutorial: 'tutorial-example'
+};
+
+export default [
+ {
+ title: 'Simple Example',
+ description: () => <>A basic example of a service that is in charge of processing user signups. Great place to start learning AsyncAPI.>,
+ template: simple,
+ type: templateTypes.protocol
+ },
+ {
+ title: 'Invalid Example',
+ description: () => <>An example of an invalid AsyncAPI document. This is only for educational purposes, to learn document validation.>,
+ template: invalid,
+ type: templateTypes.tutorial
+ },
+ {
+ title: 'Apache Kafka',
+ description: () => <>A framework implementation of a software bus using stream-processing. Open Source developed by the Apache Software Foundation.>,
+ template: kafka,
+ type: templateTypes.protocol
+ },
+ {
+ title: 'WebSocket',
+ description: () => <>A computer communications protocol, providing full-duplex communication channels over a single TCP connection.>,
+ template: websocket,
+ type: templateTypes.protocol
+ },
+ {
+ title: 'MQTT',
+ description: () => <>An OASIS standard messaging protocol for the Internet of Things. Ideal for connecting remote devices with limited processing power and bandwidth.>,
+ template: mqtt,
+ type: templateTypes.protocol
+ },
+ {
+ title: 'IBM MQ',
+ description: () => <>A robust, reliable, and secure messaging solution. IBM MQ simplifies and accelerates the integration of different applications across multiple platforms and supports a wide range of APIs and languages.>,
+ template: ibmmq,
+ type: templateTypes.protocol
+ },
+ {
+ title: 'HTTP',
+ description: () => <>A protocol for fetching resources. It is the foundation of any data exchange on the Web and it is a client-server protocol.>,
+ template: gitterStreaming,
+ type: templateTypes.protocol
+ },
+ {
+ title: 'Slack Real Time Messaging API',
+ description: () => <>Slack Real time messaging API. Using HTTP protocol.>,
+ template: slack,
+ type: templateTypes.realExample
+ },
+ {
+ title: 'Gitter Streaming API',
+ description: () => <>Gitter Streaming API from https://stream.gitter.im. Using HTTP protocol.>,
+ template: gitterStreaming,
+ type: templateTypes.realExample
+ },
+ {
+ title: 'Kraken Websockets API',
+ description: () => <>This Kraken Websocket specification. Using Websocket with request / reply>,
+ template: kraken,
+ type: templateTypes.realExample
+ }
+];
diff --git a/apps/studio-next/src/examples/real-world/gitter-streaming.yml b/apps/studio-next/src/examples/real-world/gitter-streaming.yml
new file mode 100644
index 000000000..ec0cbd12c
--- /dev/null
+++ b/apps/studio-next/src/examples/real-world/gitter-streaming.yml
@@ -0,0 +1,178 @@
+asyncapi: 3.0.0
+id: 'tag:stream.gitter.im,2022:api'
+info:
+ title: Gitter Streaming API
+ version: 1.0.0
+servers:
+ production:
+ host: stream.gitter.im
+ pathname: /v1
+ protocol: https
+ protocolVersion: '1.1'
+ security:
+ - $ref: '#/components/securitySchemes/httpBearerToken'
+channels:
+ rooms:
+ address: '/rooms/{roomId}/{resource}'
+ messages:
+ chatMessage:
+ $ref: '#/components/messages/chatMessage'
+ heartbeat:
+ $ref: '#/components/messages/heartbeat'
+ parameters:
+ roomId:
+ description: Id of the Gitter room.
+ examples:
+ - 53307860c3599d1de448e19d
+ resource:
+ enum:
+ - chatMessages
+ - events
+ description: The resource to consume.
+operations:
+ sendRoomInfo:
+ action: send
+ channel:
+ $ref: '#/channels/rooms'
+ bindings:
+ http:
+ method: POST
+ messages:
+ - $ref: '#/channels/rooms/messages/chatMessage'
+ - $ref: '#/channels/rooms/messages/heartbeat'
+components:
+ securitySchemes:
+ httpBearerToken:
+ type: http
+ scheme: bearer
+ messages:
+ chatMessage:
+ summary: >-
+ A message represents an individual chat message sent to a room. They are
+ a sub-resource of a room.
+ payload:
+ schemaFormat: application/schema+yaml;version=draft-07
+ schema:
+ type: object
+ properties:
+ id:
+ type: string
+ description: ID of the message.
+ text:
+ type: string
+ description: Original message in plain-text/markdown.
+ html:
+ type: string
+ description: HTML formatted message.
+ sent:
+ type: string
+ format: date-time
+ description: ISO formatted date of the message.
+ fromUser:
+ type: object
+ description: User that sent the message.
+ properties:
+ id:
+ type: string
+ description: Gitter User ID.
+ username:
+ type: string
+ description: Gitter/GitHub username.
+ displayName:
+ type: string
+ description: Gitter/GitHub user real name.
+ url:
+ type: string
+ description: Path to the user on Gitter.
+ avatarUrl:
+ type: string
+ format: uri
+ description: User avatar URI.
+ avatarUrlSmall:
+ type: string
+ format: uri
+ description: User avatar URI (small).
+ avatarUrlMedium:
+ type: string
+ format: uri
+ description: User avatar URI (medium).
+ v:
+ type: number
+ description: Version.
+ gv:
+ type: string
+ description: Stands for "Gravatar version" and is used for cache busting.
+ unread:
+ type: boolean
+ description: Boolean that indicates if the current user has read the message.
+ readBy:
+ type: number
+ description: Number of users that have read the message.
+ urls:
+ type: array
+ description: List of URLs present in the message.
+ items:
+ type: string
+ format: uri
+ mentions:
+ type: array
+ description: List of @Mentions in the message.
+ items:
+ type: object
+ properties:
+ screenName:
+ type: string
+ userId:
+ type: string
+ userIds:
+ type: array
+ items:
+ type: string
+ issues:
+ type: array
+ description: 'List of #Issues referenced in the message.'
+ items:
+ type: object
+ properties:
+ number:
+ type: string
+ meta:
+ type: array
+ description: Metadata. This is currently not used for anything.
+ items: {}
+ v:
+ type: number
+ description: Version.
+ gv:
+ type: string
+ description: Stands for "Gravatar version" and is used for cache busting.
+ bindings:
+ http:
+ headers:
+ type: object
+ properties:
+ Transfer-Encoding:
+ type: string
+ const: chunked
+ Trailer:
+ type: string
+ const: \r\n
+ heartbeat:
+ summary: Its purpose is to keep the connection alive.
+ payload:
+ schemaFormat: application/schema+yaml;version=draft-07
+ schema:
+ type: string
+ enum:
+ - "\r\n"
+ bindings:
+ http:
+ headers:
+ type: object
+ properties:
+ Transfer-Encoding:
+ type: string
+ const: chunked
+ Trailer:
+ type: string
+ const: \r\n
\ No newline at end of file
diff --git a/apps/studio-next/src/examples/real-world/kraken-api-request-reply-filter.yml b/apps/studio-next/src/examples/real-world/kraken-api-request-reply-filter.yml
new file mode 100644
index 000000000..9abb79398
--- /dev/null
+++ b/apps/studio-next/src/examples/real-world/kraken-api-request-reply-filter.yml
@@ -0,0 +1,388 @@
+asyncapi: 3.0.0
+
+info:
+ title: Kraken Websockets API
+ version: '1.8.0'
+ description: |
+ WebSockets API offers real-time market data updates. WebSockets is a bidirectional protocol offering fastest real-time data, helping you build real-time applications. The public message types presented below do not require authentication. Private-data messages can be subscribed on a separate authenticated endpoint.
+
+ ### General Considerations
+
+ - TLS with SNI (Server Name Indication) is required in order to establish a Kraken WebSockets API connection. See Cloudflare's [What is SNI?](https://www.cloudflare.com/learning/ssl/what-is-sni/) guide for more details.
+ - All messages sent and received via WebSockets are encoded in JSON format
+ - All decimal fields (including timestamps) are quoted to preserve precision.
+ - Timestamps should not be considered unique and not be considered as aliases for transaction IDs. Also, the granularity of timestamps is not representative of transaction rates.
+ - At least one private message should be subscribed to keep the authenticated client connection open.
+ - Please use REST API endpoint [AssetPairs](https://www.kraken.com/features/api#get-tradable-pairs) to fetch the list of pairs which can be subscribed via WebSockets API. For example, field 'wsname' gives the supported pairs name which can be used to subscribe.
+ - Cloudflare imposes a connection/re-connection rate limit (per IP address) of approximately 150 attempts per rolling 10 minutes. If this is exceeded, the IP is banned for 10 minutes.
+ - Recommended reconnection behaviour is to (1) attempt reconnection instantly up to a handful of times if the websocket is dropped randomly during normal operation but (2) after maintenance or extended downtime, attempt to reconnect no more quickly than once every 5 seconds. There is no advantage to reconnecting more rapidly after maintenance during cancel_only mode.
+
+
+channels:
+ currencyExchange:
+ address: /
+ messages:
+ ping:
+ $ref: '#/components/messages/ping'
+ pong:
+ $ref: '#/components/messages/pong'
+ heartbeat:
+ $ref: '#/components/messages/heartbeat'
+ systemStatus:
+ $ref: '#/components/messages/systemStatus'
+ subscriptionStatus:
+ $ref: '#/components/messages/subscriptionStatus'
+ subscribe:
+ $ref: '#/components/messages/subscribe'
+ unsubscribe:
+ $ref: '#/components/messages/unsubscribe'
+ dummyCurrencyInfo:
+ $ref: '#/components/messages/dummyCurrencyInfo'
+
+
+operations:
+ receivePing:
+ action: receive
+ channel:
+ $ref: '#/channels/currencyExchange'
+ reply:
+ channel:
+ $ref: '#/channels/currencyExchange'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/pong'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/ping'
+ sendHeartbeat:
+ action: send
+ channel:
+ $ref: '#/channels/currencyExchange'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/heartbeat'
+ systemStatus:
+ action: send
+ channel:
+ $ref: '#/channels/currencyExchange'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/systemStatus'
+ receiveSubscribeRequest:
+ action: receive
+ channel:
+ $ref: '#/channels/currencyExchange'
+ reply:
+ channel:
+ $ref: '#/channels/currencyExchange'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/subscriptionStatus'
+ - $ref: '#/channels/currencyExchange/messages/dummyCurrencyInfo'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/subscribe'
+ receiveUnsubscribeRequest:
+ action: receive
+ channel:
+ $ref: '#/channels/currencyExchange'
+ reply:
+ channel:
+ $ref: '#/channels/currencyExchange'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/subscriptionStatus'
+ messages:
+ - $ref: '#/channels/currencyExchange/messages/unsubscribe'
+
+
+components:
+ messages:
+ dummyCurrencyInfo:
+ summary: Dummy message with no real life details
+ description: It is here in this example to showcase that there is an additional message that normally is of a complex structure. It represents actually currency exchange value to show a reply to operation receiveSubscribeRequest with more than one possible message.
+ payload:
+ type: object
+ properties:
+ event:
+ type: string
+ const: currencyInfo
+ reqid:
+ $ref: '#/components/schemas/reqid'
+ data:
+ type: object
+ required:
+ - event
+ correlationId:
+ location: '$message.payload#/reqid'
+
+ ping:
+ summary: Ping server to determine whether connection is alive
+ description: Client can ping server to determine whether connection is alive, server responds with pong. This is an application level ping as opposed to default ping in websockets standard which is server initiated
+ payload:
+ $ref: '#/components/schemas/ping'
+ correlationId:
+ location: '$message.payload#/reqid'
+
+ pong:
+ summary: Pong is a response to ping message
+ description: Server pong response to a ping to determine whether connection is alive. This is an application level pong as opposed to default pong in websockets standard which is sent by client in response to a ping
+ payload:
+ $ref: '#/components/schemas/pong'
+ correlationId:
+ location: '$message.payload#/reqid'
+
+ subscribe:
+ description: Subscribe to a topic on a single or multiple currency pairs.
+ payload:
+ $ref: '#/components/schemas/subscribe'
+ correlationId:
+ location: '$message.payload#/reqid'
+ unsubscribe:
+ description: Unsubscribe, can specify a channelID or multiple currency pairs.
+ payload:
+ $ref: '#/components/schemas/unsubscribe'
+ correlationId:
+ location: '$message.payload#/reqid'
+ subscriptionStatus:
+ description: Subscription status response to subscribe, unsubscribe or exchange initiated unsubscribe.
+ payload:
+ $ref: '#/components/schemas/subscriptionStatus'
+ examples:
+ - payload:
+ channelID: 10001
+ channelName: ohlc-5
+ event: subscriptionStatus
+ pair: XBT/EUR
+ reqid: 42
+ status: unsubscribed
+ subscription:
+ interval: 5
+ name: ohlc
+ - payload:
+ errorMessage: Subscription depth not supported
+ event: subscriptionStatus
+ pair: XBT/USD
+ status: error
+ subscription:
+ depth: 42
+ name: book
+
+ systemStatus:
+ description: Status sent on connection or system status changes.
+ payload:
+ $ref: '#/components/schemas/systemStatus'
+
+ heartbeat:
+ description: Server heartbeat sent if no subscription traffic within 1 second (approximately)
+ payload:
+ $ref: '#/components/schemas/heartbeat'
+
+
+ schemas:
+ ping:
+ type: object
+ properties:
+ event:
+ type: string
+ const: ping
+ reqid:
+ $ref: '#/components/schemas/reqid'
+ required:
+ - event
+ heartbeat:
+ type: object
+ properties:
+ event:
+ type: string
+ const: heartbeat
+ pong:
+ type: object
+ properties:
+ event:
+ type: string
+ const: pong
+ reqid:
+ $ref: '#/components/schemas/reqid'
+ systemStatus:
+ type: object
+ properties:
+ event:
+ type: string
+ const: systemStatus
+ connectionID:
+ type: integer
+ description: The ID of the connection
+ status:
+ $ref: '#/components/schemas/status'
+ version:
+ type: string
+ status:
+ type: string
+ enum:
+ - online
+ - maintenance
+ - cancel_only
+ - limit_only
+ - post_only
+ subscribe:
+ type: object
+ properties:
+ event:
+ type: string
+ const: subscribe
+ reqid:
+ $ref: '#/components/schemas/reqid'
+ pair:
+ $ref: '#/components/schemas/pair'
+ subscription:
+ type: object
+ properties:
+ depth:
+ $ref: '#/components/schemas/depth'
+ interval:
+ $ref: '#/components/schemas/interval'
+ name:
+ $ref: '#/components/schemas/name'
+ ratecounter:
+ $ref: '#/components/schemas/ratecounter'
+ snapshot:
+ $ref: '#/components/schemas/snapshot'
+ token:
+ $ref: '#/components/schemas/token'
+ required:
+ - name
+ required:
+ - event
+ unsubscribe:
+ type: object
+ properties:
+ event:
+ type: string
+ const: unsubscribe
+ reqid:
+ $ref: '#/components/schemas/reqid'
+ pair:
+ $ref: '#/components/schemas/pair'
+ subscription:
+ type: object
+ properties:
+ depth:
+ $ref: '#/components/schemas/depth'
+ interval:
+ $ref: '#/components/schemas/interval'
+ name:
+ $ref: '#/components/schemas/name'
+ token:
+ $ref: '#/components/schemas/token'
+ required:
+ - name
+ required:
+ - event
+ subscriptionStatus:
+ type: object
+ oneOf:
+ - $ref: '#/components/schemas/subscriptionStatusError'
+ - $ref: '#/components/schemas/subscriptionStatusSuccess'
+ subscriptionStatusError:
+ allOf:
+ - properties:
+ errorMessage:
+ type: string
+ required:
+ - errorMessage
+ - $ref: '#/components/schemas/subscriptionStatusCommon'
+ subscriptionStatusSuccess:
+ allOf:
+ - properties:
+ channelID:
+ type: integer
+ description: ChannelID on successful subscription, applicable to public messages only.
+ channelName:
+ type: string
+ description: Channel Name on successful subscription. For payloads 'ohlc' and 'book', respective interval or depth will be added as suffix.
+ required:
+ - channelID
+ - channelName
+ - $ref: '#/components/schemas/subscriptionStatusCommon'
+ subscriptionStatusCommon:
+ type: object
+ required:
+ - event
+ properties:
+ event:
+ type: string
+ const: subscriptionStatus
+ reqid:
+ $ref: '#/components/schemas/reqid'
+ pair:
+ $ref: '#/components/schemas/pair'
+ status:
+ $ref: '#/components/schemas/status'
+ subscription:
+ required:
+ - name
+ type: object
+ properties:
+ depth:
+ $ref: '#/components/schemas/depth'
+ interval:
+ $ref: '#/components/schemas/interval'
+ maxratecount:
+ $ref: '#/components/schemas/maxratecount'
+ name:
+ $ref: '#/components/schemas/name'
+ token:
+ $ref: '#/components/schemas/token'
+ interval:
+ type: integer
+ description: Time interval associated with ohlc subscription in minutes.
+ default: 1
+ enum:
+ - 1
+ - 5
+ - 15
+ - 30
+ - 60
+ - 240
+ - 1440
+ - 10080
+ - 21600
+ name:
+ type: string
+ description: The name of the channel you subscribe too.
+ enum:
+ - book
+ - ohlc
+ - openOrders
+ - ownTrades
+ - spread
+ - ticker
+ - trade
+ token:
+ type: string
+ description: base64-encoded authentication token for private-data endpoints.
+ depth:
+ type: integer
+ default: 10
+ enum:
+ - 10
+ - 25
+ - 100
+ - 500
+ - 1000
+ description: Depth associated with book subscription in number of levels each side.
+ maxratecount:
+ type: integer
+ description: Max rate-limit budget. Compare to the ratecounter field in the openOrders updates to check whether you are approaching the rate limit.
+ ratecounter:
+ type: boolean
+ default: false
+ description: Whether to send rate-limit counter in updates (supported only for openOrders subscriptions)
+ snapshot:
+ type: boolean
+ default: true
+ description: Whether to send historical feed data snapshot upon subscription (supported only for ownTrades subscriptions)
+ reqid:
+ type: integer
+ description: client originated ID reflected in response message.
+ pair:
+ type: array
+ description: Array of currency pairs.
+ items:
+ type: string
+ description: Format of each pair is "A/B", where A and B are ISO 4217-A3 for standardized assets and popular unique symbol if not standardized.
+ pattern: '[A-Z\s]+\/[A-Z\s]+'
\ No newline at end of file
diff --git a/apps/studio-next/src/examples/real-world/slack-rtm.yml b/apps/studio-next/src/examples/real-world/slack-rtm.yml
new file mode 100644
index 000000000..1973e3b99
--- /dev/null
+++ b/apps/studio-next/src/examples/real-world/slack-rtm.yml
@@ -0,0 +1,982 @@
+asyncapi: 3.0.0
+id: 'wss://wss-primary.slack.com/websocket'
+info:
+ title: Slack Real Time Messaging API
+ version: 1.0.0
+servers:
+ production:
+ host: slack.com
+ pathname: /api/rtm.connect
+ protocol: https
+ protocolVersion: '1.1'
+ security:
+ - $ref: '#/components/securitySchemes/token'
+channels:
+ root:
+ address: /
+ messages:
+ outgoingMessage:
+ $ref: '#/components/messages/outgoingMessage'
+ hello:
+ $ref: '#/components/messages/hello'
+ connectionError:
+ $ref: '#/components/messages/connectionError'
+ accountsChanged:
+ $ref: '#/components/messages/accountsChanged'
+ botAdded:
+ $ref: '#/components/messages/botAdded'
+ botChanged:
+ $ref: '#/components/messages/botChanged'
+ channelArchive:
+ $ref: '#/components/messages/channelArchive'
+ channelCreated:
+ $ref: '#/components/messages/channelCreated'
+ channelDeleted:
+ $ref: '#/components/messages/channelDeleted'
+ channelHistoryChanged:
+ $ref: '#/components/messages/channelHistoryChanged'
+ channelJoined:
+ $ref: '#/components/messages/channelJoined'
+ channelLeft:
+ $ref: '#/components/messages/channelLeft'
+ channelMarked:
+ $ref: '#/components/messages/channelMarked'
+ channelRename:
+ $ref: '#/components/messages/channelRename'
+ channelUnarchive:
+ $ref: '#/components/messages/channelUnarchive'
+ commandsChanged:
+ $ref: '#/components/messages/commandsChanged'
+ dndUpdated:
+ $ref: '#/components/messages/dndUpdated'
+ dndUpdatedUser:
+ $ref: '#/components/messages/dndUpdatedUser'
+ emailDomainChanged:
+ $ref: '#/components/messages/emailDomainChanged'
+ emojiRemoved:
+ $ref: '#/components/messages/emojiRemoved'
+ emojiAdded:
+ $ref: '#/components/messages/emojiAdded'
+ fileChange:
+ $ref: '#/components/messages/fileChange'
+ fileCommentAdded:
+ $ref: '#/components/messages/fileCommentAdded'
+ fileCommentDeleted:
+ $ref: '#/components/messages/fileCommentDeleted'
+ fileCommentEdited:
+ $ref: '#/components/messages/fileCommentEdited'
+ fileCreated:
+ $ref: '#/components/messages/fileCreated'
+ fileDeleted:
+ $ref: '#/components/messages/fileDeleted'
+ filePublic:
+ $ref: '#/components/messages/filePublic'
+ fileShared:
+ $ref: '#/components/messages/fileShared'
+ fileUnshared:
+ $ref: '#/components/messages/fileUnshared'
+ goodbye:
+ $ref: '#/components/messages/goodbye'
+ groupArchive:
+ $ref: '#/components/messages/groupArchive'
+ groupClose:
+ $ref: '#/components/messages/groupClose'
+ groupHistoryChanged:
+ $ref: '#/components/messages/groupHistoryChanged'
+ groupJoined:
+ $ref: '#/components/messages/groupJoined'
+ groupLeft:
+ $ref: '#/components/messages/groupLeft'
+ groupMarked:
+ $ref: '#/components/messages/groupMarked'
+ groupOpen:
+ $ref: '#/components/messages/groupOpen'
+ groupRename:
+ $ref: '#/components/messages/groupRename'
+ groupUnarchive:
+ $ref: '#/components/messages/groupUnarchive'
+ imClose:
+ $ref: '#/components/messages/imClose'
+ imCreated:
+ $ref: '#/components/messages/imCreated'
+ imMarked:
+ $ref: '#/components/messages/imMarked'
+ imOpen:
+ $ref: '#/components/messages/imOpen'
+ manualPresenceChange:
+ $ref: '#/components/messages/manualPresenceChange'
+ memberJoinedChannel:
+ $ref: '#/components/messages/memberJoinedChannel'
+ message:
+ $ref: '#/components/messages/message'
+operations:
+ receiveOutgoingMessage:
+ action: receive
+ channel:
+ $ref: '#/channels/root'
+ messages:
+ - $ref: '#/channels/root/messages/outgoingMessage'
+ sendMessages:
+ action: send
+ channel:
+ $ref: '#/channels/root'
+ messages:
+ - $ref: '#/channels/root/messages/hello'
+ - $ref: '#/channels/root/messages/connectionError'
+ - $ref: '#/channels/root/messages/accountsChanged'
+ - $ref: '#/channels/root/messages/botAdded'
+ - $ref: '#/channels/root/messages/botChanged'
+ - $ref: '#/channels/root/messages/channelArchive'
+ - $ref: '#/channels/root/messages/channelCreated'
+ - $ref: '#/channels/root/messages/channelDeleted'
+ - $ref: '#/channels/root/messages/channelHistoryChanged'
+ - $ref: '#/channels/root/messages/channelJoined'
+ - $ref: '#/channels/root/messages/channelLeft'
+ - $ref: '#/channels/root/messages/channelMarked'
+ - $ref: '#/channels/root/messages/channelRename'
+ - $ref: '#/channels/root/messages/channelUnarchive'
+ - $ref: '#/channels/root/messages/commandsChanged'
+ - $ref: '#/channels/root/messages/dndUpdated'
+ - $ref: '#/channels/root/messages/dndUpdatedUser'
+ - $ref: '#/channels/root/messages/emailDomainChanged'
+ - $ref: '#/channels/root/messages/emojiRemoved'
+ - $ref: '#/channels/root/messages/emojiAdded'
+ - $ref: '#/channels/root/messages/fileChange'
+ - $ref: '#/channels/root/messages/fileCommentAdded'
+ - $ref: '#/channels/root/messages/fileCommentDeleted'
+ - $ref: '#/channels/root/messages/fileCommentEdited'
+ - $ref: '#/channels/root/messages/fileCreated'
+ - $ref: '#/channels/root/messages/fileDeleted'
+ - $ref: '#/channels/root/messages/filePublic'
+ - $ref: '#/channels/root/messages/fileShared'
+ - $ref: '#/channels/root/messages/fileUnshared'
+ - $ref: '#/channels/root/messages/goodbye'
+ - $ref: '#/channels/root/messages/groupArchive'
+ - $ref: '#/channels/root/messages/groupClose'
+ - $ref: '#/channels/root/messages/groupHistoryChanged'
+ - $ref: '#/channels/root/messages/groupJoined'
+ - $ref: '#/channels/root/messages/groupLeft'
+ - $ref: '#/channels/root/messages/groupMarked'
+ - $ref: '#/channels/root/messages/groupOpen'
+ - $ref: '#/channels/root/messages/groupRename'
+ - $ref: '#/channels/root/messages/groupUnarchive'
+ - $ref: '#/channels/root/messages/imClose'
+ - $ref: '#/channels/root/messages/imCreated'
+ - $ref: '#/channels/root/messages/imMarked'
+ - $ref: '#/channels/root/messages/imOpen'
+ - $ref: '#/channels/root/messages/manualPresenceChange'
+ - $ref: '#/channels/root/messages/memberJoinedChannel'
+ - $ref: '#/channels/root/messages/message'
+components:
+ securitySchemes:
+ token:
+ type: httpApiKey
+ name: token
+ in: query
+ schemas:
+ attachment:
+ type: object
+ properties:
+ fallback:
+ type: string
+ color:
+ type: string
+ pretext:
+ type: string
+ author_name:
+ type: string
+ author_link:
+ type: string
+ format: uri
+ author_icon:
+ type: string
+ format: uri
+ title:
+ type: string
+ title_link:
+ type: string
+ format: uri
+ text:
+ type: string
+ fields:
+ type: array
+ items:
+ type: object
+ properties:
+ title:
+ type: string
+ value:
+ type: string
+ short:
+ type: boolean
+ image_url:
+ type: string
+ format: uri
+ thumb_url:
+ type: string
+ format: uri
+ footer:
+ type: string
+ footer_icon:
+ type: string
+ format: uri
+ ts:
+ type: number
+ messages:
+ hello:
+ summary: First event received upon connection.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - hello
+ connectionError:
+ summary: Event received when a connection error happens.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - error
+ error:
+ type: object
+ properties:
+ code:
+ type: number
+ msg:
+ type: string
+ accountsChanged:
+ summary: The list of accounts a user is signed into has changed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - accounts_changed
+ botAdded:
+ summary: A bot user was added.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - bot_added
+ bot:
+ type: object
+ properties:
+ id:
+ type: string
+ app_id:
+ type: string
+ name:
+ type: string
+ icons:
+ type: object
+ additionalProperties:
+ type: string
+ botChanged:
+ summary: A bot user was changed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - bot_added
+ bot:
+ type: object
+ properties:
+ id:
+ type: string
+ app_id:
+ type: string
+ name:
+ type: string
+ icons:
+ type: object
+ additionalProperties:
+ type: string
+ channelArchive:
+ summary: A channel was archived.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_archive
+ channel:
+ type: string
+ user:
+ type: string
+ channelCreated:
+ summary: A channel was created.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_created
+ channel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ created:
+ type: number
+ creator:
+ type: string
+ channelDeleted:
+ summary: A channel was deleted.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_deleted
+ channel:
+ type: string
+ channelHistoryChanged:
+ summary: Bulk updates were made to a channel's history.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_history_changed
+ latest:
+ type: string
+ ts:
+ type: string
+ event_ts:
+ type: string
+ channelJoined:
+ summary: You joined a channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_joined
+ channel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ created:
+ type: number
+ creator:
+ type: string
+ channelLeft:
+ summary: You left a channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_left
+ channel:
+ type: string
+ channelMarked:
+ summary: Your channel read marker was updated.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_marked
+ channel:
+ type: string
+ ts:
+ type: string
+ channelRename:
+ summary: A channel was renamed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_rename
+ channel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ created:
+ type: number
+ channelUnarchive:
+ summary: A channel was unarchived.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - channel_unarchive
+ channel:
+ type: string
+ user:
+ type: string
+ commandsChanged:
+ summary: A slash command has been added or changed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - commands_changed
+ event_ts:
+ type: string
+ dndUpdated:
+ summary: Do not Disturb settings changed for the current user.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - dnd_updated
+ user:
+ type: string
+ dnd_status:
+ type: object
+ properties:
+ dnd_enabled:
+ type: boolean
+ next_dnd_start_ts:
+ type: number
+ next_dnd_end_ts:
+ type: number
+ snooze_enabled:
+ type: boolean
+ snooze_endtime:
+ type: number
+ dndUpdatedUser:
+ summary: Do not Disturb settings changed for a member.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - dnd_updated_user
+ user:
+ type: string
+ dnd_status:
+ type: object
+ properties:
+ dnd_enabled:
+ type: boolean
+ next_dnd_start_ts:
+ type: number
+ next_dnd_end_ts:
+ type: number
+ emailDomainChanged:
+ summary: The workspace email domain has changed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - email_domain_changed
+ email_domain:
+ type: string
+ event_ts:
+ type: string
+ emojiRemoved:
+ summary: A custom emoji has been removed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - emoji_changed
+ subtype:
+ type: string
+ enum:
+ - remove
+ names:
+ type: array
+ items:
+ type: string
+ event_ts:
+ type: string
+ emojiAdded:
+ summary: A custom emoji has been added.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - emoji_changed
+ subtype:
+ type: string
+ enum:
+ - add
+ name:
+ type: string
+ value:
+ type: string
+ format: uri
+ event_ts:
+ type: string
+ fileChange:
+ summary: A file was changed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_change
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileCommentAdded:
+ summary: A file comment was added.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_comment_added
+ comment: {}
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileCommentDeleted:
+ summary: A file comment was deleted.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_comment_deleted
+ comment:
+ type: string
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileCommentEdited:
+ summary: A file comment was edited.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_comment_edited
+ comment: {}
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileCreated:
+ summary: A file was created.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_created
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileDeleted:
+ summary: A file was deleted.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_deleted
+ file_id:
+ type: string
+ event_ts:
+ type: string
+ filePublic:
+ summary: A file was made public.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_public
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileShared:
+ summary: A file was shared.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_shared
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ fileUnshared:
+ summary: A file was unshared.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - file_unshared
+ file_id:
+ type: string
+ file:
+ type: object
+ properties:
+ id:
+ type: string
+ goodbye:
+ summary: The server intends to close the connection soon.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - goodbye
+ groupArchive:
+ summary: A private channel was archived.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_archive
+ channel:
+ type: string
+ groupClose:
+ summary: You closed a private channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_close
+ user:
+ type: string
+ channel:
+ type: string
+ groupHistoryChanged:
+ summary: Bulk updates were made to a private channel's history.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_history_changed
+ latest:
+ type: string
+ ts:
+ type: string
+ event_ts:
+ type: string
+ groupJoined:
+ summary: You joined a private channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_joined
+ channel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ created:
+ type: number
+ creator:
+ type: string
+ groupLeft:
+ summary: You left a private channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_left
+ channel:
+ type: string
+ groupMarked:
+ summary: A private channel read marker was updated.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_marked
+ channel:
+ type: string
+ ts:
+ type: string
+ groupOpen:
+ summary: You opened a private channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_open
+ user:
+ type: string
+ channel:
+ type: string
+ groupRename:
+ summary: A private channel was renamed.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_rename
+ channel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ created:
+ type: number
+ groupUnarchive:
+ summary: A private channel was unarchived.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - group_unarchive
+ channel:
+ type: string
+ user:
+ type: string
+ imClose:
+ summary: You closed a DM.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - im_close
+ channel:
+ type: string
+ user:
+ type: string
+ imCreated:
+ summary: A DM was created.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - im_created
+ channel:
+ type: object
+ properties:
+ id:
+ type: string
+ name:
+ type: string
+ created:
+ type: number
+ creator:
+ type: string
+ user:
+ type: string
+ imMarked:
+ summary: A direct message read marker was updated.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - im_marked
+ channel:
+ type: string
+ ts:
+ type: string
+ imOpen:
+ summary: You opened a DM.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - im_open
+ channel:
+ type: string
+ user:
+ type: string
+ manualPresenceChange:
+ summary: You manually updated your presence.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - manual_presence_change
+ presence:
+ type: string
+ memberJoinedChannel:
+ summary: A user joined a public or private channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - member_joined_channel
+ user:
+ type: string
+ channel:
+ type: string
+ channel_type:
+ type: string
+ enum:
+ - C
+ - G
+ team:
+ type: string
+ inviter:
+ type: string
+ memberLeftChannel:
+ summary: A user left a public or private channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - member_left_channel
+ user:
+ type: string
+ channel:
+ type: string
+ channel_type:
+ type: string
+ enum:
+ - C
+ - G
+ team:
+ type: string
+ message:
+ summary: A message was sent to a channel.
+ payload:
+ type: object
+ properties:
+ type:
+ type: string
+ enum:
+ - message
+ user:
+ type: string
+ channel:
+ type: string
+ text:
+ type: string
+ ts:
+ type: string
+ attachments:
+ type: array
+ items:
+ $ref: '#/components/schemas/attachment'
+ edited:
+ type: object
+ properties:
+ user:
+ type: string
+ ts:
+ type: string
+ outgoingMessage:
+ summary: A message was sent to a channel.
+ payload:
+ type: object
+ properties:
+ id:
+ type: number
+ type:
+ type: string
+ enum:
+ - message
+ channel:
+ type: string
+ text:
+ type: string
\ No newline at end of file
diff --git a/apps/studio-next/src/examples/simple.yml b/apps/studio-next/src/examples/simple.yml
new file mode 100644
index 000000000..75572baf4
--- /dev/null
+++ b/apps/studio-next/src/examples/simple.yml
@@ -0,0 +1,31 @@
+asyncapi: 3.0.0
+info:
+ title: Account Service
+ version: 1.0.0
+ description: This service is in charge of processing user signups
+channels:
+ userSignedup:
+ address: user/signedup
+ messages:
+ UserSignedUp:
+ $ref: '#/components/messages/UserSignedUp'
+operations:
+ sendUserSignedup:
+ action: send
+ channel:
+ $ref: '#/channels/userSignedup'
+ messages:
+ - $ref: '#/channels/userSignedup/messages/UserSignedUp'
+components:
+ messages:
+ UserSignedUp:
+ payload:
+ type: object
+ properties:
+ displayName:
+ type: string
+ description: Name of the user
+ email:
+ type: string
+ format: email
+ description: Email of the user
\ No newline at end of file
diff --git a/apps/studio-next/src/examples/streetlights-kafka.yml b/apps/studio-next/src/examples/streetlights-kafka.yml
new file mode 100644
index 000000000..0cbdc195d
--- /dev/null
+++ b/apps/studio-next/src/examples/streetlights-kafka.yml
@@ -0,0 +1,206 @@
+asyncapi: 3.0.0
+info:
+ title: Streetlights Kafka API
+ version: 1.0.0
+ description: |-
+ The Smartylighting Streetlights API allows you to remotely manage the city
+ lights.
+ ### Check out its awesome features:
+
+ * Turn a specific streetlight on/off 🌃
+ * Dim a specific streetlight 😎
+ * Receive real-time information about environmental lighting conditions 📈
+ license:
+ name: Apache 2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0'
+defaultContentType: application/json
+servers:
+ scram-connections:
+ host: 'test.mykafkacluster.org:18092'
+ protocol: kafka-secure
+ description: Test broker secured with scramSha256
+ security:
+ - $ref: '#/components/securitySchemes/saslScram'
+ tags:
+ - name: 'env:test-scram'
+ description: >-
+ This environment is meant for running internal tests through
+ scramSha256
+ - name: 'kind:remote'
+ description: This server is a remote server. Not exposed by the application
+ - name: 'visibility:private'
+ description: This resource is private and only available to certain users
+ mtls-connections:
+ host: 'test.mykafkacluster.org:28092'
+ protocol: kafka-secure
+ description: Test broker secured with X509
+ security:
+ - $ref: '#/components/securitySchemes/certs'
+ tags:
+ - name: 'env:test-mtls'
+ description: This environment is meant for running internal tests through mtls
+ - name: 'kind:remote'
+ description: This server is a remote server. Not exposed by the application
+ - name: 'visibility:private'
+ description: This resource is private and only available to certain users
+channels:
+ lightingMeasured:
+ address: 'smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured'
+ messages:
+ lightMeasured:
+ $ref: '#/components/messages/lightMeasured'
+ description: The topic on which measured values may be produced and consumed.
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ lightTurnOn:
+ address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.on'
+ messages:
+ turnOn:
+ $ref: '#/components/messages/turnOnOff'
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ lightTurnOff:
+ address: 'smartylighting.streetlights.1.0.action.{streetlightId}.turn.off'
+ messages:
+ turnOff:
+ $ref: '#/components/messages/turnOnOff'
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ lightsDim:
+ address: 'smartylighting.streetlights.1.0.action.{streetlightId}.dim'
+ messages:
+ dimLight:
+ $ref: '#/components/messages/dimLight'
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+operations:
+ receiveLightMeasurement:
+ action: receive
+ channel:
+ $ref: '#/channels/lightingMeasured'
+ summary: >-
+ Inform about environmental lighting conditions of a particular
+ streetlight.
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ messages:
+ - $ref: '#/channels/lightingMeasured/messages/lightMeasured'
+ turnOn:
+ action: send
+ channel:
+ $ref: '#/channels/lightTurnOn'
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ messages:
+ - $ref: '#/channels/lightTurnOn/messages/turnOn'
+ turnOff:
+ action: send
+ channel:
+ $ref: '#/channels/lightTurnOff'
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ messages:
+ - $ref: '#/channels/lightTurnOff/messages/turnOff'
+ dimLight:
+ action: send
+ channel:
+ $ref: '#/channels/lightsDim'
+ traits:
+ - $ref: '#/components/operationTraits/kafka'
+ messages:
+ - $ref: '#/channels/lightsDim/messages/dimLight'
+components:
+ messages:
+ lightMeasured:
+ name: lightMeasured
+ title: Light measured
+ summary: >-
+ Inform about environmental lighting conditions of a particular
+ streetlight.
+ contentType: application/json
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: '#/components/schemas/lightMeasuredPayload'
+ turnOnOff:
+ name: turnOnOff
+ title: Turn on/off
+ summary: Command a particular streetlight to turn the lights on or off.
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: '#/components/schemas/turnOnOffPayload'
+ dimLight:
+ name: dimLight
+ title: Dim light
+ summary: Command a particular streetlight to dim the lights.
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: '#/components/schemas/dimLightPayload'
+ schemas:
+ lightMeasuredPayload:
+ type: object
+ properties:
+ lumens:
+ type: integer
+ minimum: 0
+ description: Light intensity measured in lumens.
+ sentAt:
+ $ref: '#/components/schemas/sentAt'
+ turnOnOffPayload:
+ type: object
+ properties:
+ command:
+ type: string
+ enum:
+ - 'on'
+ - 'off'
+ description: Whether to turn on or off the light.
+ sentAt:
+ $ref: '#/components/schemas/sentAt'
+ dimLightPayload:
+ type: object
+ properties:
+ percentage:
+ type: integer
+ description: Percentage to which the light should be dimmed to.
+ minimum: 0
+ maximum: 100
+ sentAt:
+ $ref: '#/components/schemas/sentAt'
+ sentAt:
+ type: string
+ format: date-time
+ description: Date and time when the message was sent.
+ securitySchemes:
+ saslScram:
+ type: scramSha256
+ description: Provide your username and password for SASL/SCRAM authentication
+ certs:
+ type: X509
+ description: Download the certificate files from service provider
+ parameters:
+ streetlightId:
+ description: The ID of the streetlight.
+ messageTraits:
+ commonHeaders:
+ headers:
+ type: object
+ properties:
+ my-app-header:
+ type: integer
+ minimum: 0
+ maximum: 100
+ operationTraits:
+ kafka:
+ bindings:
+ kafka:
+ clientId:
+ type: string
+ enum:
+ - my-app-id
diff --git a/apps/studio-next/src/examples/streetlights-mqtt.yml b/apps/studio-next/src/examples/streetlights-mqtt.yml
new file mode 100644
index 000000000..d608968f8
--- /dev/null
+++ b/apps/studio-next/src/examples/streetlights-mqtt.yml
@@ -0,0 +1,260 @@
+asyncapi: 3.0.0
+info:
+ title: Streetlights MQTT API
+ version: 1.0.0
+ description: |-
+ The Smartylighting Streetlights API allows you to remotely manage the city
+ lights.
+ ### Check out its awesome features:
+
+ * Turn a specific streetlight on/off 🌃
+ * Dim a specific streetlight 😎
+ * Receive real-time information about environmental lighting conditions 📈
+ license:
+ name: Apache 2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0'
+defaultContentType: application/json
+servers:
+ production:
+ host: 'test.mosquitto.org:{port}'
+ protocol: mqtt
+ description: Test broker
+ variables:
+ port:
+ description: Secure connection (TLS) is available through port 8883.
+ default: '1883'
+ enum:
+ - '1883'
+ - '8883'
+ security:
+ - $ref: '#/components/securitySchemes/apiKey'
+ - type: oauth2
+ description: Flows to support OAuth 2.0
+ flows:
+ implicit:
+ authorizationUrl: 'https://authserver.example/auth'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ password:
+ tokenUrl: 'https://authserver.example/token'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ clientCredentials:
+ tokenUrl: 'https://authserver.example/token'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ authorizationCode:
+ authorizationUrl: 'https://authserver.example/auth'
+ tokenUrl: 'https://authserver.example/token'
+ refreshUrl: 'https://authserver.example/refresh'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ scopes:
+ - 'streetlights:on'
+ - 'streetlights:off'
+ - 'streetlights:dim'
+ - $ref: '#/components/securitySchemes/openIdConnectWellKnown'
+ tags:
+ - name: 'env:production'
+ description: This environment is meant for production use case
+ - name: 'kind:remote'
+ description: This server is a remote server. Not exposed by the application
+ - name: 'visibility:public'
+ description: This resource is public and available to everyone
+channels:
+ lightingMeasured:
+ address: 'smartylighting/streetlights/1/0/event/{streetlightId}/lighting/measured'
+ messages:
+ lightMeasured:
+ $ref: '#/components/messages/lightMeasured'
+ description: The topic on which measured values may be produced and consumed.
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ lightTurnOn:
+ address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/on'
+ messages:
+ turnOn:
+ $ref: '#/components/messages/turnOnOff'
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ lightTurnOff:
+ address: 'smartylighting/streetlights/1/0/action/{streetlightId}/turn/off'
+ messages:
+ turnOff:
+ $ref: '#/components/messages/turnOnOff'
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+ lightsDim:
+ address: 'smartylighting/streetlights/1/0/action/{streetlightId}/dim'
+ messages:
+ dimLight:
+ $ref: '#/components/messages/dimLight'
+ parameters:
+ streetlightId:
+ $ref: '#/components/parameters/streetlightId'
+operations:
+ receiveLightMeasurement:
+ action: receive
+ channel:
+ $ref: '#/channels/lightingMeasured'
+ summary: >-
+ Inform about environmental lighting conditions of a particular
+ streetlight.
+ traits:
+ - $ref: '#/components/operationTraits/mqtt'
+ messages:
+ - $ref: '#/channels/lightingMeasured/messages/lightMeasured'
+ turnOn:
+ action: send
+ channel:
+ $ref: '#/channels/lightTurnOn'
+ traits:
+ - $ref: '#/components/operationTraits/mqtt'
+ messages:
+ - $ref: '#/channels/lightTurnOn/messages/turnOn'
+ turnOff:
+ action: send
+ channel:
+ $ref: '#/channels/lightTurnOff'
+ traits:
+ - $ref: '#/components/operationTraits/mqtt'
+ messages:
+ - $ref: '#/channels/lightTurnOff/messages/turnOff'
+ dimLight:
+ action: send
+ channel:
+ $ref: '#/channels/lightsDim'
+ traits:
+ - $ref: '#/components/operationTraits/mqtt'
+ messages:
+ - $ref: '#/channels/lightsDim/messages/dimLight'
+components:
+ messages:
+ lightMeasured:
+ name: lightMeasured
+ title: Light measured
+ summary: >-
+ Inform about environmental lighting conditions of a particular
+ streetlight.
+ contentType: application/json
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: '#/components/schemas/lightMeasuredPayload'
+ turnOnOff:
+ name: turnOnOff
+ title: Turn on/off
+ summary: Command a particular streetlight to turn the lights on or off.
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: '#/components/schemas/turnOnOffPayload'
+ dimLight:
+ name: dimLight
+ title: Dim light
+ summary: Command a particular streetlight to dim the lights.
+ traits:
+ - $ref: '#/components/messageTraits/commonHeaders'
+ payload:
+ $ref: '#/components/schemas/dimLightPayload'
+ schemas:
+ lightMeasuredPayload:
+ type: object
+ properties:
+ lumens:
+ type: integer
+ minimum: 0
+ description: Light intensity measured in lumens.
+ sentAt:
+ $ref: '#/components/schemas/sentAt'
+ turnOnOffPayload:
+ type: object
+ properties:
+ command:
+ type: string
+ enum:
+ - 'on'
+ - 'off'
+ description: Whether to turn on or off the light.
+ sentAt:
+ $ref: '#/components/schemas/sentAt'
+ dimLightPayload:
+ type: object
+ properties:
+ percentage:
+ type: integer
+ description: Percentage to which the light should be dimmed to.
+ minimum: 0
+ maximum: 100
+ sentAt:
+ $ref: '#/components/schemas/sentAt'
+ sentAt:
+ type: string
+ format: date-time
+ description: Date and time when the message was sent.
+ securitySchemes:
+ apiKey:
+ type: apiKey
+ in: user
+ description: Provide your API key as the user and leave the password empty.
+ supportedOauthFlows:
+ type: oauth2
+ description: Flows to support OAuth 2.0
+ flows:
+ implicit:
+ authorizationUrl: 'https://authserver.example/auth'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ password:
+ tokenUrl: 'https://authserver.example/token'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ clientCredentials:
+ tokenUrl: 'https://authserver.example/token'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ authorizationCode:
+ authorizationUrl: 'https://authserver.example/auth'
+ tokenUrl: 'https://authserver.example/token'
+ refreshUrl: 'https://authserver.example/refresh'
+ availableScopes:
+ 'streetlights:on': Ability to switch lights on
+ 'streetlights:off': Ability to switch lights off
+ 'streetlights:dim': Ability to dim the lights
+ openIdConnectWellKnown:
+ type: openIdConnect
+ openIdConnectUrl: 'https://authserver.example/.well-known'
+ parameters:
+ streetlightId:
+ description: The ID of the streetlight.
+ messageTraits:
+ commonHeaders:
+ headers:
+ type: object
+ properties:
+ my-app-header:
+ type: integer
+ minimum: 0
+ maximum: 100
+ operationTraits:
+ mqtt:
+ bindings:
+ mqtt:
+ qos: 1
diff --git a/apps/studio-next/src/examples/tutorials/invalid.yml b/apps/studio-next/src/examples/tutorials/invalid.yml
new file mode 100644
index 000000000..aa9921099
--- /dev/null
+++ b/apps/studio-next/src/examples/tutorials/invalid.yml
@@ -0,0 +1,46 @@
+# This invalid file exists solely for educational purposes, and if you come across it, here is the tutorial: https://www.asyncapi.com/docs/tutorials/studio-document-validation
+
+asyncapi: 3.0.0
+info:
+ title: Streetlights API
+ version: '1.0.0'
+ description: |
+ The Smartylighting Streetlights API allows you
+ to remotely manage the city lights.
+ license:
+ name: Apache 2.0
+ url: 'https://www.apache.org/licenses/LICENSE-2.0'
+
+servers:
+ mosquitto:
+ url: test.mosquitto.org
+ protocol: mqtt
+
+channels:
+ lightMeasured:
+ address: 'light/measured'
+ messages:
+ lightMeasuredMessage:
+ name: LightMeasured
+ payload:
+ type: object
+ properties:
+ id:
+ type: integer
+ minimum: true
+ description: Id of the streetlight.
+ lumens:
+ type: integer
+ minimum: 0
+ description: Light intensity measured in lumens.
+ sentAt:
+ type: string
+ format: date-time
+ description: Date and time when the message was sent.
+
+operations:
+ onLightMeasured:
+ action: 'receive'
+ summary: Inform about environmental lighting conditions for a particular streetlight.
+ channel:
+ $ref: '#/channels/lightMeasured'
diff --git a/apps/studio-next/src/examples/websocket-gemini.yml b/apps/studio-next/src/examples/websocket-gemini.yml
new file mode 100644
index 000000000..0528487e1
--- /dev/null
+++ b/apps/studio-next/src/examples/websocket-gemini.yml
@@ -0,0 +1,305 @@
+asyncapi: 3.0.0
+info:
+ title: Gemini Market Data Websocket API
+ version: 1.0.0
+ description: |-
+ Market data is a public API that streams all the market data on a given
+ symbol.
+
+
+ You can quickly play with the API using
+ [websocat](https://github.com/vi/websocat#installation) like this:
+
+ ```bash
+
+ websocat wss://api.gemini.com/v1/marketdata/btcusd?heartbeat=true -S
+
+ ```
+ contact:
+ name: Gemini
+ url: 'https://www.gemini.com/'
+ externalDocs:
+ url: 'https://docs.sandbox.gemini.com/websocket-api/#market-data'
+servers:
+ public:
+ host: api.gemini.com
+ protocol: wss
+channels:
+ marketDataV1:
+ address: '/v1/marketdata/{symbol}'
+ messages:
+ marketData:
+ $ref: '#/components/messages/marketData'
+ parameters:
+ symbol:
+ enum:
+ - btcusd
+ - ethbtc
+ - ethusd
+ - zecusd
+ - zecbtc
+ - zeceth
+ - zecbch
+ - zecltc
+ - bchusd
+ - bchbtc
+ - bcheth
+ - ltcusd
+ - ltcbtc
+ - ltceth
+ - ltcbch
+ - batusd
+ - daiusd
+ - linkusd
+ - oxtusd
+ - batbtc
+ - linkbtc
+ - oxtbtc
+ - bateth
+ - linketh
+ - oxteth
+ - ampusd
+ - compusd
+ - paxgusd
+ - mkrusd
+ - zrxusd
+ - kncusd
+ - manausd
+ - storjusd
+ - snxusd
+ - crvusd
+ - balusd
+ - uniusd
+ - renusd
+ - umausd
+ - yfiusd
+ - btcdai
+ - ethdai
+ - aaveusd
+ - filusd
+ - btceur
+ - btcgbp
+ - etheur
+ - ethgbp
+ - btcsgd
+ - ethsgd
+ - sklusd
+ - grtusd
+ - bntusd
+ - 1inchusd
+ - enjusd
+ - lrcusd
+ - sandusd
+ - cubeusd
+ - lptusd
+ - bondusd
+ - maticusd
+ - injusd
+ - sushiusd
+ description: |-
+ Symbols are formatted as CCY1CCY2 where prices are in CCY2 and
+ quantities are in CCY1. To read more click
+ [here](https://docs.sandbox.gemini.com/websocket-api/#symbols-and-minimums).
+ bindings:
+ ws:
+ bindingVersion: 0.1.0
+ query:
+ type: object
+ description: |-
+ The semantics of entry type filtering is:
+
+
+ If any entry type is specified as true or false, all of them must be
+ explicitly flagged true to show up in the response
+
+ If no entry types filtering parameters are included in the url, then
+ all entry types will appear in the response
+
+
+ NOTE: top_of_book has no meaning and initial book events are empty
+ when only trades is specified
+ properties:
+ heartbeat:
+ type: boolean
+ default: false
+ description: |-
+ Optionally add this parameter and set to true to receive a
+ heartbeat every 5 seconds
+ top_of_book:
+ type: boolean
+ default: false
+ description: |-
+ If absent or false, receive full order book depth; if present
+ and true, receive top of book only. Only applies to bids and
+ offers.
+ bids:
+ type: boolean
+ default: true
+ description: Include bids in change events
+ offers:
+ type: boolean
+ default: true
+ description: Include asks in change events
+ trades:
+ type: boolean
+ default: true
+ description: Include trade events
+ auctions:
+ type: boolean
+ default: true
+ description: Include auction events
+operations:
+ sendMarketData:
+ action: send
+ channel:
+ $ref: '#/channels/marketDataV1'
+ summary: Receive market updates on a given symbol
+ messages:
+ - $ref: '#/channels/marketDataV1/messages/marketData'
+components:
+ messages:
+ marketData:
+ summary: Message with marked data information.
+ description: |-
+ The initial response message will show the existing state of the order
+ book. Subsequent messages will show all executed trades, as well as all
+ other changes to the order book from orders placed or canceled.
+ payload:
+ $ref: '#/components/schemas/market'
+ examples:
+ - name: updateMessage
+ summary: >-
+ Example of an update message that contains a change in price
+ information.
+ payload:
+ type: update
+ eventId: 36902233362
+ timestamp: '1619769673'
+ timestampms: '1619769673527'
+ socket_sequence: 661
+ events:
+ - type: change
+ side: bid
+ price: '54350.40'
+ remaining: '0.002'
+ delta: '0.002'
+ reason: place
+ - name: heartbeatMessage
+ summary: Example of additional heartbeat message when you enable them.
+ payload:
+ type: heartbeat
+ socket_sequence: 1656
+ schemas:
+ market:
+ type: object
+ oneOf:
+ - $ref: '#/components/schemas/heartbeat'
+ - $ref: '#/components/schemas/update'
+ heartbeat:
+ allOf:
+ - properties:
+ type:
+ type: string
+ const: heartbeat
+ required:
+ - type
+ - $ref: '#/components/schemas/default'
+ update:
+ allOf:
+ - properties:
+ type:
+ type: string
+ const: update
+ eventId:
+ type: integer
+ description: |-
+ A monotonically increasing sequence number indicating when this
+ change occurred. These numbers are persistent and consistent
+ between market data connections.
+ events:
+ $ref: '#/components/schemas/events'
+ timestamp:
+ type: string
+ format: date-time
+ description: |-
+ The timestamp in seconds for this group of events (included for
+ compatibility reasons). We recommend using the timestampms field
+ instead.
+ timestampms:
+ type: string
+ format: time
+ description: The timestamp in milliseconds for this group of events.
+ required:
+ - type
+ - eventId
+ - events
+ - timestamp
+ - timestampms
+ - $ref: '#/components/schemas/default'
+ default:
+ type: object
+ description: |-
+ This object is always part of the payload. In case of type=heartbeat,
+ these are the only fields.
+ required:
+ - type
+ - socket_sequence
+ properties:
+ socket_sequence:
+ type: integer
+ description: |-
+ zero-indexed monotonic increasing sequence number attached to each
+ message sent - if there is a gap in this sequence, you have missed a
+ message. If you choose to enable heartbeats, then heartbeat and
+ update messages will share a single increasing sequence. See
+ [Sequence
+ Numbers](https://docs.sandbox.gemini.com/websocket-api/#sequence-numbers)
+ for more information.
+ events:
+ type: array
+ description: |-
+ Either a change to the order book, or the indication that a trade has
+ occurred.
+ items:
+ type: object
+ additionalProperties: false
+ properties:
+ type:
+ type: string
+ enum:
+ - trade
+ - change
+ - 'auction, block_trade'
+ price:
+ type: number
+ multipleOf: 1
+ description: The price of this order book entry.
+ side:
+ type: string
+ enum:
+ - bid
+ - side
+ reason:
+ type: string
+ enum:
+ - place
+ - trade
+ - cancel
+ - initial
+ description: |-
+ Indicates why the change has occurred. initial is for the initial
+ response message, which will show the entire existing state of the
+ order book.
+ remaining:
+ type: number
+ multipleOf: 1
+ description: |-
+ The quantity remaining at that price level after this change
+ occurred. May be zero if all orders at this price level have been
+ filled or canceled.
+ delta:
+ type: number
+ multipleOf: 1
+ description: |-
+ The quantity changed. May be negative, if an order is filled or
+ canceled. For initial messages, delta will equal remaining.
diff --git a/apps/studio-next/src/helpers/debounce.ts b/apps/studio-next/src/helpers/debounce.ts
new file mode 100644
index 000000000..964861b87
--- /dev/null
+++ b/apps/studio-next/src/helpers/debounce.ts
@@ -0,0 +1,24 @@
+export function debounce(func: (...args: any[]) => any, wait: number, immediate?: boolean) {
+ let timeout: number | null;
+
+ return function executedFunction(...args: any[]) {
+ // @ts-ignore
+ const context = this;
+
+ const later = function() {
+ timeout = null;
+ if (!immediate) {
+ func.apply(context, args);
+ }
+ };
+
+ const callNow = immediate && !timeout;
+
+ timeout && clearTimeout(timeout);
+ timeout = setTimeout(later, wait) as unknown as number;
+
+ if (callNow) {
+ func.apply(context, args);
+ }
+ };
+}
diff --git a/apps/studio-next/src/helpers/index.ts b/apps/studio-next/src/helpers/index.ts
new file mode 100644
index 000000000..29cd16b41
--- /dev/null
+++ b/apps/studio-next/src/helpers/index.ts
@@ -0,0 +1,3 @@
+export * from './debounce';
+export * from './isDeepEqual';
+export * from './useOutsideClickCallback';
diff --git a/apps/studio-next/src/helpers/isDeepEqual.ts b/apps/studio-next/src/helpers/isDeepEqual.ts
new file mode 100644
index 000000000..e9530573b
--- /dev/null
+++ b/apps/studio-next/src/helpers/isDeepEqual.ts
@@ -0,0 +1,26 @@
+function isObject(object: Record) {
+ return object && typeof object === 'object';
+}
+
+export function isDeepEqual(object1: Record, object2: Record) {
+ const objKeys1 = Object.keys(object1);
+ const objKeys2 = Object.keys(object2);
+
+ if (objKeys1.length !== objKeys2.length) {
+ return false;
+ }
+
+ for (const key of objKeys1) {
+ const value1 = object1[String(key)];
+ const value2 = object2[String(key)];
+
+ const isObjects = isObject(value1) && isObject(value2);
+
+ if ((isObjects && !isDeepEqual(value1, value2)) ||
+ (!isObjects && value1 !== value2)
+ ) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/apps/studio-next/src/helpers/useOutsideClickCallback.ts b/apps/studio-next/src/helpers/useOutsideClickCallback.ts
new file mode 100644
index 000000000..cd145e9d8
--- /dev/null
+++ b/apps/studio-next/src/helpers/useOutsideClickCallback.ts
@@ -0,0 +1,18 @@
+import { useEffect } from 'react';
+
+import type { MutableRefObject } from 'react';
+
+export function useOutsideClickCallback(ref: MutableRefObject, callback: () => void) {
+ useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (ref.current && !ref.current.contains(event.target as any)) {
+ callback();
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [ref]);
+}
\ No newline at end of file
diff --git a/apps/studio-next/src/services/abstract.service.ts b/apps/studio-next/src/services/abstract.service.ts
new file mode 100644
index 000000000..048194797
--- /dev/null
+++ b/apps/studio-next/src/services/abstract.service.ts
@@ -0,0 +1,10 @@
+import type { Services } from './index';
+
+export abstract class AbstractService {
+ constructor(
+ protected readonly svcs: Services = {} as Services,
+ ) {}
+
+ public onInit(): void | Promise {}
+ public async afterAppInit(): Promise {}
+}
diff --git a/apps/studio-next/src/services/app.service.ts b/apps/studio-next/src/services/app.service.ts
new file mode 100644
index 000000000..d315c7450
--- /dev/null
+++ b/apps/studio-next/src/services/app.service.ts
@@ -0,0 +1,88 @@
+import { AbstractService } from './abstract.service';
+
+import { show } from '@ebay/nice-modal-react';
+
+import { RedirectedModal } from '../components/Modals';
+
+import { appState, filesState } from '@/state';
+
+export class ApplicationService extends AbstractService {
+ override async onInit() {
+ // subscribe to state to hide preloader
+ this.hidePreloader();
+
+ const { readOnly, url, base64 } =
+ this.svcs.navigationSvc.getUrlParameters();
+ // readOnly state should be only set to true when someone pass also url or base64 parameter
+ const isStrictReadonly = Boolean(readOnly && (url || base64));
+
+ let error: any;
+ try {
+ await this.fetchResource(url, base64);
+ } catch (err) {
+ error = err;
+ console.error(err);
+ }
+
+ if (error) {
+ appState.setState({ initErrors: [error] });
+ }
+
+ if (isStrictReadonly && !error) {
+ appState.setState({
+ readOnly,
+ initialized: true,
+ });
+ }
+ }
+
+ public async afterAppInit() {
+ const { readOnly, url, base64, redirectedFrom } =
+ this.svcs.navigationSvc.getUrlParameters();
+ const isStrictReadonly = Boolean(readOnly && (url || base64));
+
+ // show RedirectedModal modal if the redirectedFrom is set (only when readOnly state is set to false)
+ if (!isStrictReadonly && redirectedFrom) {
+ show(RedirectedModal);
+ }
+ }
+
+ private async fetchResource(url: string | null, base64: string | null) {
+ if (!url && !base64) {
+ return;
+ }
+
+ const { updateFile } = filesState.getState();
+ let content = '';
+ if (url) {
+ content = await fetch(url).then((res) => res.text());
+ } else if (base64) {
+ content = this.svcs.formatSvc.decodeBase64(base64);
+ }
+
+ const language = this.svcs.formatSvc.retrieveLangauge(content);
+ const source = url || undefined;
+ updateFile('asyncapi', {
+ content,
+ language,
+ source,
+ from: url ? 'url' : 'base64',
+ });
+ await this.svcs.parserSvc.parse('asyncapi', content, { source });
+ }
+
+ private hidePreloader() {
+ const unsunscribe = appState.subscribe((state, prevState) => {
+ if (!prevState.initialized && state.initialized) {
+ const preloader = document.getElementById('preloader');
+ if (preloader) {
+ preloader.classList.add('loaded');
+ setTimeout(() => {
+ preloader.remove();
+ }, 350);
+ unsunscribe();
+ }
+ }
+ });
+ }
+}
diff --git a/apps/studio-next/src/services/converter.service.ts b/apps/studio-next/src/services/converter.service.ts
new file mode 100644
index 000000000..b8a46060c
--- /dev/null
+++ b/apps/studio-next/src/services/converter.service.ts
@@ -0,0 +1,26 @@
+import { AbstractService } from './abstract.service';
+
+import { convert } from '@asyncapi/converter';
+
+import type { ConvertVersion, ConvertOptions } from '@asyncapi/converter';
+
+export class ConverterService extends AbstractService {
+ async convert(
+ spec: string,
+ version?: ConvertVersion,
+ options?: ConvertOptions,
+ ): Promise {
+ version = version || this.svcs.specificationSvc.latestVersion;
+
+ try {
+ const converted = convert(spec, version, options);
+ if (typeof converted === 'object') {
+ return JSON.stringify(converted, undefined, 2);
+ }
+ return converted;
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+}
\ No newline at end of file
diff --git a/apps/studio-next/src/services/editor.service.tsx b/apps/studio-next/src/services/editor.service.tsx
new file mode 100644
index 000000000..8ec0cc2c2
--- /dev/null
+++ b/apps/studio-next/src/services/editor.service.tsx
@@ -0,0 +1,365 @@
+import { AbstractService } from './abstract.service';
+
+import { KeyMod, KeyCode } from 'monaco-editor/esm/vs/editor/editor.api';
+import { DiagnosticSeverity } from '@asyncapi/parser';
+import { Range, MarkerSeverity } from 'monaco-editor/esm/vs/editor/editor.api';
+import toast from 'react-hot-toast';
+import fileDownload from 'js-file-download';
+
+import { appState, documentsState, filesState, settingsState } from '@/state';
+
+import type * as monacoAPI from 'monaco-editor/esm/vs/editor/editor.api';
+import type { Diagnostic } from '@asyncapi/parser';
+import type { ConvertVersion } from '@asyncapi/converter';
+import type { File } from '@/state/files.state';
+
+export interface UpdateState {
+ content: string;
+ updateModel?: boolean;
+ sendToServer?: boolean;
+ file?: Partial;
+}
+
+export class EditorService extends AbstractService {
+ private created = false;
+ private decorations: Map = new Map();
+ private instance: monacoAPI.editor.IStandaloneCodeEditor | undefined;
+
+ override onInit() {
+ this.subcribeToDocuments();
+ }
+
+ async onDidCreate(editor: monacoAPI.editor.IStandaloneCodeEditor) {
+ if (this.created) {
+ return;
+ }
+ this.created = true;
+ this.instance = editor;
+
+ // parse on first run - only when document is undefined
+ const document = documentsState.getState().documents.asyncapi;
+ if (!document) {
+ await this.svcs.parserSvc.parse('asyncapi', editor.getValue());
+ } else {
+ this.applyMarkersAndDecorations(document.diagnostics.filtered);
+ }
+
+ // apply save command
+ editor.addCommand(
+ KeyMod.CtrlCmd | KeyCode.KeyS,
+ () => this.saveToLocalStorage(),
+ );
+
+ appState.setState({ initialized: true });
+ }
+
+ get editor(): monacoAPI.editor.IStandaloneCodeEditor | undefined {
+ return this.instance;
+ }
+
+ get value(): string {
+ return this.editor?.getModel()?.getValue() as string;
+ }
+
+ updateState({
+ content,
+ updateModel = false,
+ sendToServer = true,
+ file = {},
+ }: UpdateState) {
+ const currentContent = filesState.getState().files['asyncapi']?.content;
+ if (currentContent === content || typeof content !== 'string') {
+ return;
+ }
+
+ const language = file.language || this.svcs.formatSvc.retrieveLangauge(content);
+ if (!language) {
+ return;
+ }
+
+ if (sendToServer) {
+ this.svcs.socketClientSvc.send('file:update', { code: content });
+ }
+
+ if (updateModel && this.editor) {
+ const model = this.editor.getModel();
+ if (model) {
+ model.setValue(content);
+ }
+ }
+
+ const { updateFile } = filesState.getState();
+ updateFile('asyncapi', {
+ language,
+ content,
+ modified: this.getFromLocalStorage() !== content,
+ ...file,
+ });
+ }
+
+ async convertSpec(version?: ConvertVersion | string) {
+ const converted = await this.svcs.converterSvc.convert(this.value, version as ConvertVersion);
+ this.updateState({ content: converted, updateModel: true });
+ }
+
+ async importFromURL(url: string): Promise {
+ if (url) {
+ return fetch(url)
+ .then(res => res.text())
+ .then(async text => {
+ this.updateState({
+ content: text,
+ updateModel: true,
+ file: {
+ source: url,
+ from: 'url'
+ },
+ });
+ })
+ .catch(err => {
+ console.error(err);
+ throw err;
+ });
+ }
+ }
+
+ async importFile(files: FileList | null) {
+ if (files === null || files?.length !== 1) {
+ return;
+ }
+ const file = files.item(0);
+ if (!file) {
+ return;
+ }
+
+ // Check if file is valid (only JSON and YAML are allowed currently) ----Change afterwards as per the requirement
+ if (
+ file.type !== 'application/json' &&
+ file.type !== 'application/x-yaml' &&
+ file.type !== 'application/yaml'
+ ) {
+ throw new Error('Invalid file type');
+ }
+
+ const fileReader = new FileReader();
+ fileReader.onload = fileLoadedEvent => {
+ const content = fileLoadedEvent.target?.result;
+ this.updateState({ content: String(content), updateModel: true });
+ };
+ fileReader.readAsText(file, 'UTF-8');
+ }
+
+ async importBase64(content: string) {
+ try {
+ const decoded = this.svcs.formatSvc.decodeBase64(content);
+ this.updateState({
+ content: String(decoded),
+ updateModel: true,
+ file: {
+ from: 'base64',
+ source: undefined,
+ },
+ });
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+
+ async exportAsBase64() {
+ try {
+ const file = filesState.getState().files['asyncapi'];
+ return this.svcs.formatSvc.encodeBase64(file.content);
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+
+ async convertToYaml() {
+ try {
+ const yamlContent = this.svcs.formatSvc.convertToYaml(this.value);
+ if (yamlContent) {
+ this.updateState({
+ content: yamlContent,
+ updateModel: true,
+ file: {
+ language: 'yaml',
+ }
+ });
+ }
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+
+ async convertToJSON() {
+ try {
+ const jsonContent = this.svcs.formatSvc.convertToJSON(this.value);
+ if (jsonContent) {
+ this.updateState({
+ content: jsonContent,
+ updateModel: true,
+ file: {
+ language: 'json',
+ }
+ });
+ }
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+
+ async saveAsYaml() {
+ try {
+ const yamlContent = this.svcs.formatSvc.convertToYaml(this.value);
+ if (yamlContent) {
+ this.downloadFile(yamlContent, `${this.fileName}.yaml`);
+ }
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+
+ async saveAsJSON() {
+ try {
+ const jsonContent = this.svcs.formatSvc.convertToJSON(this.value);
+ if (jsonContent) {
+ this.downloadFile(jsonContent, `${this.fileName}.json`);
+ }
+ } catch (err) {
+ console.error(err);
+ throw err;
+ }
+ }
+
+ saveToLocalStorage(editorValue?: string, notify = true) {
+ editorValue = editorValue || this.value;
+ localStorage.setItem('document', editorValue);
+
+ const { updateFile } = filesState.getState();
+ updateFile('asyncapi', {
+ from: 'storage',
+ source: undefined,
+ modified: false,
+ });
+
+ if (notify) {
+ if (settingsState.getState().editor.autoSaving) {
+ toast.success(
+
+
+ Studio is currently saving your work automatically 💪
+
+
,
+ );
+ } else {
+ toast.success(
+
+
+ Document succesfully saved to the local storage!
+
+
,
+ );
+ }
+ }
+ }
+
+ getFromLocalStorage() {
+ return localStorage.getItem('document');
+ }
+
+ private applyMarkersAndDecorations(diagnostics: Diagnostic[] = []) {
+ const editor = this.editor;
+ const model = editor?.getModel();
+ const monaco = this.svcs.monacoSvc.monaco;
+
+ if (!editor || !model || !monaco) {
+ return;
+ }
+
+ const { markers, decorations } = this.createMarkersAndDecorations(diagnostics);
+ monaco.editor.setModelMarkers(model, 'asyncapi', markers);
+ let oldDecorations = this.decorations.get('asyncapi') || [];
+ oldDecorations = editor.deltaDecorations(oldDecorations, decorations);
+ this.decorations.set('asyncapi', oldDecorations);
+ }
+
+ createMarkersAndDecorations(diagnostics: Diagnostic[] = []) {
+ const newDecorations: monacoAPI.editor.IModelDecoration[] = [];
+ const newMarkers: monacoAPI.editor.IMarkerData[] = [];
+
+ diagnostics.forEach(diagnostic => {
+ const { message, range, severity } = diagnostic;
+
+ if (severity !== DiagnosticSeverity.Error) {
+ newDecorations.push({
+ id: 'asyncapi',
+ ownerId: 0,
+ range: new Range(
+ range.start.line + 1,
+ range.start.character + 1,
+ range.end.line + 1,
+ range.end.character + 1
+ ),
+ options: {
+ glyphMarginClassName: this.getSeverityClassName(severity),
+ glyphMarginHoverMessage: { value: message },
+ },
+ });
+ return;
+ }
+
+ newMarkers.push({
+ startLineNumber: range.start.line + 1,
+ startColumn: range.start.character + 1,
+ endLineNumber: range.end.line + 1,
+ endColumn: range.end.character + 1,
+ severity: this.getSeverity(severity),
+ message,
+ });
+ });
+
+ return { decorations: newDecorations, markers: newMarkers };
+ }
+
+ private getSeverity(severity: DiagnosticSeverity): monacoAPI.MarkerSeverity {
+ switch (severity) {
+ case DiagnosticSeverity.Error: return MarkerSeverity.Error;
+ case DiagnosticSeverity.Warning: return MarkerSeverity.Warning;
+ case DiagnosticSeverity.Information: return MarkerSeverity.Info;
+ case DiagnosticSeverity.Hint: return MarkerSeverity.Hint;
+ default: return MarkerSeverity.Error;
+ }
+ }
+
+ private getSeverityClassName(severity: DiagnosticSeverity): string {
+ switch (severity) {
+ case DiagnosticSeverity.Warning: return 'diagnostic-warning';
+ case DiagnosticSeverity.Information: return 'diagnostic-information';
+ case DiagnosticSeverity.Hint: return 'diagnostic-hint';
+ default: return 'diagnostic-warning';
+ }
+ }
+
+ private fileName = 'asyncapi';
+ private downloadFile(content: string, fileName: string) {
+ return fileDownload(content, fileName);
+ }
+
+ private subcribeToDocuments() {
+ documentsState.subscribe((state, prevState) => {
+ const newDocuments = state.documents;
+ const oldDocuments = prevState.documents;
+
+ Object.entries(newDocuments).forEach(([uri, document]) => {
+ const oldDocument = oldDocuments[String(uri)];
+ if (document === oldDocument) return;
+ this.applyMarkersAndDecorations(document.diagnostics.filtered);
+ });
+ });
+ }
+}
diff --git a/apps/studio-next/src/services/format.service.ts b/apps/studio-next/src/services/format.service.ts
new file mode 100644
index 000000000..f0341e479
--- /dev/null
+++ b/apps/studio-next/src/services/format.service.ts
@@ -0,0 +1,42 @@
+import { AbstractService } from './abstract.service';
+
+import { encode, decode } from 'js-base64';
+import YAML from 'js-yaml';
+
+export class FormatService extends AbstractService {
+ convertToYaml(spec: string) {
+ try {
+ // Editor content -> JS object -> YAML string
+ const jsonContent = YAML.load(spec);
+ return YAML.dump(jsonContent);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ convertToJSON(spec: string) {
+ try {
+ // JSON or YAML String -> JS object
+ const jsonContent = YAML.load(spec);
+ // JS Object -> pretty JSON string
+ return JSON.stringify(jsonContent, null, 2);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ encodeBase64(content: string) {
+ return encode(content);
+ }
+
+ decodeBase64(content: string) {
+ return decode(content);
+ }
+
+ retrieveLangauge(content: string) {
+ if (content.trimStart()[0] === '{') {
+ return 'json';
+ }
+ return 'yaml';
+ }
+}
diff --git a/apps/studio-next/src/services/index.ts b/apps/studio-next/src/services/index.ts
new file mode 100644
index 000000000..6b8ef6b15
--- /dev/null
+++ b/apps/studio-next/src/services/index.ts
@@ -0,0 +1,65 @@
+'use client'
+
+import { createContext, useContext } from 'react';
+
+import { ApplicationService } from './app.service';
+import { ConverterService } from './converter.service';
+import { EditorService } from './editor.service';
+import { FormatService } from './format.service';
+import { MonacoService } from './monaco.service';
+import { NavigationService } from './navigation.service';
+import { ParserService } from './parser.service';
+import { ServerAPIService } from './server-api.service';
+import { SettingsService } from './settings.service';
+import { SocketClient } from './socket-client.service';
+import { SpecificationService } from './specification.service';
+
+export type Services = {
+ appSvc: ApplicationService;
+ converterSvc: ConverterService;
+ editorSvc: EditorService;
+ formatSvc: FormatService;
+ monacoSvc: MonacoService;
+ navigationSvc: NavigationService;
+ parserSvc: ParserService;
+ serverAPISvc: ServerAPIService;
+ settingsSvc: SettingsService;
+ socketClientSvc: SocketClient;
+ specificationSvc: SpecificationService;
+}
+
+const servicesCtx = createContext({} as Services);
+
+export function useServices() {
+ return useContext(servicesCtx);
+}
+
+export const ServicesProvider = servicesCtx.Provider;
+
+export async function createServices() {
+ const services: Services = {} as Services;
+
+ services.parserSvc = new ParserService(services);
+ services.appSvc = new ApplicationService(services);
+ services.converterSvc = new ConverterService(services);
+ services.editorSvc = new EditorService(services);
+ services.formatSvc = new FormatService(services);
+ services.monacoSvc = new MonacoService(services);
+ services.navigationSvc = new NavigationService(services);
+ services.serverAPISvc = new ServerAPIService(services);
+ services.settingsSvc = new SettingsService(services);
+ services.socketClientSvc = new SocketClient(services);
+ services.specificationSvc = new SpecificationService(services);
+
+ for (const service in services) {
+ await services[service as keyof Services].onInit();
+ }
+
+ return services;
+}
+
+export async function afterAppInit(services: Services) {
+ for (const service in services) {
+ await services[service as keyof Services].afterAppInit();
+ }
+}
diff --git a/apps/studio-next/src/services/monaco.service.ts b/apps/studio-next/src/services/monaco.service.ts
new file mode 100644
index 000000000..aad59eaeb
--- /dev/null
+++ b/apps/studio-next/src/services/monaco.service.ts
@@ -0,0 +1,186 @@
+import { AbstractService } from './abstract.service';
+
+import { loader } from '@monaco-editor/react';
+import { setDiagnosticsOptions } from 'monaco-yaml';
+import YAML from 'js-yaml';
+
+import { documentsState, filesState } from '@/state';
+
+import type * as monacoAPI from 'monaco-editor/esm/vs/editor/editor.api';
+import type { DiagnosticsOptions as YAMLDiagnosticsOptions } from 'monaco-yaml';
+import type { SpecVersions } from '../types';
+import type { JSONSchema7 } from 'json-schema';
+
+export class MonacoService extends AbstractService {
+ private jsonSchemaSpecs: Map = new Map();
+ private jsonSchemaDefinitions: monacoAPI.languages.json.DiagnosticsOptions['schemas'] = [];
+ private actualVersion = 'X.X.X';
+ private monacoInstance!: typeof monacoAPI;
+
+ override async onInit() {
+ // load monaco instance
+ await this.loadMonaco();
+ // set monaco theme
+ this.setMonacoTheme();
+ // prepare JSON Schema specs and definitions for JSON/YAML language config
+ this.prepareJSONSchemas();
+ // load initial language config (for json and yaml)
+ this.setLanguageConfig(this.svcs.specificationSvc.latestVersion);
+ // subscribe to document to update JSON/YAML language config
+ this.subcribeToDocuments();
+ }
+
+ get monaco() {
+ return this.monacoInstance;
+ }
+
+ updateLanguageConfig(version: SpecVersions = this.svcs.specificationSvc.latestVersion) {
+ if (version === this.actualVersion) {
+ return;
+ }
+ this.setLanguageConfig(version);
+ this.actualVersion = version;
+ }
+
+ private setLanguageConfig(version: SpecVersions = this.svcs.specificationSvc.latestVersion) {
+ if (!this.monaco) {
+ return;
+ }
+ const options = this.prepareLanguageConfig(version);
+
+ // json
+ const json = this.monaco.languages.json;
+ if (json && json.jsonDefaults) {
+ json.jsonDefaults.setDiagnosticsOptions(options);
+ }
+
+ // yaml
+ setDiagnosticsOptions(options as YAMLDiagnosticsOptions);
+ }
+
+ private prepareLanguageConfig(
+ version: SpecVersions,
+ ): monacoAPI.languages.json.DiagnosticsOptions {
+ const spec = this.jsonSchemaSpecs.get(version);
+
+ return {
+ enableSchemaRequest: false,
+ hover: true,
+ completion: true,
+ validate: true,
+ format: true,
+ schemas: [
+ {
+ uri: spec.$id, // id of the AsyncAPI spec schema
+ fileMatch: ['*'], // associate with all models
+ schema: spec,
+ },
+ ...(this.jsonSchemaDefinitions || []),
+ ],
+ } as any;
+ }
+
+ private async loadMonaco() {
+ // in test environment we don't need monaco loaded
+ if (process.env.NODE_ENV === 'test') {
+ return;
+ }
+
+ const monaco = this.monacoInstance = await import('monaco-editor');
+ loader.config({ monaco });
+ }
+
+ private setMonacoTheme() {
+ if (!this.monaco) {
+ return;
+ }
+
+ this.monaco.editor.defineTheme('asyncapi-theme', {
+ base: 'vs-dark',
+ inherit: true,
+ colors: {
+ 'editor.background': '#252f3f',
+ 'editor.lineHighlightBackground': '#1f2a37',
+ },
+ rules: [{ token: '', background: '#252f3f' }],
+ });
+ }
+
+ private prepareJSONSchemas() {
+ const uris: string[] = [];
+ Object.entries(this.svcs.specificationSvc.specs).forEach(([version, spec]) => {
+ this.serializeSpec(spec, version, uris);
+ });
+ }
+
+ private serializeSpec(spec: JSONSchema7, version: string, uris: string[]) {
+ // copy whole spec
+ const copiedSpec = this.copySpecification(spec);
+
+ // serialize definitions
+ const definitions = Object.entries(copiedSpec.definitions || {}).map(([uri, schema]) => {
+ if (uri === 'http://json-schema.org/draft-07/schema') {
+ uri = 'https://json-schema.org/draft-07/schema';
+ }
+
+ return {
+ uri,
+ schema,
+ };
+ });
+ delete copiedSpec.definitions;
+
+ // save spec to map
+ this.jsonSchemaSpecs.set(version, copiedSpec);
+
+ // save definitions
+ definitions.forEach(definition => {
+ if (uris.includes(definition.uri)) {
+ return;
+ }
+
+ uris.push(definition.uri);
+ if (Array.isArray(this.jsonSchemaDefinitions)) {
+ this.jsonSchemaDefinitions.push(definition);
+ }
+ });
+ }
+
+ private copySpecification(spec: JSONSchema7): JSONSchema7 {
+ return JSON.parse(JSON.stringify(spec, (_, value) => {
+ if (
+ value === 'http://json-schema.org/draft-07/schema#' ||
+ value === 'http://json-schema.org/draft-07/schema'
+ ) {
+ return 'https://json-schema.org/draft-07/schema';
+ }
+ return value;
+ })) as JSONSchema7;
+ }
+
+ private subcribeToDocuments() {
+ documentsState.subscribe((state, prevState) => {
+ const newDocuments = state.documents;
+ const oldDocuments = prevState.documents;
+
+ Object.entries(newDocuments).forEach(([uri, document]) => {
+ const oldDocument = oldDocuments[String(uri)];
+ if (document === oldDocument) return;
+ const version = document.document?.version();
+ if (version) {
+ this.updateLanguageConfig(version as SpecVersions);
+ } else {
+ try {
+ const file = filesState.getState().files['asyncapi'];
+ if (file) {
+ const version = (YAML.load(file.content) as { asyncapi: SpecVersions }).asyncapi;
+ this.svcs.monacoSvc.updateLanguageConfig(version);
+ }
+ } catch (e: any) {
+ // intentional
+ }
+ }
+ });
+ });
+ }
+}
diff --git a/apps/studio-next/src/services/navigation.service.ts b/apps/studio-next/src/services/navigation.service.ts
new file mode 100644
index 000000000..b0180e935
--- /dev/null
+++ b/apps/studio-next/src/services/navigation.service.ts
@@ -0,0 +1,107 @@
+import { AbstractService } from './abstract.service';
+
+import type React from 'react';
+
+export class NavigationService extends AbstractService {
+ override async afterAppInit() {
+ try {
+ await this.scrollToHash();
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
+ } catch (err: any) {
+ console.error(err);
+ }
+ }
+
+ getUrlParameters() {
+ const urlParams = new URLSearchParams(window.location.search);
+ return {
+ url: urlParams.get('url') || urlParams.get('load'),
+ base64: urlParams.get('base64'),
+ readOnly: urlParams.get('readOnly') === 'true' || urlParams.get('readOnly') === '',
+ liveServer: urlParams.get('liveServer'),
+ redirectedFrom: urlParams.get('redirectedFrom'),
+ };
+ }
+
+ async scrollTo(
+ jsonPointer: string | Array,
+ hash: string,
+ ) {
+ try {
+ const doc = this.svcs.editorSvc;
+ const methodType = doc.value.startsWith('asyncapi') ? 'getRangeForYamlPath' : 'getRangeForJsonPath';
+ const range = this.svcs.parserSvc[methodType]('asyncapi', jsonPointer);
+
+ if (range) {
+ await this.scrollToEditorLine(range.start.line+1);
+ }
+
+ await this.scrollToHash(hash);
+ this.emitHashChangeEvent(hash);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ async scrollToHash(hash?: string) {
+ try {
+ const sanitizedHash = this.sanitizeHash(hash);
+ if (!sanitizedHash) {
+ return;
+ }
+
+ const items = document.querySelectorAll(`#${sanitizedHash}`);
+ if (items.length) {
+ const element = items[0];
+ typeof element.scrollIntoView === 'function' &&
+ element.scrollIntoView();
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ async scrollToEditorLine(line: number, character = 1) {
+ try {
+ const editor = this.svcs.editorSvc.editor;
+ if (editor) {
+ editor.revealLineInCenter(line);
+ editor.setPosition({ lineNumber: line, column: character });
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ highlightVisualiserNode(nodeId: string, setState: React.Dispatch>) {
+ function hashChanged() {
+ if (location.hash.startsWith(nodeId)) {
+ setState(true);
+ setTimeout(() => {
+ setState(false);
+ }, 1000);
+ }
+ }
+
+ window.addEventListener('hashchange', hashChanged);
+ return () => {
+ window.removeEventListener('hashchange', hashChanged);
+ };
+ }
+
+ private sanitizeHash(hash?: string): string | undefined {
+ hash = hash || window.location.hash.substring(1);
+ try {
+ const escapedHash = CSS.escape(hash);
+ return escapedHash.startsWith('#') ? hash.substring(1) : escapedHash;
+ } catch (err: any) {
+ return;
+ }
+ }
+
+ private emitHashChangeEvent(hash: string) {
+ hash = hash.startsWith('#') ? hash : `#${hash}`;
+ window.history.pushState({}, '', hash);
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
+ }
+}
diff --git a/apps/studio-next/src/services/parser.service.ts b/apps/studio-next/src/services/parser.service.ts
new file mode 100644
index 000000000..3bd59c4da
--- /dev/null
+++ b/apps/studio-next/src/services/parser.service.ts
@@ -0,0 +1,196 @@
+import { AbstractService } from './abstract.service';
+
+import { Parser, DiagnosticSeverity } from '@asyncapi/parser';
+import { OpenAPISchemaParser } from '@asyncapi/openapi-schema-parser';
+import { AvroSchemaParser } from '@asyncapi/avro-schema-parser';
+import { ProtoBuffSchemaParser } from '@asyncapi/protobuf-schema-parser';
+import { untilde } from '@asyncapi/parser/cjs/utils';
+
+import { isDeepEqual } from '@/helpers';
+import { filesState, documentsState, settingsState } from '@/state';
+
+import type { Diagnostic, ParseOptions } from '@asyncapi/parser';
+import type { DocumentDiagnostics } from '@/state/documents.state';
+import type { SchemaParser } from '@asyncapi/parser';
+import { getLocationForJsonPath, parseWithPointers } from '@stoplight/yaml';
+
+export class ParserService extends AbstractService {
+ private parser!: Parser;
+
+ override async onInit() {
+ this.parser = new Parser({
+ schemaParsers: [
+ // Temporary fix for TS error
+ OpenAPISchemaParser() as SchemaParser,
+ AvroSchemaParser() as SchemaParser,
+ ProtoBuffSchemaParser() as SchemaParser,
+ ],
+ __unstable: {
+ resolver: {
+ cache: false,
+ }
+ }
+ });
+
+ this.subscribeToFiles();
+ this.subscribeToSettings();
+ await this.parseSavedDocuments();
+ }
+
+ async parse(uri: string, spec: string, options: ParseOptions = {}): Promise {
+ if (uri !== 'asyncapi' && !options.source) {
+ options.source = uri;
+ }
+
+ let diagnostics: Diagnostic[] = [];
+ try {
+ const { document, diagnostics: _diagnostics, extras } = await this.parser.parse(spec, options);
+ diagnostics = _diagnostics;
+ if (document) {
+ this.updateDocument(uri, {
+ uri,
+ document,
+ diagnostics: this.createDiagnostics(diagnostics),
+ extras,
+ valid: true,
+ });
+ return;
+ }
+ } catch (err: unknown) {
+ console.log(err);
+ }
+
+ this.updateDocument(uri, {
+ uri,
+ document: undefined,
+ diagnostics: this.createDiagnostics(diagnostics),
+ extras: undefined,
+ valid: false,
+ });
+ }
+
+ getRangeForJsonPath(uri: string, jsonPath: string | Array) {
+ try {
+ const { documents } = documentsState.getState();
+
+ const extras = documents[String(uri)]?.extras;
+
+ if (extras) {
+ jsonPath = Array.isArray(jsonPath) ? jsonPath : jsonPath.split('/').map(untilde);
+ if (jsonPath[0] === '') jsonPath.shift();
+
+ return extras.document.getRangeForJsonPath(jsonPath);
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ getRangeForYamlPath(uri: string, jsonPath: string | Array) {
+ try {
+ const { documents } = documentsState.getState();
+
+ const extras = documents[String(uri)]?.extras;
+
+ if (extras) {
+ jsonPath = Array.isArray(jsonPath) ? jsonPath : jsonPath.split('/').map(untilde);
+ if (jsonPath[0] === '') jsonPath.shift();
+ const yamlDoc = parseWithPointers(this.svcs.editorSvc.value);
+
+ const location = getLocationForJsonPath(yamlDoc, jsonPath, true);
+ return location?.range || { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ filterDiagnostics(diagnostics: Diagnostic[]) {
+ const { governance: { show } } = settingsState.getState();
+ return diagnostics.filter(({ severity }) => {
+ return (
+ severity === DiagnosticSeverity.Error ||
+ (severity === DiagnosticSeverity.Warning && show.warnings) ||
+ (severity === DiagnosticSeverity.Information && show.informations) ||
+ (severity === DiagnosticSeverity.Hint && show.hints)
+ );
+ });
+ }
+
+ filterDiagnosticsBySeverity(diagnostics: Diagnostic[], severity: DiagnosticSeverity) {
+ return diagnostics.filter(diagnostic => diagnostic.severity === severity);
+ }
+
+ private updateDocument = documentsState.getState().updateDocument;
+
+ private createDiagnostics(diagnostics: Diagnostic[]) {
+ // map messages of invalid ref to file
+ diagnostics.forEach(diagnostic => {
+ if (diagnostic.code === 'invalid-ref' && diagnostic.message.endsWith('readFile is not a function')) {
+ diagnostic.message = 'File references are not yet supported in Studio';
+ }
+ });
+
+ const collections: DocumentDiagnostics = {
+ original: diagnostics,
+ filtered: [],
+ errors: [],
+ warnings: [],
+ informations: [],
+ hints: [],
+ };
+
+ const { governance: { show } } = settingsState.getState();
+ diagnostics.forEach(diagnostic => {
+ const severity = diagnostic.severity;
+ if (severity === DiagnosticSeverity.Error) {
+ collections.filtered.push(diagnostic);
+ collections.errors.push(diagnostic);
+ } else if (severity === DiagnosticSeverity.Warning && show.warnings) {
+ collections.filtered.push(diagnostic);
+ collections.warnings.push(diagnostic);
+ } else if (severity === DiagnosticSeverity.Information && show.informations) {
+ collections.filtered.push(diagnostic);
+ collections.informations.push(diagnostic);
+ } else if (severity === DiagnosticSeverity.Hint && show.hints) {
+ collections.filtered.push(diagnostic);
+ collections.hints.push(diagnostic);
+ }
+ });
+
+ return collections;
+ }
+
+ private subscribeToFiles() {
+ filesState.subscribe((state, prevState) => {
+ const newFiles = state.files;
+ const oldFiles = prevState.files;
+
+ Object.entries(newFiles).forEach(([uri, file]) => {
+ const oldFile = oldFiles[String(uri)];
+ if (file === oldFile) return;
+ this.parse(uri, file.content, { source: file.source }).catch(console.error);
+ });
+ });
+ }
+
+ private subscribeToSettings() {
+ settingsState.subscribe((state, prevState) => {
+ if (isDeepEqual(state.governance, prevState.governance)) return;
+
+ const { files } = filesState.getState();
+ Object.entries(files).forEach(([uri, file]) => {
+ this.parse(uri, file.content).catch(console.error);
+ });
+ });
+ }
+
+ private parseSavedDocuments() {
+ const { files } = filesState.getState();
+ return Promise.all(
+ Object.entries(files).map(([uri, file]) => {
+ return this.parse(uri, file.content);
+ }),
+ );
+ }
+}
diff --git a/apps/studio-next/src/services/server-api.service.ts b/apps/studio-next/src/services/server-api.service.ts
new file mode 100644
index 000000000..ce7c68094
--- /dev/null
+++ b/apps/studio-next/src/services/server-api.service.ts
@@ -0,0 +1,43 @@
+import { AbstractService } from './abstract.service';
+
+import fileDownload from 'js-file-download';
+
+export interface ServerAPIProblem {
+ type: string;
+ title: string;
+ status: number;
+ detail?: string;
+ instance?: string;
+ [key: string]: any;
+}
+
+export class ServerAPIService extends AbstractService {
+ private serverPath = 'https://api.asyncapi.com/v1';
+
+ async generate(data: {
+ asyncapi: string | Record,
+ template: string,
+ parameters: Record,
+ }): Promise {
+ const response = await fetch(`${this.serverPath}/generate`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(data),
+ });
+
+ if (response.ok) {
+ const zipFile = await response.blob();
+ fileDownload(zipFile, 'asyncapi.zip');
+ }
+
+ return response;
+ }
+
+ async retrieveProblem = Record>(response: Response): Promise {
+ if (response.ok || response.status < 400) return null;
+ const responseBody = JSON.parse(await response.text());
+ return responseBody as ServerAPIProblem & AP;
+ }
+}
diff --git a/apps/studio-next/src/services/settings.service.ts b/apps/studio-next/src/services/settings.service.ts
new file mode 100644
index 000000000..449e36b6f
--- /dev/null
+++ b/apps/studio-next/src/services/settings.service.ts
@@ -0,0 +1,20 @@
+import { AbstractService } from './abstract.service';
+
+import { isDeepEqual } from '@/helpers';
+import { settingsState } from '@/state';
+
+import type { SettingsState } from '@/state/settings.state';
+
+export class SettingsService extends AbstractService {
+ get(): SettingsState {
+ return settingsState.getState();
+ }
+
+ set(state: Partial) {
+ settingsState.setState(state);
+ }
+
+ isEqual(newState: Partial): boolean {
+ return isDeepEqual(this.get(), newState);
+ }
+}
diff --git a/apps/studio-next/src/services/socket-client.service.tsx b/apps/studio-next/src/services/socket-client.service.tsx
new file mode 100644
index 000000000..d164c1111
--- /dev/null
+++ b/apps/studio-next/src/services/socket-client.service.tsx
@@ -0,0 +1,92 @@
+import { AbstractService } from './abstract.service';
+
+import toast from 'react-hot-toast';
+
+import { appState } from '@/state';
+
+interface IncomingMessage {
+ type: 'file:loaded' | 'file:changed' | 'file:deleted';
+ code?: string;
+}
+
+export class SocketClient extends AbstractService {
+ private ws!: WebSocket;
+
+ public override onInit(): void {
+ const { url, base64, readOnly, liveServer } = this.svcs.navigationSvc.getUrlParameters();
+
+ const shouldConnect = !(base64 || url || readOnly);
+ if (!shouldConnect) {
+ return;
+ }
+
+ const liveServerPort = liveServer && Number(liveServer);
+ if (typeof liveServerPort === 'number') {
+ this.connect(window.location.hostname, liveServerPort);
+ }
+ }
+
+ connect(hostname: string, port: string | number) {
+ try {
+ const ws = this.ws = new WebSocket(`ws://${hostname || 'localhost'}:${port}/live-server`);
+
+ ws.onopen = this.onOpen.bind(this);
+ ws.onmessage = this.onMessage.bind(this);
+ ws.onerror = this.onError.bind(this);
+ } catch (e) {
+ console.error(e);
+ this.onError();
+ }
+ }
+
+ send(eventName: string, content: Record) {
+ this.ws && this.ws.send(JSON.stringify({ type: eventName, ...content }));
+ }
+
+ private onMessage(event: MessageEvent) {
+ try {
+ const json: IncomingMessage = JSON.parse(event.data);
+
+ switch (json.type) {
+ case 'file:loaded':
+ case 'file:changed':
+ this.svcs.editorSvc.updateState({
+ content: json.code as string,
+ updateModel: true,
+ sendToServer: false,
+ });
+ break;
+ case 'file:deleted':
+ console.warn('Live Server: The file has been deleted on the file system.');
+ break;
+ default:
+ console.warn('Live Server: An unknown even has been received. See details:');
+ console.log(json);
+ }
+ } catch (e) {
+ console.error(`Live Server: An invalid event has been received. See details:\n${event.data}`);
+ }
+ }
+
+ private onOpen() {
+ toast.success(
+
+
+ Correctly connected to the live server!
+
+
+ );
+ appState.setState({ liveServer: true });
+ }
+
+ private onError() {
+ toast.error(
+
+
+ Failed to connect to live server. Please check developer console for more information.
+
+
+ );
+ appState.setState({ liveServer: false });
+ }
+}
diff --git a/apps/studio-next/src/services/specification.service.ts b/apps/studio-next/src/services/specification.service.ts
new file mode 100644
index 000000000..84346f539
--- /dev/null
+++ b/apps/studio-next/src/services/specification.service.ts
@@ -0,0 +1,75 @@
+import { AbstractService } from './abstract.service';
+
+import specs from '@asyncapi/specs';
+import { show } from '@ebay/nice-modal-react';
+
+import { ConvertToLatestModal } from '../components/Modals';
+
+import { documentsState, settingsState } from '@/state';
+
+import type { SpecVersions } from '../types';
+
+export class SpecificationService extends AbstractService {
+ private keySessionStorage = 'informed-about-latest';
+ override onInit() {
+ this.subcribeToDocuments();
+ this.subscribeToSettings();
+ }
+
+ get specs() {
+ return specs.schemas;
+ }
+
+ get latestVersion(): SpecVersions {
+ return Object.keys(this.specs).pop() as SpecVersions;
+ }
+
+ getSpec(version: SpecVersions) {
+ return this.specs[String(version) as SpecVersions];
+ }
+
+ private subcribeToDocuments() {
+ documentsState.subscribe((state, prevState) => {
+ const newDocuments = state.documents;
+ const oldDocuments = prevState.documents;
+
+ Object.entries(newDocuments).forEach(([uri, document]) => {
+ const oldDocument = oldDocuments[String(uri)];
+ if (document === oldDocument) return;
+ const version = document.document?.version();
+ if (version && this.tryInformAboutLatestVersion(version)) {
+ show(ConvertToLatestModal);
+ }
+ });
+ });
+ }
+
+ private subscribeToSettings() {
+ settingsState.subscribe(() => {
+ sessionStorage.removeItem(this.keySessionStorage);
+ });
+ }
+
+ private tryInformAboutLatestVersion(
+ version: string,
+ ): boolean {
+ const oneDay = 24 * 60 * 60 * 1000; /* ms */
+
+ const nowDate = new Date();
+ let dateOfLastQuestion = nowDate;
+ const localStorageItem = sessionStorage.getItem(this.keySessionStorage);
+ if (localStorageItem) {
+ dateOfLastQuestion = new Date(localStorageItem);
+ }
+
+ const isOvertime =
+ nowDate === dateOfLastQuestion ||
+ nowDate.getTime() - dateOfLastQuestion.getTime() > oneDay;
+ if (isOvertime && version !== this.latestVersion) {
+ sessionStorage.setItem(this.keySessionStorage, nowDate.toString());
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/apps/studio-next/src/services/tests/converter.service.test.ts b/apps/studio-next/src/services/tests/converter.service.test.ts
new file mode 100644
index 000000000..808054ecd
--- /dev/null
+++ b/apps/studio-next/src/services/tests/converter.service.test.ts
@@ -0,0 +1,32 @@
+import { createServices } from '../';
+
+import type { ConverterService } from '../converter.service';
+
+describe('SpecificationService', () => {
+ let converterSvc: ConverterService;
+
+ beforeAll(async () => {
+ const services = await createServices();
+ converterSvc = services.converterSvc;
+ });
+
+ describe('.convertSpec', () => {
+ test('should convert spec to the given (yaml case)', async () => {
+ const result = await converterSvc.convert('asyncapi: 2.0.0', '2.1.0');
+ expect(result).toEqual('asyncapi: 2.1.0\n');
+ });
+
+ test('should convert spec to the given (json case)', async () => {
+ const result = await converterSvc.convert('{"asyncapi": "2.0.0"}', '2.1.0');
+ expect(result).toEqual(JSON.stringify({ asyncapi: '2.1.0' }, undefined, 2));
+ });
+
+ test('should throw error if converter cannot convert spec - case with invalid version', async () => {
+ try {
+ await converterSvc.convert('asyncapi: 1.3.0', '2.1.0');
+ } catch (e: any) {
+ expect(e.message).toEqual('Cannot convert from 1.3.0 to 2.1.0.');
+ }
+ });
+ });
+});
\ No newline at end of file
diff --git a/apps/studio-next/src/services/tests/editor.service.test.ts b/apps/studio-next/src/services/tests/editor.service.test.ts
new file mode 100644
index 000000000..58aa1d4aa
--- /dev/null
+++ b/apps/studio-next/src/services/tests/editor.service.test.ts
@@ -0,0 +1,152 @@
+import * as monacoAPI from 'monaco-editor/esm/vs/editor/editor.api';
+import { DiagnosticSeverity } from '@asyncapi/parser';
+
+import { createServices } from '../';
+
+import type { EditorService } from '../editor.service';
+import type { Diagnostic } from '@asyncapi/parser';
+
+describe('EditorService', () => {
+ let editorSvc: EditorService;
+
+ beforeAll(async () => {
+ const services = await createServices();
+ editorSvc = services.editorSvc;
+ });
+
+ describe('.createMarkers', () => {
+ test('should create markers with errors', () => {
+ const errors: Diagnostic[] = [
+ {
+ message: 'some error 1',
+ range: {
+ start: {
+ line: 2,
+ character: 4,
+ },
+ end: {
+ line: 9,
+ character: 14,
+ }
+ },
+ path: ['/'],
+ code: '-',
+ severity: DiagnosticSeverity.Error,
+ },
+ {
+ message: 'some error 2',
+ range: {
+ start: {
+ line: 0,
+ character: 1,
+ },
+ end: {
+ line: 1,
+ character: 2,
+ }
+ },
+ path: ['/'],
+ code: '-',
+ severity: DiagnosticSeverity.Error,
+ }
+ ];
+
+ const { markers, decorations } = editorSvc.createMarkersAndDecorations(errors);
+
+ // markers
+ expect(markers).toHaveLength(2);
+ expect(markers[0]).toEqual({
+ endColumn: 15,
+ endLineNumber: 10,
+ startColumn: 5,
+ startLineNumber: 3,
+ message: 'some error 1',
+ severity: monacoAPI.MarkerSeverity.Error
+ });
+ expect(markers[1]).toEqual({
+ endColumn: 3,
+ endLineNumber: 2,
+ startColumn: 2,
+ startLineNumber: 1,
+ message: 'some error 2',
+ severity: monacoAPI.MarkerSeverity.Error
+ });
+ // decorations
+ expect(decorations).toHaveLength(0);
+ });
+
+ test('should create decorators with warnings', () => {
+ const errors: Diagnostic[] = [
+ {
+ message: 'some warning 1',
+ range: {
+ start: {
+ line: 2,
+ character: 4,
+ },
+ end: {
+ line: 9,
+ character: 14,
+ }
+ },
+ path: ['/'],
+ code: '-',
+ severity: DiagnosticSeverity.Warning,
+ },
+ {
+ message: 'some warning 2',
+ range: {
+ start: {
+ line: 0,
+ character: 1,
+ },
+ end: {
+ line: 1,
+ character: 2,
+ }
+ },
+ path: ['/'],
+ code: '-',
+ severity: DiagnosticSeverity.Warning,
+ }
+ ];
+
+ const { markers, decorations } = editorSvc.createMarkersAndDecorations(errors);
+
+ // markers
+ expect(markers).toHaveLength(0);
+ // decorations
+ expect(decorations).toHaveLength(2);
+ expect(decorations[0]).toEqual({
+ id: 'asyncapi',
+ options: {
+ glyphMarginClassName: 'diagnostic-warning',
+ glyphMarginHoverMessage: {
+ value: 'some warning 1',
+ },
+ },
+ ownerId: 0,
+ range: new monacoAPI.Range(3, 5, 10, 15),
+ });
+ expect(decorations[1]).toEqual({
+ id: 'asyncapi',
+ options: {
+ glyphMarginClassName: 'diagnostic-warning',
+ glyphMarginHoverMessage: {
+ value: 'some warning 2',
+ },
+ },
+ ownerId: 0,
+ range: new monacoAPI.Range(1, 2, 2, 3),
+ });
+ });
+
+ test('should not create markers and decorators without errors', () => {
+ const errors: any[] = [];
+
+ const { markers, decorations } = editorSvc.createMarkersAndDecorations(errors);
+ expect(markers.length).toEqual(0);
+ expect(decorations.length).toEqual(0);
+ });
+ });
+});
diff --git a/apps/studio-next/src/services/tests/format.service.test.ts b/apps/studio-next/src/services/tests/format.service.test.ts
new file mode 100644
index 000000000..c6a6fb861
--- /dev/null
+++ b/apps/studio-next/src/services/tests/format.service.test.ts
@@ -0,0 +1,72 @@
+import { createServices } from '../';
+
+import type { FormatService } from '../format.service';
+
+describe('FormatService', () => {
+ let formatSvc: FormatService;
+
+ beforeAll(async () => {
+ const services = await createServices();
+ formatSvc = services.formatSvc;
+ });
+
+ describe('.convertToYaml', () => {
+ const validYAML = 'asyncapi: 2.2.0\nfoobar: barfoo\n';
+
+ test('should work with valid yaml', () => {
+ const result = formatSvc.convertToYaml(validYAML);
+ expect(result).toEqual(validYAML);
+ });
+
+ test('should work with valid stringified JSON', () => {
+ const json = '{"asyncapi": "2.2.0", "foobar": "barfoo"}';
+ const result = formatSvc.convertToYaml(json);
+ expect(result).toEqual(validYAML);
+ });
+ });
+
+ describe('.convertToJson', () => {
+ const validJSON = JSON.stringify({ asyncapi: '2.2.0', foobar: 'barfoo' }, undefined, 2);
+
+ test('should work with valid yaml', () => {
+ const result = formatSvc.convertToJSON('asyncapi: 2.2.0\nfoobar: barfoo\n');
+ expect(result).toEqual(validJSON);
+ });
+
+ test('should work with valid stringified JSON', () => {
+ const result = formatSvc.convertToJSON(validJSON);
+ expect(result).toEqual(validJSON);
+ });
+ });
+
+ describe('.encodeBase64', () => {
+ test('should properly encode content to base64', () => {
+ const result = formatSvc.encodeBase64('hello world!');
+ expect(result).toEqual('aGVsbG8gd29ybGQh');
+ });
+ });
+
+ describe('.decodeBase64', () => {
+ test('should properly decode content from base64', () => {
+ const result = formatSvc.decodeBase64('aGVsbG8gd29ybGQh');
+ expect(result).toEqual('hello world!');
+ });
+ });
+
+ describe('.retrieveLangauge', () => {
+ test('should check that content is yaml', () => {
+ const result = formatSvc.retrieveLangauge('asyncapi: 2.2.0\nfoobar: barfoo\n');
+ expect(result).toEqual('yaml');
+ });
+
+ test('should check that content is json', () => {
+ const result = formatSvc.retrieveLangauge('{"asyncapi": "2.2.0", "foobar": "barfoo"}');
+ expect(result).toEqual('json');
+ });
+
+ test('should check that content is yaml - fallback for non json content', () => {
+ const result = formatSvc.retrieveLangauge('');
+ expect(result).toEqual('yaml');
+ });
+ });
+});
\ No newline at end of file
diff --git a/apps/studio-next/src/services/tests/navigation.service.test.ts b/apps/studio-next/src/services/tests/navigation.service.test.ts
new file mode 100644
index 000000000..9078a211c
--- /dev/null
+++ b/apps/studio-next/src/services/tests/navigation.service.test.ts
@@ -0,0 +1,49 @@
+import { createServices } from '../';
+
+import type { NavigationService } from '../navigation.service';
+
+describe('NavigationService', () => {
+ let navigationSvc: NavigationService;
+
+ beforeAll(async () => {
+ const services = await createServices();
+ navigationSvc = services.navigationSvc;
+ });
+
+ function updateLocation(search: string) {
+ const location = {
+ ...window.location,
+ search,
+ };
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: location,
+ });
+ }
+
+ describe('.getUrlParameters() - checking readOnly parameter', () => {
+ test('should return false if reaOnly flag is not defined', () => {
+ updateLocation('?url=some-url.json');
+ const result = navigationSvc.getUrlParameters();
+ expect(result.readOnly).toEqual(false);
+ });
+
+ test('should return true if reaOnly flag is defined - empty value case', () => {
+ updateLocation('?readOnly');
+ const result = navigationSvc.getUrlParameters();
+ expect(result.readOnly).toEqual(true);
+ });
+
+ test('should return true if reaOnly flag is defined - true value case', () => {
+ updateLocation('?readOnly=true');
+ const result = navigationSvc.getUrlParameters();
+ expect(result.readOnly).toEqual(true);
+ });
+
+ test('should return false if reaOnly flag is not defined - non empty/true value case', () => {
+ updateLocation('?readOnly=false');
+ const result = navigationSvc.getUrlParameters();
+ expect(result.readOnly).toEqual(false);
+ });
+ });
+});
diff --git a/apps/studio-next/src/state/app.state.ts b/apps/studio-next/src/state/app.state.ts
new file mode 100644
index 000000000..8a062ec91
--- /dev/null
+++ b/apps/studio-next/src/state/app.state.ts
@@ -0,0 +1,17 @@
+import { create } from 'zustand';
+
+export type AppState = {
+ initialized: boolean;
+ readOnly: boolean;
+ liveServer: boolean;
+ initErrors: any[],
+}
+
+export const appState = create(() => ({
+ initialized: false,
+ readOnly: false,
+ liveServer: false,
+ initErrors: [],
+}));
+
+export const useAppState = appState;
diff --git a/apps/studio-next/src/state/documents.state.ts b/apps/studio-next/src/state/documents.state.ts
new file mode 100644
index 000000000..3dc90ca70
--- /dev/null
+++ b/apps/studio-next/src/state/documents.state.ts
@@ -0,0 +1,37 @@
+import { create } from 'zustand';
+
+import type { AsyncAPIDocumentInterface, Diagnostic, ParseOutput } from '@asyncapi/parser';
+
+export type DocumentDiagnostics = {
+ original: Diagnostic[];
+ filtered: Diagnostic[];
+ errors: Diagnostic[];
+ warnings: Diagnostic[];
+ informations: Diagnostic[];
+ hints: Diagnostic[];
+}
+
+export type Document = {
+ uri: string;
+ document?: AsyncAPIDocumentInterface;
+ extras?: ParseOutput['extras'];
+ diagnostics: DocumentDiagnostics;
+ valid?: boolean;
+}
+
+export type DocumentsState = {
+ documents: Record;
+}
+
+export type DocumentsActions = {
+ updateDocument: (uri: string, document: Partial) => void;
+}
+
+export const documentsState = create(set => ({
+ documents: {},
+ updateDocument(uri: string, document: Partial) {
+ set(state => ({ documents: { ...state.documents, [String(uri)]: { ...state.documents[String(uri)] || {}, ...document } } }));
+ },
+}));
+
+export const useDocumentsState = documentsState;
diff --git a/apps/studio-next/src/state/files.state.ts b/apps/studio-next/src/state/files.state.ts
index 4f69b43b3..42077f4e9 100644
--- a/apps/studio-next/src/state/files.state.ts
+++ b/apps/studio-next/src/state/files.state.ts
@@ -1,7 +1,8 @@
import { create } from 'zustand';
-const schema =`
-asyncapi: 3.0.0
+const document = typeof window !== 'undefined' ? localStorage.getItem('document') : undefined
+const schema =
+ document || `asyncapi: 3.0.0
info:
title: Streetlights Kafka API
version: 1.0.0
diff --git a/apps/studio-next/src/state/index.ts b/apps/studio-next/src/state/index.ts
new file mode 100644
index 000000000..84a191f99
--- /dev/null
+++ b/apps/studio-next/src/state/index.ts
@@ -0,0 +1,43 @@
+import { appState, useAppState } from './app.state';
+import { documentsState, useDocumentsState } from './documents.state';
+import { filesState, useFilesState } from './files.state';
+import { otherState, useOtherState } from './other.state';
+import { panelsState, usePanelsState } from './panels.state';
+import { settingsState, useSettingsState } from './settings.state';
+
+export {
+ appState, useAppState,
+ documentsState, useDocumentsState,
+ filesState, useFilesState,
+ otherState, useOtherState,
+ panelsState, usePanelsState,
+ settingsState, useSettingsState,
+};
+
+const state = {
+ // app
+ app: appState,
+ useAppState,
+
+ // documents
+ documents: documentsState,
+ useDocumentsState,
+
+ // file-system
+ files: filesState,
+ useFilesState,
+
+ // other
+ other: otherState,
+ useOtherState,
+
+ // panels
+ panels: panelsState,
+ usePanelsState,
+
+ // settings
+ settings: settingsState,
+ useSettingsState,
+};
+
+export default state;
\ No newline at end of file
diff --git a/apps/studio-next/src/state/other.state.ts b/apps/studio-next/src/state/other.state.ts
new file mode 100644
index 000000000..453485e61
--- /dev/null
+++ b/apps/studio-next/src/state/other.state.ts
@@ -0,0 +1,13 @@
+import { create } from 'zustand';
+
+export type OtherState = {
+ editorHeight: string;
+ templateRerender: boolean;
+}
+
+export const otherState = create(() => ({
+ editorHeight: 'calc(100% - 36px)',
+ templateRerender: false,
+}));
+
+export const useOtherState = otherState;
diff --git a/apps/studio-next/src/state/panels.state.ts b/apps/studio-next/src/state/panels.state.ts
new file mode 100644
index 000000000..d920f56a1
--- /dev/null
+++ b/apps/studio-next/src/state/panels.state.ts
@@ -0,0 +1,39 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+export type PanelsState = {
+ show: {
+ activityBar: boolean;
+ statusBar: boolean;
+ primarySidebar: boolean;
+ secondarySidebar: boolean;
+ primaryPanel: boolean;
+ secondaryPanel: boolean;
+ contextPanel: boolean;
+ };
+ // TODO: remove when panels tabs will be introduced
+ secondaryPanelType: 'template' | 'visualiser';
+}
+
+export const panelsState = create(
+ persist(() =>
+ ({
+ show: {
+ activityBar: true,
+ statusBar: true,
+ primarySidebar: true,
+ secondarySidebar: true,
+ primaryPanel: true,
+ secondaryPanel: true,
+ contextPanel: true,
+ },
+ secondaryPanelType: 'template',
+ }),
+ {
+ name: 'studio-panels',
+ getStorage: () => localStorage,
+ }
+ ),
+);
+
+export const usePanelsState = panelsState;
diff --git a/apps/studio-next/src/state/settings.state.ts b/apps/studio-next/src/state/settings.state.ts
new file mode 100644
index 000000000..1b4fbd10b
--- /dev/null
+++ b/apps/studio-next/src/state/settings.state.ts
@@ -0,0 +1,46 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+export type SettingsState = {
+ editor: {
+ autoSaving: boolean;
+ savingDelay: number;
+ };
+ governance: {
+ show: {
+ warnings: boolean;
+ informations: boolean;
+ hints: boolean;
+ }
+ };
+ templates: {
+ autoRendering: boolean;
+ };
+}
+
+export const settingsState = create(
+ persist(() =>
+ ({
+ editor: {
+ autoSaving: true,
+ savingDelay: 625,
+ },
+ governance: {
+ show: {
+ warnings: true,
+ informations: true,
+ hints: true,
+ },
+ },
+ templates: {
+ autoRendering: true,
+ },
+ }),
+ {
+ name: 'studio-settings',
+ getStorage: () => localStorage,
+ }
+ ),
+);
+
+export const useSettingsState = settingsState;
\ No newline at end of file
diff --git a/apps/studio-next/src/types.ts b/apps/studio-next/src/types.ts
new file mode 100644
index 000000000..221ecc3d5
--- /dev/null
+++ b/apps/studio-next/src/types.ts
@@ -0,0 +1,3 @@
+import type specs from '@asyncapi/specs';
+
+export type SpecVersions = keyof typeof specs.schemas;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2d741d867..8dce01fa8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -395,30 +395,48 @@ importers:
apps/studio-next:
dependencies:
- '@codemirror/commands':
- specifier: ^6.3.3
- version: 6.5.0
- '@codemirror/lang-json':
- specifier: ^6.0.1
- version: 6.0.1
- '@codemirror/lang-yaml':
- specifier: ^6.0.0
- version: 6.1.1(@codemirror/view@6.26.3)
- '@codemirror/language':
- specifier: ^6.10.1
- version: 6.10.1
- '@codemirror/state':
- specifier: ^6.4.1
- version: 6.4.1
- '@codemirror/theme-one-dark':
- specifier: ^6.1.2
- version: 6.1.2
+ '@asyncapi/avro-schema-parser':
+ specifier: ^3.0.19
+ version: 3.0.22
+ '@asyncapi/converter':
+ specifier: ^1.4.16
+ version: 1.4.19
+ '@asyncapi/openapi-schema-parser':
+ specifier: ^3.0.18
+ version: 3.0.22
+ '@asyncapi/parser':
+ specifier: ^3.0.11
+ version: 3.0.14
+ '@asyncapi/protobuf-schema-parser':
+ specifier: ^3.2.8
+ version: 3.2.12
+ '@asyncapi/react-component':
+ specifier: ^1.2.2
+ version: 1.4.10(react-dom@18.2.0)(react@18.2.0)
+ '@asyncapi/specs':
+ specifier: ^6.5.4
+ version: 6.7.1
'@codemirror/view':
specifier: ^6.26.3
version: 6.26.3
+ '@ebay/nice-modal-react':
+ specifier: ^1.2.10
+ version: 1.2.13(react-dom@18.2.0)(react@18.2.0)
+ '@headlessui/react':
+ specifier: ^1.7.4
+ version: 1.7.19(react-dom@18.2.0)(react@18.2.0)
+ '@hookstate/core':
+ specifier: ^4.0.0-rc21
+ version: 4.0.1(react@18.2.0)
+ '@monaco-editor/react':
+ specifier: ^4.4.6
+ version: 4.6.0(monaco-editor@0.34.1)(react-dom@18.2.0)(react@18.2.0)
'@stoplight/yaml':
specifier: ^4.3.0
version: 4.3.0
+ '@tippyjs/react':
+ specifier: ^4.2.6
+ version: 4.2.6(react-dom@18.2.0)(react@18.2.0)
'@types/node':
specifier: 20.4.6
version: 20.4.6
@@ -437,9 +455,24 @@ importers:
eslint-config-next:
specifier: 13.4.12
version: 13.4.12(eslint@8.57.0)(typescript@5.1.6)
+ js-base64:
+ specifier: ^3.7.3
+ version: 3.7.7
+ js-file-download:
+ specifier: ^0.4.12
+ version: 0.4.12
+ js-yaml:
+ specifier: ^4.1.0
+ version: 4.1.0
+ monaco-editor:
+ specifier: 0.34.1
+ version: 0.34.1
+ monaco-yaml:
+ specifier: 4.0.2
+ version: 4.0.2(monaco-editor@0.34.1)
next:
specifier: 14.2.3
- version: 14.2.3(react-dom@18.2.0)(react@18.2.0)
+ version: 14.2.3(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.2.0)
postcss:
specifier: 8.4.31
version: 8.4.31
@@ -449,6 +482,15 @@ importers:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
+ react-hot-toast:
+ specifier: 2.4.0
+ version: 2.4.0(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0)
+ react-icons:
+ specifier: ^4.6.0
+ version: 4.12.0(react@18.2.0)
+ reactflow:
+ specifier: ^11.2.0
+ version: 11.11.3(@types/react@18.2.18)(react-dom@18.2.0)(react@18.2.0)
tailwindcss:
specifier: 3.3.3
version: 3.3.3(ts-node@10.9.2)
@@ -461,6 +503,109 @@ importers:
zustand:
specifier: ^4.5.2
version: 4.5.2(@types/react@18.2.18)(react@18.2.0)
+ devDependencies:
+ '@asyncapi/dotnet-nats-template':
+ specifier: ^0.12.1
+ version: 0.12.1
+ '@asyncapi/go-watermill-template':
+ specifier: ^0.2.72
+ version: 0.2.75
+ '@asyncapi/html-template':
+ specifier: ^2.3.0
+ version: 2.3.5(react@18.2.0)
+ '@asyncapi/java-spring-cloud-stream-template':
+ specifier: ^0.13.4
+ version: 0.13.4
+ '@asyncapi/java-spring-template':
+ specifier: ^1.5.1
+ version: 1.5.1
+ '@asyncapi/java-template':
+ specifier: ^0.2.1
+ version: 0.2.10
+ '@asyncapi/markdown-template':
+ specifier: ^1.5.0
+ version: 1.6.1
+ '@asyncapi/nodejs-template':
+ specifier: ^2.0.1
+ version: 2.0.1(eslint@8.57.0)
+ '@asyncapi/nodejs-ws-template':
+ specifier: ^0.9.33
+ version: 0.9.35
+ '@asyncapi/python-paho-template':
+ specifier: ^0.2.13
+ version: 0.2.13
+ '@asyncapi/ts-nats-template':
+ specifier: ^0.10.3
+ version: 0.10.3
+ '@tailwindcss/typography':
+ specifier: ^0.5.8
+ version: 0.5.13(tailwindcss@3.3.3)
+ '@testing-library/jest-dom':
+ specifier: ^5.16.5
+ version: 5.17.0
+ '@testing-library/react':
+ specifier: ^13.4.0
+ version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
+ '@testing-library/user-event':
+ specifier: ^14.4.3
+ version: 14.5.2(@testing-library/dom@10.1.0)
+ '@types/jest':
+ specifier: ^29.2.3
+ version: 29.5.12
+ '@types/js-yaml':
+ specifier: ^4.0.5
+ version: 4.0.9
+ '@types/json-schema':
+ specifier: ^7.0.11
+ version: 7.0.15
+ assert:
+ specifier: ^2.0.0
+ version: 2.1.0
+ browserify-zlib:
+ specifier: ^0.2.0
+ version: 0.2.0
+ buffer:
+ specifier: ^6.0.3
+ version: 6.0.3
+ eslint-plugin-security:
+ specifier: ^1.5.0
+ version: 1.7.1
+ eslint-plugin-sonarjs:
+ specifier: ^0.16.0
+ version: 0.16.0(eslint@8.57.0)
+ https-browserify:
+ specifier: ^1.0.0
+ version: 1.0.0
+ markdown-toc:
+ specifier: ^1.2.0
+ version: 1.2.0
+ path-browserify:
+ specifier: ^1.0.1
+ version: 1.0.1
+ process:
+ specifier: ^0.11.10
+ version: 0.11.10
+ stream-browserify:
+ specifier: ^3.0.0
+ version: 3.0.0
+ stream-http:
+ specifier: ^3.2.0
+ version: 3.2.0
+ ts-node:
+ specifier: ^10.9.1
+ version: 10.9.2(@swc/core@1.5.7)(@types/node@20.4.6)(typescript@5.1.6)
+ url:
+ specifier: ^0.11.0
+ version: 0.11.3
+ util:
+ specifier: ^0.12.5
+ version: 0.12.5
+ web-vitals:
+ specifier: ^3.1.0
+ version: 3.5.2
+ webpack:
+ specifier: ^5.75.0
+ version: 5.91.0(@swc/core@1.5.7)
packages/eslint-config-custom:
dependencies:
@@ -842,6 +987,24 @@ packages:
- encoding
dev: true
+ /@asyncapi/nodejs-template@2.0.1(eslint@8.57.0):
+ resolution: {integrity: sha512-XKjHWo91aUrhAXN7yZGLFhTr46g0wTRa79i2MlTV57xVBsfYOxMjM4lvubaoJJQSDvmEaUlaKsK3HmeT1T4mBA==}
+ dependencies:
+ '@asyncapi/generator-filters': 2.1.0
+ '@asyncapi/generator-hooks': 0.1.0
+ '@asyncapi/generator-react-sdk': 1.0.18
+ eslint-plugin-react: 7.34.1(eslint@8.57.0)
+ filenamify: 4.3.0
+ js-beautify: 1.15.1
+ lodash: 4.17.21
+ markdown-toc: 1.2.0
+ transitivePeerDependencies:
+ - '@types/babel__core'
+ - encoding
+ - eslint
+ - supports-color
+ dev: true
+
/@asyncapi/nodejs-template@3.0.0(eslint@8.57.0):
resolution: {integrity: sha512-0CxOJ0sYxE3FCXQqyQM0rIqWy9DMCjMJcHwSR4eU8EXR8/TA8MxGarw5DETp4Rbkc/llPEgZOqfbjK+8njODxA==}
dependencies:
@@ -3786,26 +3949,6 @@ packages:
'@lezer/common': 1.2.1
dev: false
- /@codemirror/lang-json@6.0.1:
- resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==}
- dependencies:
- '@codemirror/language': 6.10.1
- '@lezer/json': 1.0.2
- dev: false
-
- /@codemirror/lang-yaml@6.1.1(@codemirror/view@6.26.3):
- resolution: {integrity: sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==}
- dependencies:
- '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1)
- '@codemirror/language': 6.10.1
- '@codemirror/state': 6.4.1
- '@lezer/common': 1.2.1
- '@lezer/highlight': 1.2.0
- '@lezer/yaml': 1.0.3
- transitivePeerDependencies:
- - '@codemirror/view'
- dev: false
-
/@codemirror/language@6.10.1:
resolution: {integrity: sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==}
dependencies:
@@ -3837,15 +3980,6 @@ packages:
resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==}
dev: false
- /@codemirror/theme-one-dark@6.1.2:
- resolution: {integrity: sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==}
- dependencies:
- '@codemirror/language': 6.10.1
- '@codemirror/state': 6.4.1
- '@codemirror/view': 6.26.3
- '@lezer/highlight': 1.2.0
- dev: false
-
/@codemirror/view@6.26.3:
resolution: {integrity: sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==}
dependencies:
@@ -5041,28 +5175,12 @@ packages:
'@lezer/common': 1.2.1
dev: false
- /@lezer/json@1.0.2:
- resolution: {integrity: sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==}
- dependencies:
- '@lezer/common': 1.2.1
- '@lezer/highlight': 1.2.0
- '@lezer/lr': 1.4.0
- dev: false
-
/@lezer/lr@1.4.0:
resolution: {integrity: sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==}
dependencies:
'@lezer/common': 1.2.1
dev: false
- /@lezer/yaml@1.0.3:
- resolution: {integrity: sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==}
- dependencies:
- '@lezer/common': 1.2.1
- '@lezer/highlight': 1.2.0
- '@lezer/lr': 1.4.0
- dev: false
-
/@manypkg/find-root@1.1.0:
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
dependencies:
@@ -8379,7 +8497,7 @@ packages:
/@types/graceful-fs@4.1.9:
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
dependencies:
- '@types/node': 20.4.6
+ '@types/node': 18.19.33
dev: true
/@types/html-minifier-terser@6.1.0:
@@ -12102,6 +12220,7 @@ packages:
- eslint-import-resolver-node
- eslint-import-resolver-webpack
- supports-color
+ dev: false
/eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
@@ -12131,6 +12250,36 @@ packages:
eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0)
transitivePeerDependencies:
- supports-color
+ dev: false
+
+ /eslint-module-utils@2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
+ resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: '*'
+ eslint-import-resolver-node: '*'
+ eslint-import-resolver-typescript: '*'
+ eslint-import-resolver-webpack: '*'
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+ eslint:
+ optional: true
+ eslint-import-resolver-node:
+ optional: true
+ eslint-import-resolver-typescript:
+ optional: true
+ eslint-import-resolver-webpack:
+ optional: true
+ dependencies:
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@4.9.5)
+ debug: 3.2.7
+ eslint: 8.57.0
+ eslint-import-resolver-node: 0.3.9
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
/eslint-module-utils@2.8.1(@typescript-eslint/parser@7.9.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0):
resolution: {integrity: sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==}
@@ -12185,7 +12334,7 @@ packages:
'@typescript-eslint/parser':
optional: true
dependencies:
- '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@5.1.6)
+ '@typescript-eslint/parser': 5.62.0(eslint@8.57.0)(typescript@4.9.5)
array-includes: 3.1.8
array.prototype.findlastindex: 1.2.5
array.prototype.flat: 1.3.2
@@ -12194,7 +12343,7 @@ packages:
doctrine: 2.1.0
eslint: 8.57.0
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)
+ eslint-module-utils: 2.8.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint@8.57.0)
hasown: 2.0.2
is-core-module: 2.13.1
is-glob: 4.0.3
@@ -12378,6 +12527,12 @@ packages:
semver: 6.3.1
string.prototype.matchall: 4.0.11
+ /eslint-plugin-security@1.7.1:
+ resolution: {integrity: sha512-sMStceig8AFglhhT2LqlU5r+/fn9OwsA72O5bBuQVTssPCdQAOQzL+oMn/ZcpeUY6KcNfLJArgcrsSULNjYYdQ==}
+ dependencies:
+ safe-regex: 2.1.1
+ dev: true
+
/eslint-plugin-security@3.0.0:
resolution: {integrity: sha512-2Ij7PkmXIF2cKwoVkEgemwoXbOnxg5UfdhdcpNxZwJxC/10dbsdhHISrTyJ/n8DUkt3yiN6P1ywEgcMGjIwHIw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -12385,6 +12540,15 @@ packages:
safe-regex: 2.1.1
dev: true
+ /eslint-plugin-sonarjs@0.16.0(eslint@8.57.0):
+ resolution: {integrity: sha512-al8ojAzcQW8Eu0tWn841ldhPpPcjrJ59TzzTfAVWR45bWvdAASCmrGl8vK0MWHyKVDdC0i17IGbtQQ1KgxLlVA==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ eslint: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
+ dependencies:
+ eslint: 8.57.0
+ dev: true
+
/eslint-plugin-sonarjs@0.25.1(eslint@8.57.0):
resolution: {integrity: sha512-5IOKvj/GMBNqjxBdItfotfRHo7w48496GOu1hxdeXuD0mB1JBlDCViiLHETDTfA8pDAVSBimBEQoetRXYceQEw==}
engines: {node: '>=16'}
@@ -13315,6 +13479,7 @@ packages:
resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==}
dependencies:
resolve-pkg-maps: 1.0.0
+ dev: false
/giget@1.2.3:
resolution: {integrity: sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==}
@@ -14902,7 +15067,7 @@ packages:
resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
- '@types/node': 20.4.6
+ '@types/node': 18.19.33
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -16274,7 +16439,7 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
- /next@14.2.3(react-dom@18.2.0)(react@18.2.0):
+ /next@14.2.3(@babel/core@7.24.5)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==}
engines: {node: '>=18.17.0'}
hasBin: true
@@ -16300,7 +16465,7 @@ packages:
postcss: 8.4.31
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
- styled-jsx: 5.1.1(react@18.2.0)
+ styled-jsx: 5.1.1(@babel/core@7.24.5)(react@18.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 14.2.3
'@next/swc-darwin-x64': 14.2.3
@@ -17247,7 +17412,7 @@ packages:
dependencies:
lilconfig: 3.1.1
postcss: 8.4.31
- ts-node: 10.9.2(@swc/core@1.5.7)(@types/node@18.19.33)(typescript@4.9.5)
+ ts-node: 10.9.2(@swc/core@1.5.7)(@types/node@20.4.6)(typescript@5.1.6)
yaml: 2.4.2
/postcss-loader@6.2.1(postcss@8.4.31)(webpack@5.91.0):
@@ -18826,6 +18991,7 @@ packages:
/resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+ dev: false
/resolve-url-loader@4.0.0:
resolution: {integrity: sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==}
@@ -19709,7 +19875,7 @@ packages:
resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==}
dev: false
- /styled-jsx@5.1.1(react@18.2.0):
+ /styled-jsx@5.1.1(@babel/core@7.24.5)(react@18.2.0):
resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
engines: {node: '>= 12.0.0'}
peerDependencies:
@@ -19722,6 +19888,7 @@ packages:
babel-plugin-macros:
optional: true
dependencies:
+ '@babel/core': 7.24.5
client-only: 0.0.1
react: 18.2.0
dev: false
@@ -20026,6 +20193,31 @@ packages:
webpack: 5.91.0(@swc/core@1.5.7)(esbuild@0.18.20)
dev: true
+ /terser-webpack-plugin@5.3.10(@swc/core@1.5.7)(webpack@5.91.0):
+ resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==}
+ engines: {node: '>= 10.13.0'}
+ peerDependencies:
+ '@swc/core': '*'
+ esbuild: '*'
+ uglify-js: '*'
+ webpack: ^5.1.0
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ esbuild:
+ optional: true
+ uglify-js:
+ optional: true
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.25
+ '@swc/core': 1.5.7
+ jest-worker: 27.5.1
+ schema-utils: 3.3.0
+ serialize-javascript: 6.0.2
+ terser: 5.31.0
+ webpack: 5.91.0(@swc/core@1.5.7)
+ dev: true
+
/terser@5.31.0:
resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==}
engines: {node: '>=10'}
@@ -20286,6 +20478,38 @@ packages:
typescript: 4.9.5
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
+ dev: true
+
+ /ts-node@10.9.2(@swc/core@1.5.7)(@types/node@20.4.6)(typescript@5.1.6):
+ resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
+ hasBin: true
+ peerDependencies:
+ '@swc/core': '>=1.2.50'
+ '@swc/wasm': '>=1.2.50'
+ '@types/node': '*'
+ typescript: '>=2.7'
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ '@swc/wasm':
+ optional: true
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@swc/core': 1.5.7
+ '@tsconfig/node10': 1.0.11
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 20.4.6
+ acorn: 8.11.3
+ acorn-walk: 8.3.2
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.2
+ make-error: 1.3.6
+ typescript: 5.1.6
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
/ts-pnp@1.2.0(typescript@5.1.6):
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
@@ -21200,6 +21424,46 @@ packages:
resolution: {integrity: sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==}
dev: true
+ /webpack@5.91.0(@swc/core@1.5.7):
+ resolution: {integrity: sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+ peerDependencies:
+ webpack-cli: '*'
+ peerDependenciesMeta:
+ webpack-cli:
+ optional: true
+ dependencies:
+ '@types/eslint-scope': 3.7.7
+ '@types/estree': 1.0.5
+ '@webassemblyjs/ast': 1.12.1
+ '@webassemblyjs/wasm-edit': 1.12.1
+ '@webassemblyjs/wasm-parser': 1.12.1
+ acorn: 8.11.3
+ acorn-import-assertions: 1.9.0(acorn@8.11.3)
+ browserslist: 4.23.0
+ chrome-trace-event: 1.0.3
+ enhanced-resolve: 5.16.1
+ es-module-lexer: 1.5.2
+ eslint-scope: 5.1.1
+ events: 3.3.0
+ glob-to-regexp: 0.4.1
+ graceful-fs: 4.2.11
+ json-parse-even-better-errors: 2.3.1
+ loader-runner: 4.3.0
+ mime-types: 2.1.35
+ neo-async: 2.6.2
+ schema-utils: 3.3.0
+ tapable: 2.2.1
+ terser-webpack-plugin: 5.3.10(@swc/core@1.5.7)(webpack@5.91.0)
+ watchpack: 2.4.1
+ webpack-sources: 3.2.3
+ transitivePeerDependencies:
+ - '@swc/core'
+ - esbuild
+ - uglify-js
+ dev: true
+
/webpack@5.91.0(@swc/core@1.5.7)(esbuild@0.18.20):
resolution: {integrity: sha512-rzVwlLeBWHJbmgTC/8TvAcu5vpJNII+MelQpylD4jNERPwpBJOE2lEcko1zJX3QJeLjTTAnQxn/OJ8bjDzVQaw==}
engines: {node: '>=10.13.0'}