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;
+  }
+}

Reply via email to