diff --git a/.changeset/giant-bulldogs-pretend.md b/.changeset/giant-bulldogs-pretend.md new file mode 100644 index 000000000..8e67a90be --- /dev/null +++ b/.changeset/giant-bulldogs-pretend.md @@ -0,0 +1,5 @@ +--- +"studio-next": patch +--- + +Use Next.js framework diff --git a/.sonarcloud.properties b/.sonarcloud.properties index ad0891100..fe9a6793a 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,4 +1,4 @@ # Disable specific file since it would introduce more complexity to reduce it - mainly code complexity and complex template literals sonar.exclusions=apps/studio/public/js/monaco/**,apps/studio/src/tailwind.css,apps/studio/src/components/SplitPane/** # Disable duplicate code in tests since it would introduce more complexity to reduce it. -sonar.cpd.exclusions=apps/studio/** +sonar.cpd.exclusions=apps/studio/**,apps/studio-next/src/components/Navigationv3.tsx,apps/studio-next/src/components/Navigation.tsx diff --git a/apps/studio-next/next.config.js b/apps/studio-next/next.config.js index 767719fc4..ec3b149da 100644 --- a/apps/studio-next/next.config.js +++ b/apps/studio-next/next.config.js @@ -1,4 +1,41 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + webpack: ( + config, + { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } + ) => { + config.module.rules.push({ + test: /\.yml$/i, + type: 'asset/source', + }) + + if (isServer) return config + // Important: return the modified config + config.resolve.fallback = { + ...config.resolve.fallback, + assert: require.resolve('assert/'), + buffer: require.resolve('buffer'), + http: require.resolve('stream-http'), + https: require.resolve('https-browserify'), + path: require.resolve('path-browserify'), + stream: require.resolve('stream-browserify'), + zlib: require.resolve('browserify-zlib'), + url: require.resolve('url/'), + util: require.resolve('util/'), + debug: false, + canvas: false, + fs: false, + } + + config.plugins.push( + new webpack.ProvidePlugin({ + process: 'process/browser', + Buffer: ['buffer', 'Buffer'], + }) + ); + + return config + }, +} module.exports = nextConfig diff --git a/apps/studio-next/package.json b/apps/studio-next/package.json index 4a5301f99..b34f52e34 100644 --- a/apps/studio-next/package.json +++ b/apps/studio-next/package.json @@ -9,12 +9,26 @@ "lint": "echo 'No linting configured'" }, "dependencies": { - "@codemirror/state": "^6.4.1", - "@codemirror/commands": "^6.3.3", - "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-yaml": "^6.0.0", - "@codemirror/language": "^6.10.1", - "@codemirror/theme-one-dark": "^6.1.2", + "@asyncapi/avro-schema-parser": "^3.0.19", + "@asyncapi/converter": "^1.4.16", + "@asyncapi/openapi-schema-parser": "^3.0.18", + "@asyncapi/parser": "^3.0.11", + "@asyncapi/protobuf-schema-parser": "^3.2.8", + "@asyncapi/react-component": "^1.2.2", + "@asyncapi/specs": "^6.5.4", + "@ebay/nice-modal-react": "^1.2.10", + "@headlessui/react": "^1.7.4", + "@hookstate/core": "^4.0.0-rc21", + "@monaco-editor/react": "^4.4.6", + "@tippyjs/react": "^4.2.6", + "js-base64": "^3.7.3", + "js-file-download": "^0.4.12", + "js-yaml": "^4.1.0", + "monaco-editor": "0.34.1", + "monaco-yaml": "4.0.2", + "react-hot-toast": "2.4.0", + "react-icons": "^4.6.0", + "reactflow": "^11.2.0", "@stoplight/yaml": "^4.3.0", "@codemirror/view": "^6.26.3", "@types/node": "20.4.6", @@ -31,5 +45,48 @@ "tippy.js": "^6.3.7", "typescript": "5.1.6", "zustand": "^4.5.2" + }, + "devDependencies": { + "@asyncapi/dotnet-nats-template": "^0.12.1", + "@asyncapi/go-watermill-template": "^0.2.72", + "@asyncapi/html-template": "^2.3.0", + "@asyncapi/java-spring-cloud-stream-template": "^0.13.4", + "@asyncapi/java-spring-template": "^1.5.1", + "@asyncapi/java-template": "^0.2.1", + "@asyncapi/markdown-template": "^1.5.0", + "@asyncapi/nodejs-template": "^2.0.1", + "@asyncapi/nodejs-ws-template": "^0.9.33", + "@asyncapi/python-paho-template": "^0.2.13", + "@asyncapi/ts-nats-template": "^0.10.3", + "@tailwindcss/typography": "^0.5.8", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^13.4.0", + "@testing-library/user-event": "^14.4.3", + "@types/jest": "^29.2.3", + "@types/js-yaml": "^4.0.5", + "@types/json-schema": "^7.0.11", + "@types/node": "^18.11.9", + "@types/react": "^18.0.25", + "@types/react-dom": "^18.0.9", + "assert": "^2.0.0", + "autoprefixer": "^10.4.13", + "browserify-zlib": "^0.2.0", + "buffer": "^6.0.3", + "eslint-config-next": "14.1.4", + "eslint-plugin-security": "^1.5.0", + "eslint-plugin-sonarjs": "^0.16.0", + "https-browserify": "^1.0.0", + "markdown-toc": "^1.2.0", + "path-browserify": "^1.0.1", + "postcss": "^8.4.31", + "process": "^0.11.10", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "tailwindcss": "^3.2.4", + "ts-node": "^10.9.1", + "url": "^0.11.0", + "util": "^0.12.5", + "web-vitals": "^3.1.0", + "webpack": "^5.75.0" } } diff --git a/apps/studio-next/public/_redirects b/apps/studio-next/public/_redirects new file mode 100644 index 000000000..9392728f9 --- /dev/null +++ b/apps/studio-next/public/_redirects @@ -0,0 +1,19 @@ +# Redirect editor.asyncapi.org to Studio + +http://editor.asyncapi.org/* https://studio.asyncapi.com 301! +https://editor.asyncapi.org/* https://studio.asyncapi.com 301! + +# Redirect playground.asyncapi.io to Studio + +http://playground.asyncapi.io/* https://studio.asyncapi.com/?redirectedFrom=playground 301! +https://playground.asyncapi.io/* https://studio.asyncapi.com/?redirectedFrom=playground 301! + +http://playground.asyncapi.io/* load=:load https://studio.asyncapi.com/?redirectedFrom=playground&load=:load 301! +https://playground.asyncapi.io/* load=:load https://studio.asyncapi.com/?redirectedFrom=playground&load=:load 301! +http://playground.asyncapi.io/* load=:load readOnly=:readOnly https://studio.asyncapi.com/?redirectedFrom=playground&load=:load&readOnly=true 301! +https://playground.asyncapi.io/* load=:load readOnly=:readOnly https://studio.asyncapi.com/?redirectedFrom=playground&load=:load&readOnly=true 301! + +http://playground.asyncapi.io/* url=:url https://studio.asyncapi.com/?redirectedFrom=playground&url=:url 301! +https://playground.asyncapi.io/* url=:url https://studio.asyncapi.com/?redirectedFrom=playground&url=:url 301! +http://playground.asyncapi.io/* url=:url readOnly=:readOnly https://studio.asyncapi.com/?redirectedFrom=playground&url=:url&readOnly=true 301! +https://playground.asyncapi.io/* url=:url readOnly=:readOnly https://studio.asyncapi.com/?redirectedFrom=playground&url=:url&readOnly=true 301! diff --git a/apps/studio-next/public/favicon-16x16.png b/apps/studio-next/public/favicon-16x16.png new file mode 100644 index 000000000..f52b03ec9 Binary files /dev/null and b/apps/studio-next/public/favicon-16x16.png differ diff --git a/apps/studio-next/public/favicon-194x194.png b/apps/studio-next/public/favicon-194x194.png new file mode 100644 index 000000000..0d6bc7985 Binary files /dev/null and b/apps/studio-next/public/favicon-194x194.png differ diff --git a/apps/studio-next/public/favicon-32x32.png b/apps/studio-next/public/favicon-32x32.png new file mode 100644 index 000000000..a65f8093c Binary files /dev/null and b/apps/studio-next/public/favicon-32x32.png differ diff --git a/apps/studio-next/public/favicon.ico b/apps/studio-next/public/favicon.ico new file mode 100644 index 000000000..2597a4cf4 Binary files /dev/null and b/apps/studio-next/public/favicon.ico differ diff --git a/apps/studio-next/public/img/logo-studio.svg b/apps/studio-next/public/img/logo-studio.svg new file mode 100644 index 000000000..e663e2b0e --- /dev/null +++ b/apps/studio-next/public/img/logo-studio.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio-next/public/img/meta-studio-og-image.jpeg b/apps/studio-next/public/img/meta-studio-og-image.jpeg new file mode 100644 index 000000000..1a6b4b2de Binary files /dev/null and b/apps/studio-next/public/img/meta-studio-og-image.jpeg differ diff --git a/apps/studio-next/public/img/survey-illustration.svg b/apps/studio-next/public/img/survey-illustration.svg new file mode 100644 index 000000000..cd45a399d --- /dev/null +++ b/apps/studio-next/public/img/survey-illustration.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/studio-next/public/robots.txt b/apps/studio-next/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/apps/studio-next/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/studio-next/scripts/template-parameters.ts b/apps/studio-next/scripts/template-parameters.ts new file mode 100644 index 000000000..35a8db0a8 --- /dev/null +++ b/apps/studio-next/scripts/template-parameters.ts @@ -0,0 +1,116 @@ +/* eslint-disable security/detect-non-literal-fs-filename */ + +import fs from 'fs'; +import path from 'path'; +import { JSONSchema7 } from 'json-schema'; + +const DESTINATION_JSON = path.join(__dirname, '../src/components/Modals/Generator/template-parameters.json'); +const TEMPLATES: Record = { + '@asyncapi/dotnet-nats-template': '.NET Nats Project', + '@asyncapi/go-watermill-template': 'GO Lang Watermill Project', + '@asyncapi/html-template': 'HTML website', + '@asyncapi/java-spring-cloud-stream-template': 'Java Spring Cloud Stream Project', + '@asyncapi/java-spring-template': 'Java Spring Project', + '@asyncapi/java-template': 'Java Project', + '@asyncapi/markdown-template': 'Markdown Documentation', + '@asyncapi/nodejs-template': 'NodeJS Project', + '@asyncapi/nodejs-ws-template': 'NodeJS WebSocket Project', + '@asyncapi/python-paho-template': 'Python Paho Project', + '@asyncapi/ts-nats-template': 'Typescript Nats Project', +}; +const SUPPORTED_TEMPLATES = Object.keys(TEMPLATES); + +interface TemplateParameter { + description?: string; + default?: any; + required?: boolean; +} + +interface TemplateConfig { + parameters: Record; +} + +function serializeParam(configParam: TemplateParameter): JSONSchema7 { + const param: JSONSchema7 = { + description: configParam.description, + }; + + if (typeof configParam.default === 'boolean' || configParam.default === 'true' || configParam.default === 'false') { + param.type = 'boolean'; + if (typeof configParam.default === 'boolean') { + param.default = configParam.default; + } else if (configParam.default === 'true') { + param.default = true; + } else if (configParam.default === 'false') { + param.default = false; + } + } else if (typeof configParam.default === 'number') { + param.type = 'number'; + param.default = Number(configParam.default); + } else { + param.type = 'string'; + param.default = configParam.default; + } + + return param; +} + +function serializeTemplateParameters(config: TemplateConfig): JSONSchema7 | undefined { + if (!config || !config.parameters) { + return; + } + + const configParameters = config.parameters; + const parameters: Record = {}; + const required: string[] = []; + for (const parameter in configParameters) { + const configParam = configParameters[String(parameter)]; + + const param = serializeParam(configParam); + if (configParam.required) { + required.push(parameter); + } + + parameters[String(parameter)] = param; + } + + return { + $schema: 'http://json-schema.org/draft-07/schema#', + type: 'object', + properties: parameters, + required, + // don't allow non supported properties + additionalProperties: false, + } as JSONSchema7; +} + +async function main() { + const schemas: Record = {}; + for (let i = 0, l = SUPPORTED_TEMPLATES.length; i < l; i++) { + const templateName = SUPPORTED_TEMPLATES[Number(i)]; + + console.info(`[INFO]: Prepare parameters for ${templateName}.`); + + const pathToPackageJSON = path.join(__dirname, `../../../node_modules/${templateName}/package.json`); + const packageJSONContent = await fs.promises.readFile(pathToPackageJSON, 'utf-8'); + const packageJSON = JSON.parse(packageJSONContent); + const generatorConfig = packageJSON.generator; + + const schema = serializeTemplateParameters(generatorConfig); + if (schema) { + schemas[String(templateName)] = { + title: TEMPLATES[String(templateName)], + schema, + supportedProtocols: generatorConfig.supportedProtocols, + }; + } + } + + console.info(`[INFO]: Save template parameters schemas to ${DESTINATION_JSON}.`); + await fs.promises.writeFile(DESTINATION_JSON, JSON.stringify(schemas), 'utf-8'); +} +main(); diff --git a/apps/studio-next/src/app/globals.css b/apps/studio-next/src/app/globals.css index fd81e8858..7db48e56c 100644 --- a/apps/studio-next/src/app/globals.css +++ b/apps/studio-next/src/app/globals.css @@ -2,26 +2,119 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + +/** Preloader */ +#preloader { + position: fixed; + left: 0; + top: 0; + bottom: 0; + right: 0; + z-index: 10000; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + background: #1f2937; + color: #ffffff; + transition: .3s all ease; +} + +#preloader.loaded { + opacity: 0; + visibility: hidden; +} + +.rotating-wheel { + width: 32px; + height: 32px; + margin: 0 auto; + border: 3px solid #ec4899; + border-radius: 50%; + border-left-color: transparent; + border-bottom-color: transparent; + animation: rotating-spin .88s infinite linear; +} + +@keyframes rotating-spin { + 100% { + transform: rotate(360deg); + } +} + +/** Resizer */ +.Resizer { + background: #374251; + z-index: 1; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -moz-background-clip: padding; + -webkit-background-clip: padding; + background-clip: padding-box; +} + +.Resizer:hover { + -webkit-transition: all 2s ease; + transition: all 2s ease; +} + +.Resizer.horizontal { + height: 11px; + margin: -5px 0; + border-top: 5px solid rgba(255, 255, 255, 0); + border-bottom: 5px solid rgba(255, 255, 255, 0); + cursor: row-resize; + width: 100%; +} + +.Resizer.horizontal:hover { + border-top: 5px solid rgba(0, 0, 0, 0.5); + border-bottom: 5px solid rgba(0, 0, 0, 0.5); +} + +.Resizer.vertical { + width: 11px; + margin: 0 -5px; + border-left: 5px solid rgba(255, 255, 255, 0); + border-right: 5px solid rgba(255, 255, 255, 0); + cursor: col-resize; +} + +.Resizer.vertical:hover { + border-left: 5px solid rgba(0, 0, 0, 0.5); + border-right: 5px solid rgba(0, 0, 0, 0.5); +} + +.Resizer.disabled { + cursor: not-allowed; +} + +.Resizer.disabled:hover { + border-color: transparent; +} + +/** Monaco editor */ +.diagnostic-warning { + background: rgba(245, 158, 11); + width: 3px !important; +} + +.diagnostic-information { + background: rgba(59, 130, 246); + width: 3px !important; +} + +.diagnostic-hint { + background: rgba(16, 185, 129); + width: 3px !important; +} + +/** Tippy.js **/ +.tippy-box[data-placement^="bottom"] > .tippy-arrow:before { + border-bottom-color: rgba(17, 24, 39) !important; +} + +.tippy-box[data-placement^="right"] > .tippy-arrow:before { + border-right-color: rgba(17, 24, 39) !important; } diff --git a/apps/studio-next/src/app/layout.tsx b/apps/studio-next/src/app/layout.tsx index 90683c28d..4a998b426 100644 --- a/apps/studio-next/src/app/layout.tsx +++ b/apps/studio-next/src/app/layout.tsx @@ -1,14 +1,11 @@ /* eslint-disable no-undef */ -import './globals.css' -import type { Metadata } from 'next' -import { Inter } from 'next/font/google' - -const inter = Inter({ subsets: ['latin'] }) +import { Toolbar } from '@/components/Toolbar' -export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', -} +import 'tippy.js/dist/tippy.css'; +import 'tippy.js/animations/shift-away.css'; +import '@asyncapi/react-component/styles/default.min.css'; +import 'reactflow/dist/style.css'; +import './globals.css' export default function RootLayout({ children, @@ -17,7 +14,29 @@ export default function RootLayout({ }) { return ( - {children} + + +
+ {children} +
+
+
+
+ AsyncAPI Logo + + beta + +
+
+
+
+
+
+ ) } diff --git a/apps/studio-next/src/app/page.tsx b/apps/studio-next/src/app/page.tsx index 5e8dc6c85..e53256269 100644 --- a/apps/studio-next/src/app/page.tsx +++ b/apps/studio-next/src/app/page.tsx @@ -1,9 +1,7 @@ -import { Editor } from '@/components/Editor/Editor'; - -export default function Home() { +import dynamic from 'next/dynamic'; +const StudioWrapper = dynamic(() => import('@/components/StudioWrapper'), {ssr: false}) +export default async function Home() { return ( -
- -
+ ) } diff --git a/apps/studio-next/src/components/CodeEditor.tsx b/apps/studio-next/src/components/CodeEditor.tsx new file mode 100644 index 000000000..d188d52e0 --- /dev/null +++ b/apps/studio-next/src/components/CodeEditor.tsx @@ -0,0 +1,46 @@ +import React, { useEffect } from 'react'; +import toast, { Toaster } from 'react-hot-toast'; + +import { Content, Sidebar, Template } from '@/components'; + +import { afterAppInit, useServices } from '@/services'; +import { appState } from '@/state'; + +export interface AsyncAPIStudioProps {} + +export const AsyncAPIStudio: React.FunctionComponent< + AsyncAPIStudioProps +> = () => { + const services = useServices(); + + useEffect(() => { + setTimeout(() => { + afterAppInit(services).catch(console.error); + }, 250); + }, []); + + if (appState.getState().readOnly) { + return ( +
+