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 a95e98ab8 fix(users): preserve session drawer ownership after 
revocation (#6183)
a95e98ab8 is described below

commit a95e98ab819dd0e8bc17ed709e9352248a09d658
Author: zmuxuny <[email protected]>
AuthorDate: Mon Oct 12 00:17:18 2026 -0700

    fix(users): preserve session drawer ownership after revocation (#6183)
    
    `revokeSessions` captured `sessionDrawerUser` at call time and its 
continuation compared that stale value at `UserManagement.tsx:331`. Closing the 
drawer while a revocation was in flight and opening another user's drawer left 
the comparison true against the old id, so `loadSessionDetails(record)` ran for 
the revoked user and wrote its rows at `:217` — the drawer showed one user's 
title over another user's sessions. The existing `sessionDetailsRequestSeqRef` 
cannot catch this: the sequ [...]
    
    Two new cases in `UserManagement.test.tsx` release the pending revocation 
manually and assert both the drawer title and the concrete row contents. 
Restoring the `sessionDrawerUser?.id` comparison makes `does not replace 
another user session drawer when a previous revocation completes` render the 
revoked user's row again, and makes `does not reload session details after 
their drawer is closed during revocation` issue a second 
`listStudioUserSessions` call; removing the close/unmount cl [...]
---
 web/src/pages/studio/UserManagement.tsx            |  9 +++-
 .../pages/studio/__tests__/UserManagement.test.tsx | 61 ++++++++++++++++++++++
 2 files changed, 69 insertions(+), 1 deletion(-)

diff --git a/web/src/pages/studio/UserManagement.tsx 
b/web/src/pages/studio/UserManagement.tsx
index c40068e06..1dae92cac 100644
--- a/web/src/pages/studio/UserManagement.tsx
+++ b/web/src/pages/studio/UserManagement.tsx
@@ -142,6 +142,7 @@ const UserManagementPage = () => {
   const [passwordForm] = Form.useForm<PasswordFormValues>();
   const requestSeqRef = useRef(0);
   const sessionDetailsRequestSeqRef = useRef(0);
+  const sessionDrawerUserIdRef = useRef<number | null>(null);
   const mutatingUserIdsRef = useRef(new Set<number>());
 
   useEffect(() => {
@@ -155,6 +156,7 @@ const UserManagementPage = () => {
       setUsers([]);
       setTotal(0);
       setSessionOverview(null);
+      sessionDrawerUserIdRef.current = null;
       setSessionDrawerUser(null);
       setSessionDetails([]);
       return;
@@ -203,6 +205,7 @@ const UserManagementPage = () => {
     () => () => {
       requestSeqRef.current += 1;
       sessionDetailsRequestSeqRef.current += 1;
+      sessionDrawerUserIdRef.current = null;
     },
     [],
   );
@@ -233,12 +236,14 @@ const UserManagementPage = () => {
   );
 
   const openSessionDrawer = (record: StudioUser) => {
+    sessionDrawerUserIdRef.current = record.id;
     setSessionDrawerUser(record);
     setSessionDetails([]);
     void loadSessionDetails(record);
   };
 
   const closeSessionDrawer = () => {
+    sessionDrawerUserIdRef.current = null;
     sessionDetailsRequestSeqRef.current += 1;
     setSessionDrawerUser(null);
     setSessionDetails([]);
@@ -328,7 +333,9 @@ const UserManagementPage = () => {
           navigate('/login', { replace: true });
           return;
         }
-        if (sessionDrawerUser?.id === record.id) {
+        // The operator may have closed or changed drawers while revocation 
was pending.
+        // Do not let its follow-up read take ownership of another user's 
session details.
+        if (sessionDrawerUserIdRef.current === record.id) {
           await loadSessionDetails({ ...record, activeSessionCount: 0 });
         }
         await loadUsers();
diff --git a/web/src/pages/studio/__tests__/UserManagement.test.tsx 
b/web/src/pages/studio/__tests__/UserManagement.test.tsx
index 87db6f59b..f98ac4ac0 100644
--- a/web/src/pages/studio/__tests__/UserManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/UserManagement.test.tsx
@@ -335,6 +335,67 @@ describe('UserManagementPage', () => {
     expect(within(drawer).getByText('暂无活跃会话')).toBeInTheDocument();
   });
 
+  it('does not replace another user session drawer when a previous revocation 
completes', async () => {
+    vi.mocked(listStudioUsers).mockResolvedValue({
+      ...studioUserPage,
+      items: [
+        studioUserPage.items[0],
+        { ...studioUserPage.items[0], id: 8, username: 'second-user' },
+      ],
+    });
+    const secondSessions = [{ ...sessionDetails[0], id: 99, userId: 8 }];
+    vi.mocked(listStudioUserSessions).mockImplementation(async (id) =>
+      id === 8 ? secondSessions : sessionDetails,
+    );
+    let completeRevoke!: () => void;
+    vi.mocked(revokeStudioUserSessions).mockImplementationOnce(
+      () =>
+        new Promise((resolve) => {
+          completeRevoke = () => resolve({ userId: 7, revokedSessionCount: 2 
});
+        }),
+    );
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    renderPage();
+    const firstRow = await screen.findByRole('row', { name: /operator/ });
+    await user.click(within(firstRow).getByRole('button', { name: '会话' }));
+    const drawer = await screen.findByRole('dialog', { name: 'operator 的会话' });
+    await user.click(within(drawer).getByRole('button', { name: '注销全部' }));
+    await confirmRevokePopover(user);
+    await waitFor(() => 
expect(revokeStudioUserSessions).toHaveBeenCalledWith(7));
+    await user.click(within(drawer).getByRole('button', { name: /close/i }));
+    const secondRow = screen.getByRole('row', { name: /second-user/ });
+    await user.click(within(secondRow).getByRole('button', { name: '会话' }));
+    const secondDrawer = await screen.findByRole('dialog', { name: 
'second-user 的会话' });
+    expect(await within(secondDrawer).findByText('99')).toBeInTheDocument();
+    await act(async () => completeRevoke());
+    await waitFor(() => expect(listStudioUsers).toHaveBeenCalledTimes(2));
+    expect(within(secondDrawer).getByText('99')).toBeInTheDocument();
+    expect(within(secondDrawer).queryByText('19')).not.toBeInTheDocument();
+  });
+
+  it('does not reload session details after their drawer is closed during 
revocation', async () => {
+    let completeRevoke!: () => void;
+    vi.mocked(revokeStudioUserSessions).mockImplementationOnce(
+      () =>
+        new Promise((resolve) => {
+          completeRevoke = () => resolve({ userId: 7, revokedSessionCount: 2 
});
+        }),
+    );
+    const user = userEvent.setup({ pointerEventsCheck: 0 });
+    renderPage();
+    await screen.findByText('operator');
+    await user.click(screen.getByRole('button', { name: '会话' }));
+    const drawer = await screen.findByRole('dialog', { name: 'operator 的会话' });
+    await user.click(within(drawer).getByRole('button', { name: '注销全部' }));
+    await confirmRevokePopover(user);
+    await waitFor(() => 
expect(revokeStudioUserSessions).toHaveBeenCalledWith(7));
+    await user.click(within(drawer).getByRole('button', { name: /close/i }));
+    await act(async () => completeRevoke());
+    await waitFor(() => expect(listStudioUsers).toHaveBeenCalledTimes(2));
+    expect(listStudioUserSessions).toHaveBeenCalledTimes(1);
+    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
+  });
+
   it('refreshes the open session detail drawer', async () => {
     vi.mocked(listStudioUserSessions)
       .mockResolvedValueOnce(sessionDetails)

Reply via email to