diff --git a/frontend/console/src/components/CodeEditor.tsx b/frontend/console/src/components/CodeEditor.tsx index 9bb37f84db..7012e9590c 100644 --- a/frontend/console/src/components/CodeEditor.tsx +++ b/frontend/console/src/components/CodeEditor.tsx @@ -13,7 +13,7 @@ import { githubLight } from '@uiw/codemirror-theme-github' import { defaultKeymap, indentWithTab } from '@codemirror/commands' import { handleRefresh, jsonSchemaHover, jsonSchemaLinter, stateExtensions } from 'codemirror-json-schema' import { json5, json5ParseLinter } from 'codemirror-json5' -import { useCallback, useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import { useUserPreferences } from '../providers/user-preferences-provider' const commonExtensions = [ @@ -26,58 +26,54 @@ const commonExtensions = [ EditorState.tabSize.of(2), ] -export interface InitialState { - initialText: string - schema?: string +export const CodeEditor = ({ + value = '', + onTextChanged, + readonly = false, + schema, +}: { + value: string + onTextChanged?: (text: string) => void readonly?: boolean -} - -export const CodeEditor = ({ initialState, onTextChanged }: { initialState: InitialState; onTextChanged?: (text: string) => void }) => { + schema?: string +}) => { const { isDarkMode } = useUserPreferences() const editorContainerRef = useRef(null) const editorViewRef = useRef(null) - const handleEditorTextChange = useCallback( - (state: EditorState) => { - const currentText = state.doc.toString() - if (onTextChanged) { - onTextChanged(currentText) - } - }, - [onTextChanged], - ) - useEffect(() => { if (editorContainerRef.current) { - const sch = initialState.schema ? JSON.parse(initialState.schema) : null + const sch = schema ? JSON.parse(schema) : null - const editingExtensions: Extension[] = - initialState.readonly || false - ? [EditorState.readOnly.of(true)] - : [ - autocompletion(), - lineNumbers(), - lintGutter(), - indentOnInput(), - drawSelection(), - foldGutter(), - linter(json5ParseLinter(), { - delay: 300, - }), - linter(jsonSchemaLinter(), { - needsRefresh: handleRefresh, - }), - hoverTooltip(jsonSchemaHover()), - EditorView.updateListener.of((update) => { - if (update.docChanged) { - handleEditorTextChange(update.state) + const editingExtensions: Extension[] = readonly + ? [EditorState.readOnly.of(true)] + : [ + autocompletion(), + lineNumbers(), + lintGutter(), + indentOnInput(), + drawSelection(), + foldGutter(), + linter(json5ParseLinter(), { + delay: 300, + }), + linter(jsonSchemaLinter(), { + needsRefresh: handleRefresh, + }), + hoverTooltip(jsonSchemaHover()), + EditorView.updateListener.of((update) => { + if (update.docChanged) { + const currentText = update.state.doc.toString() + if (onTextChanged) { + onTextChanged(currentText) } - }), - stateExtensions(sch), - ] + } + }), + stateExtensions(sch), + ] const state = EditorState.create({ - doc: initialState.initialText, + doc: value, extensions: [ ...commonExtensions, isDarkMode ? atomone : githubLight, @@ -100,7 +96,20 @@ export const CodeEditor = ({ initialState, onTextChanged }: { initialState: Init view.destroy() } } - }, [initialState, isDarkMode]) + }, [isDarkMode, readonly, schema]) + + useEffect(() => { + if (editorViewRef.current && value !== undefined) { + const currentText = editorViewRef.current.state.doc.toString() + if (currentText !== value) { + const { state } = editorViewRef.current + const transaction = state.update({ + changes: { from: 0, to: state.doc.length, insert: value }, + }) + editorViewRef.current.dispatch(transaction) + } + } + }, [value]) return
} diff --git a/frontend/console/src/components/ResizableVerticalPanels.tsx b/frontend/console/src/components/ResizableVerticalPanels.tsx index 7861378b05..acbb6c5d2c 100644 --- a/frontend/console/src/components/ResizableVerticalPanels.tsx +++ b/frontend/console/src/components/ResizableVerticalPanels.tsx @@ -20,6 +20,8 @@ export const ResizableVerticalPanels: React.FC = ( const [topPanelHeight, setTopPanelHeight] = useState() const [isDragging, setIsDragging] = useState(false) + const hasBottomPanel = !!bottomPanelContent + useEffect(() => { const updateDimensions = () => { if (containerRef.current) { @@ -32,9 +34,12 @@ export const ResizableVerticalPanels: React.FC = ( updateDimensions() window.addEventListener('resize', updateDimensions) return () => window.removeEventListener('resize', updateDimensions) - }, [initialTopPanelHeightPercent]) + }, [initialTopPanelHeightPercent, hasBottomPanel]) const startDragging = (e: React.MouseEvent) => { + if (!hasBottomPanel) { + return + } e.preventDefault() setIsDragging(true) } @@ -44,7 +49,7 @@ export const ResizableVerticalPanels: React.FC = ( } const onDrag = (e: React.MouseEvent) => { - if (!isDragging || !containerRef.current) { + if (!isDragging || !containerRef.current || !hasBottomPanel) { return } const containerDims = containerRef.current.getBoundingClientRect() @@ -57,15 +62,20 @@ export const ResizableVerticalPanels: React.FC = ( return (
-
+
+ {' '} {topPanelContent}
-
-
{bottomPanelContent}
+ {hasBottomPanel && ( + <> +
+
{bottomPanelContent}
+ + )}
) } diff --git a/frontend/console/src/features/verbs/VerbRequestForm.tsx b/frontend/console/src/features/verbs/VerbRequestForm.tsx index 3adcf224e9..6e6a2c8a28 100644 --- a/frontend/console/src/features/verbs/VerbRequestForm.tsx +++ b/frontend/console/src/features/verbs/VerbRequestForm.tsx @@ -1,6 +1,6 @@ import { Copy01Icon } from 'hugeicons-react' -import { useContext, useEffect, useState } from 'react' -import { CodeEditor, type InitialState } from '../../components/CodeEditor' +import { useCallback, useContext, useEffect, useState } from 'react' +import { CodeEditor } from '../../components/CodeEditor' import { ResizableVerticalPanels } from '../../components/ResizableVerticalPanels' import { useClient } from '../../hooks/use-client' import type { Module, Verb } from '../../protos/xyz/block/ftl/v1/console/console_pb' @@ -24,15 +24,13 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb const client = useClient(VerbService) const { showNotification } = useContext(NotificationsContext) const [activeTabId, setActiveTabId] = useState('body') - const [initialEditorState, setInitialEditorText] = useState({ initialText: '' }) - const [editorText, setEditorText] = useState('') - const [initialHeadersState, setInitialHeadersText] = useState({ initialText: '' }) + const [bodyText, setBodyText] = useState('') const [headersText, setHeadersText] = useState('') const [response, setResponse] = useState(null) const [error, setError] = useState(null) const [path, setPath] = useState('') - const editorTextKey = `${module?.name}-${verb?.verb?.name}-editor-text` + const bodyTextKey = `${module?.name}-${verb?.verb?.name}-body-text` const headersTextKey = `${module?.name}-${verb?.verb?.name}-headers-text` useEffect(() => { @@ -41,35 +39,22 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb useEffect(() => { if (verb) { - const savedEditorValue = localStorage.getItem(editorTextKey) - let editorValue: string - if (savedEditorValue != null && savedEditorValue !== '') { - editorValue = savedEditorValue - } else { - editorValue = defaultRequest(verb) - } - - const schemaString = JSON.stringify(simpleJsonSchema(verb)) - setInitialEditorText({ initialText: editorValue, schema: schemaString }) - localStorage.setItem(editorTextKey, editorValue) - handleEditorTextChanged(editorValue) + const savedBodyValue = localStorage.getItem(bodyTextKey) + const bodyValue = savedBodyValue ?? defaultRequest(verb) + setBodyText(bodyValue) const savedHeadersValue = localStorage.getItem(headersTextKey) - let headerValue: string - if (savedHeadersValue != null && savedHeadersValue !== '') { - headerValue = savedHeadersValue - } else { - headerValue = '{}' - } - setInitialHeadersText({ initialText: headerValue }) + const headerValue = savedHeadersValue ?? '{}' setHeadersText(headerValue) - localStorage.setItem(headersTextKey, headerValue) + + setResponse(null) + setError(null) } }, [verb, activeTabId]) - const handleEditorTextChanged = (text: string) => { - setEditorText(text) - localStorage.setItem(editorTextKey, text) + const handleBodyTextChanged = (text: string) => { + setBodyText(text) + localStorage.setItem(bodyTextKey, text) } const handleHeadersTextChanged = (text: string) => { @@ -99,7 +84,7 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb 'Content-Type': 'application/json', ...JSON.parse(headersText), }, - ...(method === 'POST' || method === 'PUT' ? { body: editorText } : {}), + ...(method === 'POST' || method === 'PUT' ? { body: bodyText } : {}), }) .then(async (response) => { if (response.ok) { @@ -121,7 +106,7 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb module: module?.name, } as Ref - const requestBytes = createCallRequest(path, verb, editorText, headersText) + const requestBytes = createCallRequest(path, verb, bodyText, headersText) client .call({ verb: verbRef, body: requestBytes }) .then((response) => { @@ -140,8 +125,8 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb } const handleSubmit = async (path: string) => { - setResponse(null) - setError(null) + setResponse('') + setError('') try { if (isHttpIngress(verb)) { @@ -160,7 +145,7 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb return } - const cliCommand = generateCliCommand(verb, path, headersText, editorText) + const cliCommand = generateCliCommand(verb, path, headersText, bodyText) navigator.clipboard .writeText(cliCommand) .then(() => { @@ -175,18 +160,15 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb }) } - const bottomText = response ?? error ?? '' + const handleResetBody = useCallback(() => { + if (verb) { + console.log('resetting body') + handleBodyTextChanged(defaultRequest(verb)) + } + }, [verb, bodyTextKey]) - const bodyEditor = - const bodyPanels = - bottomText === '' ? ( - bodyEditor - ) : ( - } - /> - ) + const bottomText = response ?? error ?? '' + const schemaString = verb ? JSON.stringify(simpleJsonSchema(verb)) : '' return (
@@ -232,10 +214,26 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb
- {activeTabId === 'body' && bodyPanels} - {activeTabId === 'verbschema' && } - {activeTabId === 'jsonschema' && } - {activeTabId === 'headers' && } + {activeTabId === 'body' && ( + + + +
+ } + bottomPanelContent={bottomText !== '' ? : null} + /> + )} + {activeTabId === 'verbschema' && } + {activeTabId === 'jsonschema' && } + {activeTabId === 'headers' && }
diff --git a/frontend/console/src/features/verbs/verb.utils.ts b/frontend/console/src/features/verbs/verb.utils.ts index 354df7ec13..5ad5521d30 100644 --- a/frontend/console/src/features/verbs/verb.utils.ts +++ b/frontend/console/src/features/verbs/verb.utils.ts @@ -68,9 +68,17 @@ export const defaultRequest = (verb?: Verb): string => { const schema = simpleJsonSchema(verb) JSONSchemaFaker.option({ - alwaysFakeOptionals: false, - useDefaultValue: true, - requiredOnly: true, + alwaysFakeOptionals: true, + optionalsProbability: 0, + useDefaultValue: false, + minItems: 0, + maxItems: 0, + minLength: 0, + maxLength: 0, + }) + + JSONSchemaFaker.format('date-time', () => { + return new Date().toISOString() }) try {