Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: verb for resetting after submit #2986

Merged
merged 1 commit into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 52 additions & 43 deletions frontend/console/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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<EditorView | null>(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,
Expand All @@ -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 <div className='h-full' ref={editorContainerRef} />
}
28 changes: 19 additions & 9 deletions frontend/console/src/components/ResizableVerticalPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const ResizableVerticalPanels: React.FC<ResizableVerticalPanelsProps> = (
const [topPanelHeight, setTopPanelHeight] = useState<number>()
const [isDragging, setIsDragging] = useState(false)

const hasBottomPanel = !!bottomPanelContent

useEffect(() => {
const updateDimensions = () => {
if (containerRef.current) {
Expand All @@ -32,9 +34,12 @@ export const ResizableVerticalPanels: React.FC<ResizableVerticalPanelsProps> = (
updateDimensions()
window.addEventListener('resize', updateDimensions)
return () => window.removeEventListener('resize', updateDimensions)
}, [initialTopPanelHeightPercent])
}, [initialTopPanelHeightPercent, hasBottomPanel])

const startDragging = (e: React.MouseEvent<HTMLDivElement>) => {
if (!hasBottomPanel) {
return
}
e.preventDefault()
setIsDragging(true)
}
Expand All @@ -44,7 +49,7 @@ export const ResizableVerticalPanels: React.FC<ResizableVerticalPanelsProps> = (
}

const onDrag = (e: React.MouseEvent<HTMLDivElement>) => {
if (!isDragging || !containerRef.current) {
if (!isDragging || !containerRef.current || !hasBottomPanel) {
return
}
const containerDims = containerRef.current.getBoundingClientRect()
Expand All @@ -57,15 +62,20 @@ export const ResizableVerticalPanels: React.FC<ResizableVerticalPanelsProps> = (

return (
<div ref={containerRef} className='flex flex-col h-full w-full' onMouseMove={onDrag} onMouseUp={stopDragging} onMouseLeave={stopDragging}>
<div style={{ height: `${topPanelHeight}px` }} className='overflow-auto'>
<div style={{ height: hasBottomPanel ? `${topPanelHeight}px` : '100%' }} className='overflow-auto'>
{' '}
{topPanelContent}
</div>
<div
className='cursor-row-resize bg-gray-200 dark:bg-gray-700 hover:bg-indigo-600'
onMouseDown={startDragging}
style={{ height: '3px', cursor: 'row-resize' }}
/>
<div className='flex-1 overflow-auto'>{bottomPanelContent}</div>
{hasBottomPanel && (
<>
<div
className='cursor-row-resize bg-gray-200 dark:bg-gray-700 hover:bg-indigo-600'
onMouseDown={startDragging}
style={{ height: '3px', cursor: 'row-resize' }}
/>
<div className='flex-1 overflow-auto'>{bottomPanelContent}</div>
</>
)}
</div>
)
}
95 changes: 46 additions & 49 deletions frontend/console/src/features/verbs/VerbRequestForm.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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<InitialState>({ initialText: '' })
const [editorText, setEditorText] = useState('')
const [initialHeadersState, setInitialHeadersText] = useState<InitialState>({ initialText: '' })
const [bodyText, setBodyText] = useState('')
const [headersText, setHeadersText] = useState('')
const [response, setResponse] = useState<string | null>(null)
const [error, setError] = useState<string | null>(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(() => {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) => {
Expand All @@ -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)) {
Expand All @@ -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(() => {
Expand All @@ -175,18 +160,14 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb
})
}

const bottomText = response ?? error ?? ''
const handleResetBody = useCallback(() => {
if (verb) {
handleBodyTextChanged(defaultRequest(verb))
}
}, [verb, bodyTextKey])

const bodyEditor = <CodeEditor initialState={initialEditorState} onTextChanged={handleEditorTextChanged} />
const bodyPanels =
bottomText === '' ? (
bodyEditor
) : (
<ResizableVerticalPanels
topPanelContent={bodyEditor}
bottomPanelContent={<CodeEditor initialState={{ initialText: bottomText, readonly: true }} onTextChanged={setHeadersText} />}
/>
)
const bottomText = response ?? error ?? ''
const schemaString = verb ? JSON.stringify(simpleJsonSchema(verb)) : ''

return (
<div className='flex flex-col h-full overflow-hidden pt-4'>
Expand Down Expand Up @@ -232,10 +213,26 @@ export const VerbRequestForm = ({ module, verb }: { module?: Module; verb?: Verb
</div>
<div className='flex-1 overflow-hidden'>
<div className='h-full overflow-y-scroll'>
{activeTabId === 'body' && bodyPanels}
{activeTabId === 'verbschema' && <CodeEditor initialState={{ initialText: verb?.schema ?? 'what', readonly: true }} />}
{activeTabId === 'jsonschema' && <CodeEditor initialState={{ initialText: verb?.jsonRequestSchema ?? '', readonly: true }} />}
{activeTabId === 'headers' && <CodeEditor initialState={initialHeadersState} onTextChanged={handleHeadersTextChanged} />}
{activeTabId === 'body' && (
<ResizableVerticalPanels
topPanelContent={
<div className='relative h-full'>
<button
type='button'
onClick={handleResetBody}
className='text-sm absolute top-2 right-2 z-10 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 py-1 px-2 rounded'
>
Reset
</button>
<CodeEditor value={bodyText} onTextChanged={handleBodyTextChanged} schema={schemaString} />
</div>
}
bottomPanelContent={bottomText !== '' ? <CodeEditor value={bottomText} readonly onTextChanged={setHeadersText} /> : null}
/>
)}
{activeTabId === 'verbschema' && <CodeEditor readonly value={verb?.schema ?? ''} />}
{activeTabId === 'jsonschema' && <CodeEditor readonly value={verb?.jsonRequestSchema ?? ''} />}
{activeTabId === 'headers' && <CodeEditor value={headersText} onTextChanged={handleHeadersTextChanged} />}
</div>
</div>
</div>
Expand Down
14 changes: 11 additions & 3 deletions frontend/console/src/features/verbs/verb.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading