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
     />

Reply via email to