This is an automated email from the ASF dual-hosted git repository.
guoqqqi 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 9dd8e4782 fix: keep plugin editor drawer open when saving fails (#3427)
9dd8e4782 is described below
commit 9dd8e4782eaf4493535f7ea2d9d750e5a60a8242
Author: Yuhan <[email protected]>
AuthorDate: Tue Jul 14 14:49:15 2026 +0800
fix: keep plugin editor drawer open when saving fails (#3427)
---
...adata.drawer-keeps-edits-on-failed-save.spec.ts | 115 +++++++++++++++++++++
.../FormItemPlugins/PluginEditorDrawer.tsx | 15 ++-
2 files changed, 127 insertions(+), 3 deletions(-)
diff --git
a/e2e/tests/regression/plugin-metadata.drawer-keeps-edits-on-failed-save.spec.ts
b/e2e/tests/regression/plugin-metadata.drawer-keeps-edits-on-failed-save.spec.ts
new file mode 100644
index 000000000..9e043391d
--- /dev/null
+++
b/e2e/tests/regression/plugin-metadata.drawer-keeps-edits-on-failed-save.spec.ts
@@ -0,0 +1,115 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// Regression: PluginEditorDrawer called its void-typed onSave and then
+// synchronously closed the drawer and reset the form. On the Plugin
+// Metadata page onSave is putMetadata.mutateAsync, so a failed PUT closed
+// the drawer as if it had succeeded and destroyed the user's edited JSON.
+// The drawer now awaits onSave and keeps the drawer (and edits) on failure.
+//
+// Part of apache/apisix-dashboard#3417 (error handling item 5).
+
+import { pluginMetadataPom } from '@e2e/pom/plugin_metadata';
+import { e2eReq } from '@e2e/utils/req';
+import { test } from '@e2e/utils/test';
+import {
+ uiFillMonacoEditor,
+ uiGetMonacoEditor,
+ uiHasToastMsg,
+} from '@e2e/utils/ui';
+import { expect } from '@playwright/test';
+
+import { API_PLUGIN_METADATA } from '@/config/constant';
+
+const PLUGIN = 'syslog';
+const EDITED_HOST = '10.0.0.99';
+
+const deletePluginMetadata = async () => {
+ await e2eReq.delete(`${API_PLUGIN_METADATA}/${PLUGIN}`).catch(() => {
+ // ignore when it does not exist
+ });
+};
+
+test.beforeAll(async () => {
+ await deletePluginMetadata();
+ await e2eReq.put(`${API_PLUGIN_METADATA}/${PLUGIN}`, { host: '127.0.0.1' });
+});
+
+test.afterAll(async () => {
+ await deletePluginMetadata();
+});
+
+test('failed metadata save keeps the drawer open with the edits intact', async
({
+ page,
+}) => {
+ await pluginMetadataPom.toIndex(page);
+ await pluginMetadataPom.isIndexPage(page);
+
+ // Force the PUT to fail with a server-style error; reads stay live.
+ // Keep the matcher in a const: page.unroute matches function matchers
+ // by reference, so the same instance must be used to lift the fault.
+ const metadataUrl = (url: URL) =>
+ url.pathname.endsWith(`/apisix/admin/plugin_metadata/${PLUGIN}`);
+ await page.route(metadataUrl, async (route) => {
+ if (route.request().method() === 'PUT') {
+ await route.fulfill({
+ status: 500,
+ contentType: 'application/json',
+ body: JSON.stringify({ error_msg: 'forced 500 for regression' }),
+ });
+ } else {
+ await route.fallback();
+ }
+ });
+
+ const card = page.getByTestId(`plugin-${PLUGIN}`);
+ await card.getByRole('button', { name: 'Edit' }).click();
+
+ const dialog = page.getByRole('dialog', { name: 'Edit Plugin' });
+ await expect(dialog).toBeVisible();
+
+ const editor = await uiGetMonacoEditor(page, dialog);
+ await uiFillMonacoEditor(
+ page,
+ editor,
+ `{"host": "${EDITED_HOST}", "port": 5140}`
+ );
+
+ await dialog.getByRole('button', { name: 'Save' }).click();
+
+ // The failure toast comes from the axios interceptor.
+ await uiHasToastMsg(page, { hasText: 'forced 500 for regression' });
+
+ // Core contract: the drawer did NOT close and the edits are intact.
+ await expect(dialog).toBeVisible();
+ const editorValue = await page.evaluate(() =>
+ window.__monacoEditor__?.getValue()
+ );
+ expect(editorValue).toContain(EDITED_HOST);
+
+ // Recovery: lift the fault and the same Save must go through.
+ await page.unroute(metadataUrl);
+ await dialog.getByRole('button', { name: 'Save' }).click();
+ await uiHasToastMsg(page, { hasText: 'success' });
+ await expect(dialog).toBeHidden();
+
+ const saved = await e2eReq.get<
+ unknown,
+ { data: { value: { host: string; port: number } } }
+ >(`${API_PLUGIN_METADATA}/${PLUGIN}`);
+ expect(saved.data.value.host).toBe(EDITED_HOST);
+});
diff --git a/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
b/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
index 25175b420..eb55ede2b 100644
--- a/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
+++ b/src/components/form-slice/FormItemPlugins/PluginEditorDrawer.tsx
@@ -16,6 +16,7 @@
*/
import { Drawer, Group, Text, Title } from '@mantine/core';
import { modals } from '@mantine/modals';
+import { isAxiosError } from 'axios';
import { isEmpty, isNil } from 'rambdax';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
@@ -30,7 +31,7 @@ export type PluginConfig = { name: string; config: object };
export type PluginEditorDrawerProps = Pick<PluginCardListProps, 'mode'> & {
opened: boolean;
onClose: () => void;
- onSave: (props: PluginConfig) => void;
+ onSave: (props: PluginConfig) => void | Promise<unknown>;
plugin: PluginConfig;
schema?: object;
};
@@ -104,8 +105,16 @@ export const PluginEditorDrawer = (props:
PluginEditorDrawerProps) => {
<FormSubmitBtn
size="xs"
variant="light"
- onClick={methods.handleSubmit(({ config }) => {
- onSave({ name, config: JSON.parse(config) });
+ onClick={methods.handleSubmit(async ({ config }) => {
+ try {
+ await onSave({ name, config: JSON.parse(config) });
+ } catch (e) {
+ // the axios interceptor owns the error toast; keep the
+ // drawer open with the edits intact so the save can be
+ // retried, and rethrow non-axios errors (real bugs)
+ if (!isAxiosError(e)) throw e;
+ return;
+ }
onClose();
methods.reset();
})}