This is an automated email from the ASF dual-hosted git repository. young pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
The following commit(s) were added to refs/heads/master by this push: new d1f63e560 fix: support edit and show vars (#3152) d1f63e560 is described below commit d1f63e56046dbc6e2361c73baf48bc29507efea8 Author: YYYoung <isk...@outlook.com> AuthorDate: Mon Jul 28 15:07:48 2025 +0800 fix: support edit and show vars (#3152) --- e2e/tests/hot-path.upstream-service-route.spec.ts | 37 ++++--- e2e/tests/routes.crud-all-fields.spec.ts | 48 ++++++--- e2e/utils/ui/index.ts | 41 +++++-- .../FormItemPlugins/PluginEditorDrawer.tsx | 6 +- .../form-slice/FormItemPlugins/index.tsx | 2 +- src/components/form-slice/FormPartRoute/index.tsx | 3 +- src/components/form-slice/FormPartRoute/schema.ts | 13 ++- src/components/form-slice/FormPartRoute/util.ts | 19 +++- src/components/form/Editor.tsx | 119 +++++++++------------ .../page-slice/plugin_metadata/PluginMetadata.tsx | 2 +- src/routes/routes/detail.$id.tsx | 24 +++-- src/styles/global.css | 38 +++++++ 12 files changed, 235 insertions(+), 117 deletions(-) diff --git a/e2e/tests/hot-path.upstream-service-route.spec.ts b/e2e/tests/hot-path.upstream-service-route.spec.ts index 8f1c63bb8..60521f3bc 100644 --- a/e2e/tests/hot-path.upstream-service-route.spec.ts +++ b/e2e/tests/hot-path.upstream-service-route.spec.ts @@ -20,7 +20,11 @@ import { upstreamsPom } from '@e2e/pom/upstreams'; import { randomId } from '@e2e/utils/common'; import { e2eReq } from '@e2e/utils/req'; import { test } from '@e2e/utils/test'; -import { uiClearEditor, uiHasToastMsg } from '@e2e/utils/ui'; +import { + uiFillMonacoEditor, + uiGetMonacoEditor, + uiHasToastMsg, +} from '@e2e/utils/ui'; import { expect } from '@playwright/test'; import { deleteAllRoutes } from '@/apis/routes'; @@ -139,7 +143,7 @@ test('can create upstream -> service -> route', async ({ page }) => { * Plugins: Enable limit-count with custom configuration */ const servicePluginName = 'limit-count'; - const service: Partial<APISIXType['Service']> = { + const service = { // will be set in test id: undefined, name: randomId('HTTPBIN Service'), @@ -150,9 +154,10 @@ test('can create upstream -> service -> route', async ({ page }) => { time_window: 60, rejected_code: 429, key: 'remote_addr', + policy: 'local', }, }, - }; + } satisfies Partial<APISIXType['Service']>; await test.step('create service', async () => { // upstream id should be set expect(service.upstream_id).not.toBeUndefined(); @@ -197,15 +202,14 @@ test('can create upstream -> service -> route', async ({ page }) => { // Configure the plugin const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' }); - const editorLoading = addPluginDialog.getByTestId('editor-loading'); - await expect(editorLoading).toBeHidden(); - - // Clear the editor and add custom configuration - const editor = addPluginDialog.getByRole('code').getByRole('textbox'); - await uiClearEditor(page); + const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog); // Add plugin configuration - await editor.fill(JSON.stringify(service.plugins?.[servicePluginName])); + await uiFillMonacoEditor( + page, + pluginEditor, + JSON.stringify(service.plugins?.[servicePluginName]) + ); // Add the plugin await addPluginDialog.getByRole('button', { name: 'Add' }).click(); @@ -311,15 +315,14 @@ test('can create upstream -> service -> route', async ({ page }) => { // Configure the plugin const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' }); - const editorLoading = addPluginDialog.getByTestId('editor-loading'); - await expect(editorLoading).toBeHidden(); - - // Clear the editor and add custom configuration - const editor = addPluginDialog.getByRole('code').getByRole('textbox'); - await uiClearEditor(page); + const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog); // Add plugin configuration - await editor.fill(JSON.stringify(route.plugins?.[routePluginName])); + await uiFillMonacoEditor( + page, + pluginEditor, + JSON.stringify(route.plugins?.[routePluginName]) + ); // Add the plugin await addPluginDialog.getByRole('button', { name: 'Add' }).click(); diff --git a/e2e/tests/routes.crud-all-fields.spec.ts b/e2e/tests/routes.crud-all-fields.spec.ts index 31928b773..8ddd6fa96 100644 --- a/e2e/tests/routes.crud-all-fields.spec.ts +++ b/e2e/tests/routes.crud-all-fields.spec.ts @@ -18,7 +18,12 @@ import { routesPom } from '@e2e/pom/routes'; import { randomId } from '@e2e/utils/common'; import { e2eReq } from '@e2e/utils/req'; import { test } from '@e2e/utils/test'; -import { uiClearEditor, uiHasToastMsg } from '@e2e/utils/ui'; +import { + uiClearMonacoEditor, + uiFillMonacoEditor, + uiGetMonacoEditor, + uiHasToastMsg, +} from '@e2e/utils/ui'; import { uiFillUpstreamAllFields } from '@e2e/utils/ui/upstreams'; import { expect } from '@playwright/test'; @@ -33,13 +38,18 @@ const nodes: APISIXType['UpstreamNode'][] = [ { host: 'test.com', port: 80, weight: 100 }, { host: 'test2.com', port: 80, weight: 100 }, ]; +// Define vars values for testing +const initialVars = '[["arg_name", "==", "json"], ["arg_age", ">", 18]]'; +const updatedVars = '[["arg_name", "==", "updated"], ["arg_age", ">", 21]]'; test.beforeAll(async () => { await deleteAllRoutes(e2eReq); }); test('should CRUD route with all fields', async ({ page }) => { - test.setTimeout(30000); + test.slow(); + + const varsSection = page.getByText('Vars').locator('..'); // Navigate to the route list page await routesPom.toIndex(page); @@ -85,6 +95,10 @@ test('should CRUD route with all fields', async ({ page }) => { await page.getByRole('option', { name: 'Disabled' }).click(); await expect(status).toHaveValue('Disabled'); + // Fill in Vars field + const varsEditor = await uiGetMonacoEditor(page, varsSection); + await uiFillMonacoEditor(page, varsEditor, initialVars); + // Add upstream nodes const upstreamSection = page.getByRole('group', { name: 'Upstream', @@ -120,11 +134,8 @@ test('should CRUD route with all fields', async ({ page }) => { .click(); const addPluginDialog = page.getByRole('dialog', { name: 'Add Plugin' }); - const editorLoading = addPluginDialog.getByTestId('editor-loading'); - await expect(editorLoading).toBeHidden(); - const editor = addPluginDialog.getByRole('code').getByRole('textbox'); - await uiClearEditor(page); - await editor.fill('{"hide_credentials": true}'); + const pluginEditor = await uiGetMonacoEditor(page, addPluginDialog); + await uiFillMonacoEditor(page, pluginEditor, '{"hide_credentials": true}'); // add plugin await addPluginDialog.getByRole('button', { name: 'Add' }).click(); await expect(addPluginDialog).toBeHidden(); @@ -135,7 +146,6 @@ test('should CRUD route with all fields', async ({ page }) => { // should show edit plugin dialog const editPluginDialog = page.getByRole('dialog', { name: 'Edit Plugin' }); - await expect(editorLoading).toBeHidden(); await expect(editPluginDialog.getByText('hide_credentials')).toBeVisible(); // save edit plugin dialog @@ -157,13 +167,12 @@ test('should CRUD route with all fields', async ({ page }) => { // real-ip need config, otherwise it will show an error await addPluginDialog.getByRole('button', { name: 'Add' }).click(); await expect(addPluginDialog).toBeVisible(); - await expect(editorLoading).toBeHidden(); await expect( addPluginDialog.getByText('Missing property "source"') ).toBeVisible(); // clear the editor, will show JSON format is not valid - await uiClearEditor(page); + await uiClearMonacoEditor(page, pluginEditor); await expect( addPluginDialog.getByText('JSON format is not valid') ).toBeVisible(); @@ -175,7 +184,11 @@ test('should CRUD route with all fields', async ({ page }) => { ).toBeVisible(); // add a valid config - await editor.fill('{"source": "X-Forwarded-For"}'); + await uiFillMonacoEditor( + page, + pluginEditor, + '{"source": "X-Forwarded-For"}' + ); await addPluginDialog.getByRole('button', { name: 'Add' }).click(); await expect(addPluginDialog).toBeHidden(); @@ -183,7 +196,6 @@ test('should CRUD route with all fields', async ({ page }) => { const realIpPlugin = page.getByTestId('plugin-real-ip'); await realIpPlugin.getByRole('button', { name: 'Edit' }).click(); await expect(editPluginDialog).toBeVisible(); - await expect(editorLoading).toBeHidden(); await expect(editPluginDialog.getByText('X-Forwarded-For')).toBeVisible(); // close await editPluginDialog.getByRole('button', { name: 'Save' }).click(); @@ -249,6 +261,10 @@ test('should CRUD route with all fields', async ({ page }) => { const status = page.getByRole('textbox', { name: 'Status', exact: true }); await expect(status).toHaveValue('Disabled'); + // Verify Vars field + await expect(varsSection.getByText('arg_name')).toBeVisible(); + await expect(varsSection.getByText('json')).toBeVisible(); + // Verify Plugins await expect(page.getByText('basic-auth')).toBeHidden(); await expect(page.getByText('real-ip')).toBeVisible(); @@ -279,6 +295,10 @@ test('should CRUD route with all fields', async ({ page }) => { // Update Priority await page.getByLabel('Priority', { exact: true }).first().fill('200'); + // Update Vars field + const varsEditor = await uiGetMonacoEditor(page, varsSection); + await uiFillMonacoEditor(page, varsEditor, updatedVars); + // Click the Save button to save changes const saveBtn = page.getByRole('button', { name: 'Save' }); await saveBtn.click(); @@ -312,6 +332,10 @@ test('should CRUD route with all fields', async ({ page }) => { page.getByLabel('Priority', { exact: true }).first() ).toHaveValue('200'); + // Verify updated Vars field + await expect(varsSection.getByText('arg_name')).toBeVisible(); + await expect(varsSection.getByText('updated')).toBeVisible(); + // Return to list page and verify the route exists await routesPom.getRouteNavBtn(page).click(); await routesPom.isIndexPage(page); diff --git a/e2e/utils/ui/index.ts b/e2e/utils/ui/index.ts index a16d619bf..03e462468 100644 --- a/e2e/utils/ui/index.ts +++ b/e2e/utils/ui/index.ts @@ -15,7 +15,6 @@ * limitations under the License. */ import type { CommonPOM } from '@e2e/pom/type'; -import { type Monaco } from '@monaco-editor/react'; import { expect, type Locator, type Page } from '@playwright/test'; import type { FileRouteTypes } from '@/routeTree.gen'; @@ -64,10 +63,38 @@ export async function uiFillHTTPStatuses( } } -export async function uiClearEditor(page: Page) { - await page.evaluate(() => { - (window as unknown as { monaco?: Monaco })?.monaco?.editor - ?.getEditors()[0] - ?.setValue(''); - }); +export const uiClearMonacoEditor = async (page: Page, editor: Locator) => { + await editor.click(); + await page.keyboard.press('ControlOrMeta+A'); + await page.keyboard.press('Backspace'); + await editor.blur(); +}; + +export const uiGetMonacoEditor = async ( + page: Page, + parent: Locator, + clear = true +) => { + // Wait for Monaco editor to load + const editorLoading = parent.getByTestId('editor-loading'); + await expect(editorLoading).toBeHidden(); + const editor = parent.locator('.monaco-editor').first(); + await expect(editor).toBeVisible({ timeout: 10000 }); + + if (clear) { + await uiClearMonacoEditor(page, editor); + } + + return editor; +}; + +export const uiFillMonacoEditor = async ( + page: Page, + editor: Locator, + value: string +) => { + await editor.click(); + await editor.getByRole('textbox').pressSequentially(value); + await editor.blur(); + await page.waitForTimeout(800); }; diff --git a/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx b/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx index 23742630a..8a72b8122 100644 --- a/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx +++ b/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import { Drawer, Group, Title } from '@mantine/core'; -import { isEmpty } from 'rambdax'; +import { isEmpty, isNil } from 'rambdax'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -35,7 +35,7 @@ export type PluginEditorDrawerProps = Pick<PluginCardListProps, 'mode'> & { }; const toConfigStr = (p: object): string => { - return !isEmpty(p) ? JSON.stringify(p, null, 2) : '{}'; + return !isEmpty(p) && !isNil(p) ? JSON.stringify(p, null, 2) : '{}'; }; export const PluginEditorDrawer = (props: PluginEditorDrawerProps) => { const { opened, onSave, onClose, plugin, mode, schema } = props; @@ -44,7 +44,7 @@ export const PluginEditorDrawer = (props: PluginEditorDrawerProps) => { const methods = useForm<{ config: string }>({ criteriaMode: 'all', disabled: mode === 'view', - defaultValues: { config: toConfigStr(plugin) }, + defaultValues: { config: toConfigStr(config) }, }); const handleClose = () => { onClose(); diff --git a/src/components/form-slice/FormItemPlugins/index.tsx b/src/components/form-slice/FormItemPlugins/index.tsx index 746067cb2..d3f3fd8eb 100644 --- a/src/components/form-slice/FormItemPlugins/index.tsx +++ b/src/components/form-slice/FormItemPlugins/index.tsx @@ -180,7 +180,7 @@ export const FormItemPlugins = <T extends FieldValues>( schema={toJS(pluginsOb.curPluginSchema)} opened={pluginsOb.editorOpened} onClose={pluginsOb.closeEditor} - plugin={pluginsOb.curPlugin} + plugin={toJS(pluginsOb.curPlugin)} onSave={pluginsOb.update} /> </Drawer.Stack> diff --git a/src/components/form-slice/FormPartRoute/index.tsx b/src/components/form-slice/FormPartRoute/index.tsx index 23c7007e3..bb483eba0 100644 --- a/src/components/form-slice/FormPartRoute/index.tsx +++ b/src/components/form-slice/FormPartRoute/index.tsx @@ -18,6 +18,7 @@ import { Divider, InputWrapper } from '@mantine/core'; import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { FormItemEditor } from '@/components/form/Editor'; import { FormItemNumberInput } from '@/components/form/NumberInput'; import { FormItemSwitch } from '@/components/form/Switch'; import { FormItemTagsInput } from '@/components/form/TagInput'; @@ -94,7 +95,7 @@ const FormSectionMatchRules = () => { name="remote_addrs" label={t('form.routes.remoteAddrs')} /> - <FormItemTagsInput + <FormItemEditor control={control} name="vars" label={t('form.routes.vars')} diff --git a/src/components/form-slice/FormPartRoute/schema.ts b/src/components/form-slice/FormPartRoute/schema.ts index 305d5a5ea..dc56cb724 100644 --- a/src/components/form-slice/FormPartRoute/schema.ts +++ b/src/components/form-slice/FormPartRoute/schema.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import type { z } from 'zod'; +import { z } from 'zod'; import { APISIX } from '@/types/schema/apisix'; @@ -22,6 +22,17 @@ export const RoutePostSchema = APISIX.Route.omit({ id: true, create_time: true, update_time: true, +}).extend({ + // the FormItemEditor (monaco) is for editing text, + // and passing the original schema of `vars` for validation + // is not in line with this usage. + vars: z.string().optional(), }); export type RoutePostType = z.infer<typeof RoutePostSchema>; + +export const RoutePutSchema = APISIX.Route.extend({ + vars: z.string().optional(), +}); + +export type RoutePutType = z.infer<typeof RoutePutSchema>; diff --git a/src/components/form-slice/FormPartRoute/util.ts b/src/components/form-slice/FormPartRoute/util.ts index 9ae7f3cc3..7b406f7e7 100644 --- a/src/components/form-slice/FormPartRoute/util.ts +++ b/src/components/form-slice/FormPartRoute/util.ts @@ -14,9 +14,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { produce } from 'immer'; + import { produceRmUpstreamWhenHas } from '@/utils/form-producer'; import { pipeProduce } from '@/utils/producer'; +import type { RoutePostType, RoutePutType } from './schema'; + +export const produceVarsToForm = produce((draft: RoutePostType) => { + if (draft.vars && Array.isArray(draft.vars)) { + draft.vars = JSON.stringify(draft.vars); + } +}) as (draft: RoutePostType) => RoutePutType; + +export const produceVarsToAPI = produce((draft: RoutePostType) => { + if (draft.vars && typeof draft.vars === 'string') { + draft.vars = JSON.parse(draft.vars); + } +}); + export const produceRoute = pipeProduce( - produceRmUpstreamWhenHas('service_id', 'upstream_id') + produceRmUpstreamWhenHas('service_id', 'upstream_id'), + produceVarsToAPI ); diff --git a/src/components/form/Editor.tsx b/src/components/form/Editor.tsx index 56357cf03..2501fb22a 100644 --- a/src/components/form/Editor.tsx +++ b/src/components/form/Editor.tsx @@ -14,18 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { InputWrapper, type InputWrapperProps,Skeleton } from '@mantine/core'; -import { Editor, loader, type Monaco,useMonaco } from '@monaco-editor/react'; -import { editor, Uri } from 'monaco-editor'; +import { InputWrapper, type InputWrapperProps, Skeleton } from '@mantine/core'; +import { Editor, loader, type Monaco, useMonaco } from '@monaco-editor/react'; +import clsx from 'clsx'; +import { editor } from 'monaco-editor'; import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { type FieldValues, useController, type UseControllerProps, useFormContext, - useFormState, } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -73,67 +73,63 @@ export const FormItemEditor = <T extends FieldValues>( const { t } = useTranslation(); const { controllerProps, restProps } = genControllerProps(props, ''); const { customSchema, language, isLoading, ...wrapperProps } = restProps; - const { setError, clearErrors } = useFormContext<{ - [name: string]: object; - }>(); - const customErrorField = `${props.name}-editor`; - const { errors } = useFormState({ name: customErrorField }); + const { trigger } = useFormContext(); + const monacoErrorRef = useRef<string | null>(null); + const enhancedControllerProps = useMemo(() => { + return { + ...controllerProps, + rules: { + ...controllerProps.rules, + validate: (value: string) => { + // Check JSON syntax + try { + JSON.parse(value); + } catch { + return t('form.json.parseError'); + } + // Check Monaco markers + if (monacoErrorRef.current) { + return monacoErrorRef.current; + } + return true; + }, + }, + }; + }, [controllerProps, t, monacoErrorRef]); + const { field: { value, onChange: fOnChange, ...restField }, fieldState, - } = useController<T>(controllerProps); + } = useController<T>(enhancedControllerProps); const monaco = useMonaco(); const [internalLoading, setLoading] = useState(false); - const showErrOnMarkers = useCallback( - (resource: Uri) => { - const markers = monaco?.editor.getModelMarkers({ resource }); - const marker = markers?.[0]; - if (!marker) return false; - setError(customErrorField, { - type: 'custom', - message: marker.message, - }); - return true; - }, - [customErrorField, monaco?.editor, setError] - ); - useEffect(() => { - if (!monaco || !customSchema) return; + if (!monaco) return; setLoading(true); + + const schemas = []; + if (customSchema) { + schemas.push({ + uri: 'https://apisix.apache.org', + fileMatch: ['*'], + schema: customSchema, + }); + } monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, - schemas: [ - { - uri: 'https://apisix.apache.org', - fileMatch: ['*'], - schema: customSchema, - }, - ], + schemas, trailingCommas: 'error', enableSchemaRequest: false, }); - // when markers change, show error - monaco.editor.onDidChangeMarkers(([uri]) => { - showErrOnMarkers(uri); - }); setLoading(false); - }, [customSchema, monaco, showErrOnMarkers]); + }, [monaco, customSchema]); return ( <InputWrapper - error={ - fieldState.error?.message || - (errors[customErrorField]?.message as string) - } - style={{ - border: '1px solid var(--mantine-color-gray-2)', - borderRadius: 'var(--mantine-radius-sm)', - position: 'relative', - }} + error={fieldState.error?.message} id="#editor-wrapper" {...wrapperProps} > @@ -153,14 +149,20 @@ export const FormItemEditor = <T extends FieldValues>( /> )} <Editor - beforeMount={(monaco) => { - setupMonaco({ - monaco, - }); + wrapperProps={{ + className: clsx( + 'editor-wrapper', + restField.disabled && 'editor-wrapper--disabled' + ), }} + beforeMount={(monaco) => setupMonaco({ monaco })} defaultValue={controllerProps.defaultValue} value={value} onChange={fOnChange} + onValidate={(markers) => { + monacoErrorRef.current = markers?.[0]?.message || null; + trigger(props.name); + }} loading={ <Skeleton data-testid="editor-loading" @@ -170,21 +172,6 @@ export const FormItemEditor = <T extends FieldValues>( /> } options={{ ...options, readOnly: restField.disabled }} - onMount={(editor) => { - // this only check json validity, will clear error when json is valid and no markers - editor.onDidChangeModelContent(() => { - try { - const model = editor.getModel()!; - JSON.parse(model.getValue()); - clearErrors(customErrorField); - } catch { - return setError(customErrorField, { - type: 'custom', - message: t('form.json.parseError'), - }); - } - }); - }} defaultLanguage="json" {...(language && { language })} /> diff --git a/src/components/page-slice/plugin_metadata/PluginMetadata.tsx b/src/components/page-slice/plugin_metadata/PluginMetadata.tsx index 486f1131e..177ba75c4 100644 --- a/src/components/page-slice/plugin_metadata/PluginMetadata.tsx +++ b/src/components/page-slice/plugin_metadata/PluginMetadata.tsx @@ -161,7 +161,7 @@ export const PluginMetadata = () => { schema={toJS(pluginsOb.curPluginSchema)} opened={pluginsOb.editorOpened} onClose={pluginsOb.closeEditor} - plugin={pluginsOb.curPlugin} + plugin={toJS(pluginsOb.curPlugin)} onSave={pluginsOb.update} /> </Drawer.Stack> diff --git a/src/routes/routes/detail.$id.tsx b/src/routes/routes/detail.$id.tsx index 1f5ab4724..ca49bf2de 100644 --- a/src/routes/routes/detail.$id.tsx +++ b/src/routes/routes/detail.$id.tsx @@ -15,7 +15,7 @@ * limitations under the License. */ import { zodResolver } from '@hookform/resolvers/zod'; -import { Button, Group,Skeleton } from '@mantine/core'; +import { Button, Group, Skeleton } from '@mantine/core'; import { notifications } from '@mantine/notifications'; import { useMutation, useQuery } from '@tanstack/react-query'; import { @@ -32,7 +32,14 @@ import { getRouteQueryOptions } from '@/apis/hooks'; import { putRouteReq } from '@/apis/routes'; import { FormSubmitBtn } from '@/components/form/Btn'; import { FormPartRoute } from '@/components/form-slice/FormPartRoute'; -import { produceRoute } from '@/components/form-slice/FormPartRoute/util'; +import { + RoutePutSchema, + type RoutePutType, +} from '@/components/form-slice/FormPartRoute/schema'; +import { + produceRoute, + produceVarsToForm, +} from '@/components/form-slice/FormPartRoute/util'; import { produceToUpstreamForm } from '@/components/form-slice/FormPartUpstream/util'; import { FormTOCBox } from '@/components/form-slice/FormSection'; import { FormSectionGeneral } from '@/components/form-slice/FormSectionGeneral'; @@ -40,7 +47,7 @@ import { DeleteResourceBtn } from '@/components/page/DeleteResourceBtn'; import PageHeader from '@/components/page/PageHeader'; import { API_ROUTES } from '@/config/constant'; import { req } from '@/config/req'; -import { APISIX, type APISIXType } from '@/types/schema/apisix'; +import { type APISIXType } from '@/types/schema/apisix'; type Props = { readOnly: boolean; @@ -56,7 +63,7 @@ const RouteDetailForm = (props: Props) => { const { data: routeData, isLoading, refetch } = routeQuery; const form = useForm({ - resolver: zodResolver(APISIX.Route), + resolver: zodResolver(RoutePutSchema), shouldUnregister: true, shouldFocusError: true, mode: 'all', @@ -65,14 +72,17 @@ const RouteDetailForm = (props: Props) => { useEffect(() => { if (routeData?.value && !isLoading) { - form.reset( - produceToUpstreamForm(routeData.value.upstream || {}, routeData.value) + const upstreamProduced = produceToUpstreamForm( + routeData.value.upstream || {}, + routeData.value ); + form.reset(produceVarsToForm(upstreamProduced)); } }, [routeData, form, isLoading]); const putRoute = useMutation({ - mutationFn: (d: APISIXType['Route']) => putRouteReq(req, produceRoute(d)), + mutationFn: (d: RoutePutType) => + putRouteReq(req, produceRoute(d) as APISIXType['Route']), async onSuccess() { notifications.show({ message: t('info.edit.success', { name: t('routes.singular') }), diff --git a/src/styles/global.css b/src/styles/global.css index 02fe48f3c..2e63b257e 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -12,3 +12,41 @@ .monaco-editor { padding-bottom: 200px; } + +.editor-wrapper { + border: 1px solid var(--mantine-color-gray-4); + border-radius: var(--mantine-radius-sm); + position: relative; + height: 100%; + + &:focus-within { + border-color: var(--mantine-color-blue-6); + } +} + +.editor-wrapper--disabled { + background-color: var(--mantine-color-gray-0); + border-color: var(--mantine-color-gray-3); + cursor: not-allowed !important; + + &:focus-within { + border-color: var(--mantine-color-gray-3); + } + + & * { + cursor: not-allowed !important; + pointer-events: none !important; + } + + .monaco-editor, + .monaco-editor .monaco-editor-background, + .monaco-editor .overflow-guard, + .monaco-editor .margin-view-overlays { + background-color: var(--mantine-color-gray-0) !important; + } + + .monaco-editor .view-lines, + .monaco-editor .view-line * { + color: var(--mantine-color-gray-5) !important; + } +}