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 12ce4b5a1 fix(acl): reload the user page after creating or deleting a
user (#3300)
12ce4b5a1 is described below
commit 12ce4b5a104ff1a2dfe05e3513dd21f7768418e0
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Sep 7 16:43:25 2026 +0800
fix(acl): reload the user page after creating or deleting a user (#3300)
User create/delete only mutated the local page array: the pagination
total kept its stale server value, a full page pushed its last row out
of view after create, and deleting the last row on the last page left
an empty page with no refetch to fall back on. The rules tab already
refetches the authoritative server page after its mutations via a
refresh key; give the user list the same refresh key so totals follow
mutations and an emptied page self-corrects.
Co-authored-by: unbridled-41
<[email protected]>
---
web/src/pages/instance/__tests__/AclPage.test.tsx | 49 +++++++++++++++++++++++
web/src/pages/instance/acl.tsx | 12 ++++--
2 files changed, 58 insertions(+), 3 deletions(-)
diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx
b/web/src/pages/instance/__tests__/AclPage.test.tsx
index e916f9c76..0e07ec39a 100644
--- a/web/src/pages/instance/__tests__/AclPage.test.tsx
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -597,6 +597,55 @@ describe('ACL page', () => {
});
});
+ it('reloads the server user page after creating an ACL user', async () => {
+ const user = userEvent.setup();
+ vi.mocked(aclService.createAclUser).mockResolvedValue({
+ id: 13,
+ username: 'orders-service',
+ accessKey: 'acce****3456',
+ secretKey: 'secr****7654',
+ admin: false,
+ clusters: ['cluster-a', 'cluster-b'],
+ gmtCreate: '2026-08-01T00:00:00Z',
+ });
+ renderWithProviders(<AclPage />);
+
+ await user.click(await screen.findByText('用户管理'));
+ const userPanel = screen.getByRole('tabpanel', { name: '用户管理' });
+ await user.click(within(userPanel).getByRole('button', { name: /添加用户/ }));
+ const dialog = await screen.findByRole('dialog');
+
+ await user.type(
+ within(dialog).getByPlaceholderText('例:user-order-service'),
+ 'orders-service',
+ );
+ const clusterInput = within(dialog).getByRole('combobox');
+ await user.type(clusterInput, 'cluster-a,cluster-b,');
+ await user.click(within(dialog).getByRole('button', { name: /添\s*加/ }));
+
+ await waitFor(() =>
expect(aclService.createAclUser).toHaveBeenCalledTimes(1));
+ await waitFor(() =>
expect(aclService.pageAclUsers).toHaveBeenCalledTimes(2));
+ });
+
+ it('reloads the server user page after deleting one ACL user', async () => {
+ const user = userEvent.setup();
+ const confirmSpy = vi.spyOn(Modal, 'confirm').mockImplementation((config)
=> {
+ void config.onOk?.();
+ return { destroy: vi.fn(), update: vi.fn() } as unknown as
ReturnType<typeof Modal.confirm>;
+ });
+ vi.mocked(aclService.deleteAclUser).mockResolvedValue(undefined);
+ renderWithProviders(<AclPage />);
+
+ await user.click(await screen.findByText('用户管理'));
+ const userPanel = screen.getByRole('tabpanel', { name: '用户管理' });
+ const row = await within(userPanel).findByRole('row', { name:
/remote-admin/ });
+ await user.click(within(row).getByRole('button', { name: /删除/ }));
+
+ await waitFor(() =>
expect(aclService.deleteAclUser).toHaveBeenCalledWith(11, undefined));
+ await waitFor(() =>
expect(aclService.pageAclUsers).toHaveBeenCalledTimes(2));
+ confirmSpy.mockRestore();
+ });
+
it('replaces the cluster scope of an existing user', async () => {
const user = userEvent.setup();
vi.mocked(aclService.updateAclUser).mockResolvedValue({
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index c04566cc4..0b511ffd6 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -142,6 +142,7 @@ const AclPageContent = ({
const [userSubmitting, setUserSubmitting] = useState(false);
const [activeTab, setActiveTab] = useState('rules');
const [ruleRefreshKey, setRuleRefreshKey] = useState(0);
+ const [userRefreshKey, setUserRefreshKey] = useState(0);
const [ruleTotal, setRuleTotal] = useState(0);
const [rulePage, setRulePage] = useState(1);
const [rulePageSize, setRulePageSize] = useState(20);
@@ -251,6 +252,7 @@ const AclPageContent = ({
userPage,
userPageSize,
userKeyword,
+ userRefreshKey,
]);
/* ─── Rule helpers ─── */
@@ -417,13 +419,15 @@ const AclPageContent = ({
setUsers((prev) => prev.map((u) => (u.id === editingUser.id ?
normalized : u)));
message.success(t('acl.userUpdated'));
} else {
- const created = await createAclUser({
+ await createAclUser({
username: values.username,
admin: values.admin ?? false,
clusters: values.clusters ?? [],
instanceId: selectedInstanceId,
});
- setUsers((prev) => [normalizeUser(created), ...prev]);
+ // Reload the authoritative server page so the pagination total and
page count
+ // stay consistent with the created row (same refresh pattern as the
rules tab).
+ setUserRefreshKey((prev) => prev + 1);
message.success(t('acl.userAdded'));
}
setUserModalOpen(false);
@@ -438,7 +442,9 @@ const AclPageContent = ({
const handleDeleteUser = async (id: AclEntityId) => {
try {
await deleteAclUser(id, selectedInstanceId);
- setUsers((prev) => prev.filter((u) => u.id !== id));
+ // Reload the authoritative server page so the pagination total follows
the delete
+ // and an emptied last page falls back to the previous one (same as the
rules tab).
+ setUserRefreshKey((prev) => prev + 1);
message.success(t('acl.userDeleted'));
} catch {
message.error(t('common.operationFailed'));