This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 1226f0131 fix(users): refresh the query on screen after a row mutation 
(#6523)
1226f0131 is described below

commit 1226f0131d383d0694000f27ea9001adcf26452e
Author: Apulupie <[email protected]>
AuthorDate: Thu Oct 15 18:14:02 2026 +0800

    fix(users): refresh the query on screen after a row mutation (#6523)
    
    Two mutations on the users page refreshed the list by awaiting the 
`loadUsers` captured when the
    operator confirmed them: the row status switch (`setEnabled`) and the 
session revoke popovers
    (`revokeSessions`, from the row and from the drawer). Both leave the page 
interactive, so the search
    and the page can change while the request is on the wire; the continuation 
then re-issued the old
    query (`search: undefined`, the old page) and wrote its rows and total over 
the newer result, leaving
    the table showing rows the search box no longer selects.
    `#6198` fixed the same shape for the two settings tabs.
    
    Both refreshes now bump a `refreshVersion` the list effect depends on, so 
they run against the query
    on screen; the request-sequence guard still decides which response may 
write. The modal-confirmed
    paths (create user, password reset) keep awaiting the loader: their modals 
block the page, so the
    query cannot change while they run.
---
 web/src/pages/studio/UserManagement.tsx            | 11 +++--
 .../pages/studio/__tests__/UserManagement.test.tsx | 48 ++++++++++++++++++++++
 2 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/web/src/pages/studio/UserManagement.tsx 
b/web/src/pages/studio/UserManagement.tsx
index 61662e3af..e33972a0a 100644
--- a/web/src/pages/studio/UserManagement.tsx
+++ b/web/src/pages/studio/UserManagement.tsx
@@ -139,6 +139,7 @@ const UserManagementPage = () => {
   const [passwordTarget, setPasswordTarget] = useState<StudioUser | 
null>(null);
   const [userExporting, setUserExporting] = useState(false);
   const [mutatingUserIds, setMutatingUserIds] = useState<Set<number>>(() => 
new Set());
+  const [refreshVersion, setRefreshVersion] = useState(0);
   const [createForm] = Form.useForm<CreateFormValues>();
   const [passwordForm] = Form.useForm<PasswordFormValues>();
   const requestSeqRef = useRef(0);
@@ -195,12 +196,16 @@ const UserManagementPage = () => {
     }
   }, [admin, debouncedSearch, page, pageSize, roleFilter, statusFilter, t]);
 
+  // Mutations refresh through the effect (bumping `refreshVersion`) instead 
of awaiting the
+  // `loadUsers` captured when the operator confirmed: a row's status switch 
and the session
+  // revoke popovers leave the list interactive, so the search or the page can 
change while the
+  // request is on the wire, and the refresh must re-query what is on screen 
then.
   useEffect(() => {
     const timer = window.setTimeout(() => {
       void loadUsers();
     }, 0);
     return () => window.clearTimeout(timer);
-  }, [loadUsers]);
+  }, [loadUsers, refreshVersion]);
 
   useEffect(
     () => () => {
@@ -304,7 +309,7 @@ const UserManagementPage = () => {
       async () => {
         await setStudioUserEnabled(record.id, enabled);
         message.success(enabled ? t('userMgmt.userEnabled') : 
t('userMgmt.userDisabled'));
-        await loadUsers();
+        setRefreshVersion((version) => version + 1);
       },
       t('userMgmt.updateStatusFailed'),
     );
@@ -351,7 +356,7 @@ const UserManagementPage = () => {
         if (sessionModalUserIdRef.current === record.id) {
           await loadSessionDetails({ ...record, activeSessionCount: 0 });
         }
-        await loadUsers();
+        setRefreshVersion((version) => version + 1);
       },
       t('userMgmt.revokeFailed'),
     );
diff --git a/web/src/pages/studio/__tests__/UserManagement.test.tsx 
b/web/src/pages/studio/__tests__/UserManagement.test.tsx
index 72ea55605..6b3459987 100644
--- a/web/src/pages/studio/__tests__/UserManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/UserManagement.test.tsx
@@ -106,6 +106,16 @@ const sessionDetails: StudioUserSessionDetail[] = [
   },
 ];
 
+const deferred = <T,>() => {
+  let resolve!: (value: T) => void;
+  let reject!: (reason?: unknown) => void;
+  const promise = new Promise<T>((resolvePromise, rejectPromise) => {
+    resolve = resolvePromise;
+    reject = rejectPromise;
+  });
+  return { promise, resolve, reject };
+};
+
 const renderPage = () =>
   render(
     <MemoryRouter>
@@ -295,6 +305,44 @@ describe('UserManagementPage', () => {
     expect(listStudioUsers).toHaveBeenCalledTimes(2);
   });
 
+  it('refreshes the query on screen when a revocation finishes after the 
search changes', async () => {
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    const pendingRevoke = deferred<{ userId: number; revokedSessionCount: 
number }>();
+    
vi.mocked(revokeStudioUserSessions).mockReturnValueOnce(pendingRevoke.promise);
+    const narrowedPage = {
+      items: [{ ...studioUserPage.items[0], id: 8, username: 'current-user' }],
+      total: 1,
+      page: 1,
+      size: 20,
+    };
+    vi.mocked(listStudioUsers).mockImplementation(async (params) =>
+      params?.search === 'current' ? narrowedPage : studioUserPage,
+    );
+    renderPage();
+    await screen.findByText('operator');
+
+    await user.click(screen.getByRole('button', { name: '注销' }));
+    await screen.findByText('注销 operator 的活跃会话?');
+    await confirmRevokePopover(user);
+    await waitFor(() => 
expect(revokeStudioUserSessions).toHaveBeenCalledWith(7));
+
+    // The operator narrows the list while the revocation is still on the 
wire. The refresh that
+    // follows must re-query what is on screen, not the search captured when 
the popover was
+    // confirmed - otherwise the table snaps back to rows the search box no 
longer selects.
+    await user.type(screen.getByPlaceholderText('搜索用户名'), 'current');
+    await screen.findByText('current-user');
+
+    await act(async () => pendingRevoke.resolve({ userId: 7, 
revokedSessionCount: 2 }));
+
+    await waitFor(() =>
+      expect(vi.mocked(listStudioUsers).mock.lastCall?.[0]).toEqual(
+        expect.objectContaining({ search: 'current' }),
+      ),
+    );
+    expect(screen.getByText('current-user')).toBeInTheDocument();
+    expect(screen.queryByText('operator')).not.toBeInTheDocument();
+  });
+
   it('renders session timestamps as UTC values in the viewer timezone', async 
() => {
     // The user/session APIs serialize UTC LocalDateTime values without an 
offset, so
     // the table and drawer must not parse them as browser-local wall times. 
Pin the

Reply via email to