From 1c971ee8e8ad71f4024667c98c6aba47a4f73ee7 Mon Sep 17 00:00:00 2001 From: Edward Irby Date: Tue, 29 Aug 2023 17:39:28 -0700 Subject: [PATCH] feat(console): monocao editor + tab bug fix --- console/client/package-lock.json | 8 +- console/client/package.json | 3 +- .../client/src/features/verbs/VerbForm.tsx | 43 +++++---- console/client/src/layout/IDELayout.tsx | 94 ++++++++++--------- 4 files changed, 82 insertions(+), 66 deletions(-) diff --git a/console/client/package-lock.json b/console/client/package-lock.json index 77da5ae2b7..01d9ce0620 100644 --- a/console/client/package-lock.json +++ b/console/client/package-lock.json @@ -13,8 +13,9 @@ "@bufbuild/protobuf": "1.3.0", "@headlessui/react": "1.7.16", "@heroicons/react": "2.0.18", - "@monaco-editor/react": "^4.5.2", + "@monaco-editor/react": "4.5.2", "@tailwindcss/forms": "^0.5.5", + "json-schema": "0.4.0", "json-schema-faker": "0.5.0-rcv.46", "react": "18.2.0", "react-dom": "18.2.0", @@ -9386,6 +9387,11 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + }, "node_modules/json-schema-faker": { "version": "0.5.0-rcv.46", "resolved": "https://registry.npmjs.org/json-schema-faker/-/json-schema-faker-0.5.0-rcv.46.tgz", diff --git a/console/client/package.json b/console/client/package.json index 07b6488250..9398fd4c47 100644 --- a/console/client/package.json +++ b/console/client/package.json @@ -36,9 +36,10 @@ "@bufbuild/protobuf": "1.3.0", "@headlessui/react": "1.7.16", "@heroicons/react": "2.0.18", - "@monaco-editor/react": "^4.5.2", + "@monaco-editor/react": "4.5.2", "@tailwindcss/forms": "^0.5.5", "json-schema-faker": "0.5.0-rcv.46", + "json-schema": "0.4.0", "react": "18.2.0", "react-dom": "18.2.0", "react-router-dom": "6.15.0", diff --git a/console/client/src/features/verbs/VerbForm.tsx b/console/client/src/features/verbs/VerbForm.tsx index 8955598068..feb5a543db 100644 --- a/console/client/src/features/verbs/VerbForm.tsx +++ b/console/client/src/features/verbs/VerbForm.tsx @@ -1,14 +1,17 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -import {JSONSchemaFaker, Schema} from 'json-schema-faker' -import React, {useEffect} from 'react' +import {JSONSchemaFaker} from 'json-schema-faker' +import React from 'react' import {CodeBlock} from '../../components/CodeBlock' import {useClient} from '../../hooks/use-client' import {Module, Verb} from '../../protos/xyz/block/ftl/v1/console/console_pb' import {VerbService} from '../../protos/xyz/block/ftl/v1/ftl_connect' import {VerbRef} from '../../protos/xyz/block/ftl/v1/schema/schema_pb' -import Editor from '@monaco-editor/react' +import Editor, {Monaco} from '@monaco-editor/react' import {useDarkMode} from '../../providers/dark-mode-provider' +import type {JSONSchema4, JSONSchema6, JSONSchema7} from 'json-schema' + +export type Schema = JSONSchema4 | JSONSchema6 | JSONSchema7 type Props = { module?: Module @@ -21,20 +24,22 @@ export const VerbForm: React.FC = ({module, verb}) => { const [editorText, setEditorText] = React.useState('') const [response, setResponse] = React.useState(null) const [error, setError] = React.useState(null) - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [schema, setSchema] = React.useState() - useEffect(() => { + const [monaco, setMonaco] = React.useState() + + React.useEffect(() => { if (verb?.jsonRequestSchema) { JSONSchemaFaker.option('maxItems', 2) JSONSchemaFaker.option('alwaysFakeOptionals', true) - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const verbSchema = JSON.parse(verb.jsonRequestSchema) as Schema setSchema(verbSchema) - setEditorText(JSON.stringify(JSONSchemaFaker.generate(verbSchema), null, 2)) + setEditorText( + JSON.stringify(JSONSchemaFaker.generate(verbSchema), null, 2) + ) } }, []) - function handleEditorChange(value: string | undefined, _) { setEditorText(value ?? '') } @@ -69,18 +74,20 @@ export const VerbForm: React.FC = ({module, verb}) => { setError(String(error)) } } - const handleEditorWillMount = monaco => { - verb?.jsonRequestSchema && monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - validate: true, - schemas: [{ - uri: 'http://myserver/json-schema', - fileMatch: ['*'], - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - schema, - }], - }) + const handleEditorWillMount = (monaco: Monaco) => { + setMonaco(monaco) } + React.useEffect(() => { + schema && + monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + schemas: [ + {schema, uri: 'http://myserver/foo-schema.json', fileMatch: ['*']}, + ], + }) + }, [monaco, schema]) + return ( <>
() const id = searchParams.get(TabSearchParams.id) as string const type = searchParams.get(TabSearchParams.type) as string - // Set active tab index whenever our activeTab context changes - React.useEffect(() => { - const index = tabs.findIndex(tab => tab.id === activeTab?.id) - setActiveIndex(index) - }, [activeTab]) const handleCloseTab = (id: string, index: number) => { const nextActiveTab = { @@ -59,52 +53,60 @@ export function IDELayout() { }) } + // Handle loading a page with the tab query parameters React.useEffect(() => { - const validateTabs = async () => { - const modules = await client.getModules({}) - const msg = invalidTab({id, type}) - if (msg) { - // IDs an invalid tab ID and type fallback to timeline - setActiveTab({id: timelineTab.id, type: timelineTab.type}) - // On intial mount we have no query params set for tabs so we want to skip setting invalidTabMessage - if (type === null && id === null) return - return setInvalidTabMessage(msg) + const msg = invalidTab({id, type}) + if (msg) { + // Default fallback to timeline + setActiveTab({id: timelineTab.id, type: timelineTab.type}) + if (type === null && id === null) return + + return setInvalidTabMessage(msg) + } + if (modules.length) { + const ids = id.split('.') + // Handle edge case where the id contains and invalid module or verb + if (modules.length) { + const [moduleId, verbId] = ids + // Check to see if they exist on controller + const moduleExist = modules.find(module => module?.name === moduleId) + const verbExist = moduleExist?.verbs.some( + ({verb}) => verb?.name === verbId + ) + if (moduleExist && !verbExist) { + setInvalidTabMessage(`Verb ${verbId} does not exist on ${moduleId}`) + return setActiveTab({id: timelineTab.id, type: timelineTab.type}) + } + if (!moduleExist) { + setInvalidTabMessage(`Module ${moduleId} does not exist`) + return setActiveTab({id: timelineTab.id, type: timelineTab.type}) + } } - const inTabsList = tabs.some( - ({id: tabId, type: tabType}) => tabId === id && tabType === type - ) - // Tab is in tab list just set active tab - if (inTabsList) return setActiveTab({id, type}) - // Get module and Verb ids - const [moduleId, verbId] = id.split('.') - // Check to see if they exist on controller - const moduleExist = modules.modules.find( - module => module?.name === moduleId - ) - const verbExist = moduleExist?.verbs.some( - ({verb}) => verb?.name === verbId - ) - // Set tab if they both exists - if (moduleExist && verbExist) { + // Handle if tab is not already in tab list + if ( + !tabs.some( + ({id: tabId, type: tabType}) => tabId === id && tabType === type + ) + ) { const newTab = { - id: moduleId, - label: verbId, + id, + label: ids[1], type: TabType.Verb, } const nextTabs = [...tabs, newTab] - setActiveTab({id, type}) - return setTabs(nextTabs) + setTabs(nextTabs) + return setActiveTab({id: newTab.id, type: newTab.type}) } - if (moduleExist && !verbExist) { - setInvalidTabMessage(`Verb ${verbId} does not exist on ${moduleId}`) - } - if (!moduleExist) { - setInvalidTabMessage(`Module ${moduleId} does not exist`) - } - setActiveTab({id: timelineTab.id, type: timelineTab.type}) + // Handle if tab is in tab list + return setActiveTab({id, type}) } - void validateTabs() - }, [id, type]) + }, [id, type, modules]) + + // Set active tab index whenever our activeTab context changes + React.useEffect(() => { + const index = tabs.findIndex(tab => tab.id === activeTab?.id) + setActiveIndex(index) + }, [activeTab]) return ( <>