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 9e9497be8 fix(settings): debounce the admin-key query refresh (#3422)
9e9497be8 is described below
commit 9e9497be84c0842807b748952aa17d55932c9ce2
Author: Ming Wen <[email protected]>
AuthorDate: Mon Jul 13 09:58:15 2026 +0800
fix(settings): debounce the admin-key query refresh (#3422)
---
.../settings.admin-key-debounced-refetch.spec.ts | 72 ++++++++++++++++++++++
src/components/page/SettingsModal.tsx | 27 ++++++--
2 files changed, 95 insertions(+), 4 deletions(-)
diff --git a/e2e/tests/regression/settings.admin-key-debounced-refetch.spec.ts
b/e2e/tests/regression/settings.admin-key-debounced-refetch.spec.ts
new file mode 100644
index 000000000..6dce785f6
--- /dev/null
+++ b/e2e/tests/regression/settings.admin-key-debounced-refetch.spec.ts
@@ -0,0 +1,72 @@
+/**
+ * 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: the Admin Key field used to fire
+// queryClient.invalidateQueries() + refetchQueries() on EVERY keystroke —
+// typing a 32-char key hammered the Admin API with 32 full refetch storms
+// of every mounted query. The refresh must be debounced.
+
+import { getAPISIXConf } from '@e2e/utils/common';
+import { test } from '@e2e/utils/test';
+import { expect } from '@playwright/test';
+
+// start with no stored key so the settings modal is open on load
+test.use({ storageState: { cookies: [], origins: [] } });
+
+test('typing the admin key does not refetch per keystroke', async ({
+ page,
+}) => {
+ await page.goto('./services');
+ const adminKeyInput = page.getByLabel('Admin Key');
+ await expect(adminKeyInput).toBeVisible();
+
+ // count Admin API requests issued while typing
+ let adminRequests = 0;
+ page.on('request', (req) => {
+ if (req.url().includes('/apisix/admin/')) adminRequests += 1;
+ });
+
+ const { adminKey } = await getAPISIXConf();
+ // type character by character — the old implementation refetched every
+ // mounted query once per character
+ await adminKeyInput.pressSequentially(adminKey, { delay: 30 });
+
+ // the debounced refresh must actually fire and succeed with the
+ // completed key — 401-retry noise alone cannot satisfy this
+ await page.waitForResponse(
+ (res) => res.url().includes('/apisix/admin/') && res.ok(),
+ { timeout: 5000 }
+ );
+
+ // wait until the request count settled
+ let lastSeen = -1;
+ await expect
+ .poll(
+ () => {
+ const settled = adminRequests > 0 && adminRequests === lastSeen;
+ lastSeen = adminRequests;
+ return settled;
+ },
+ { timeout: 10000, intervals: [1000] }
+ )
+ .toBe(true);
+
+ // the old behavior issued at least one refetch per keystroke
+ // (>= adminKey.length); one debounced refresh plus 401-retry noise
+ // stays far below that
+ expect(adminRequests).toBeLessThan(adminKey.length);
+});
diff --git a/src/components/page/SettingsModal.tsx
b/src/components/page/SettingsModal.tsx
index 10cde2ce5..3f861cbdc 100644
--- a/src/components/page/SettingsModal.tsx
+++ b/src/components/page/SettingsModal.tsx
@@ -21,6 +21,7 @@ import {
PasswordInput,
Text,
} from '@mantine/core';
+import { useDebouncedCallback } from '@mantine/hooks';
import { useAtom } from 'jotai';
import { useTranslation } from 'react-i18next';
@@ -28,20 +29,38 @@ import { queryClient } from '@/config/global';
import { adminKeyAtom, isSettingsOpenAtom } from '@/stores/global';
import { sha } from '~build/git';
+const REFRESH_DEBOUNCE_MS = 500;
+
const AdminKey = () => {
const { t } = useTranslation();
const [adminKey, setAdminKey] = useAtom(adminKeyAtom);
+ // the atom must update per keystroke (controlled input + the axios
+ // interceptor reads it), but refreshing every mounted query per
+ // keystroke hammered the Admin API: typing a 32-char key fired 32
+ // full refetch storms. Refresh once the user pauses.
+ // - invalidate with refetchType 'none' + a single refetchQueries():
+ // marks everything stale and issues exactly one fetch per query
+ // (the old invalidate + refetch pair double-fetched active queries),
+ // while still refetching observer-less error-state queries, which
+ // recovery from a wrong key depends on
+ // - flushOnUnmount: pasting a key and immediately closing the modal
+ // unmounts this component — the pending refresh must still fire
+ const refreshQueries = useDebouncedCallback(
+ () => {
+ queryClient.invalidateQueries({ refetchType: 'none' });
+ queryClient.refetchQueries();
+ },
+ { delay: REFRESH_DEBOUNCE_MS, flushOnUnmount: true }
+ );
+
return (
<PasswordInput
label={t('settings.adminKey')}
value={adminKey}
onChange={(e) => {
setAdminKey(e.currentTarget.value);
- setTimeout(() => {
- queryClient.invalidateQueries();
- queryClient.refetchQueries();
- });
+ refreshQueries();
}}
required
/>