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

commit 22f4550484763472ec973f460eb3784694b3d663
Author: aias00 <[email protected]>
AuthorDate: Thu Jul 23 23:21:37 2026 -0700

    fix: ACL page API integration (#516)
    
    Wire ACL management page to backend API, add update endpoints for ACL rules 
and users with proper validation.
---
 .../studio/instance/acl/AclController.java         |  10 ++
 .../rocketmq/studio/instance/acl/AclService.java   |  25 +++
 .../studio/instance/acl/AclControllerTest.java     |  42 +++++
 .../studio/instance/acl/AclServiceTest.java        |  61 +++++++
 web/src/api/acl.test.ts                            |  55 +++++-
 web/src/api/acl.ts                                 |  12 +-
 web/src/pages/instance/__tests__/AclPage.test.tsx  | 108 ++++++++++++
 web/src/pages/instance/acl.tsx                     | 186 ++++++++++++++++-----
 web/src/services/aclService.ts                     |  26 +++
 9 files changed, 481 insertions(+), 44 deletions(-)

diff --git 
a/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java 
b/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java
index 3d1d83cb..9c35cd6b 100644
--- a/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclController.java
@@ -47,6 +47,11 @@ public class AclController {
         return Result.ok(aclService.createRule(rule));
     }
 
+    @PostMapping("/rules/update")
+    public Result<AclRuleVO> updateRule(@RequestBody AclRuleVO rule) {
+        return Result.ok(aclService.updateRule(rule));
+    }
+
     @PostMapping("/rules/delete")
     public Result<Void> deleteRule(@RequestBody Map<String, String> request) {
         aclService.deleteRule(request.get("id"));
@@ -63,6 +68,11 @@ public class AclController {
         return Result.ok(aclService.createUser(user));
     }
 
+    @PostMapping("/users/update")
+    public Result<AclUserVO> updateUser(@RequestBody AclUserVO user) {
+        return Result.ok(aclService.updateUser(user));
+    }
+
     @PostMapping("/users/delete")
     public Result<Void> deleteUser(@RequestBody Map<String, String> request) {
         aclService.deleteUser(request.get("id"));
diff --git 
a/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java 
b/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java
index 537b81e8..ceff7680 100644
--- a/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java
+++ b/server/src/main/java/com/rocketmq/studio/instance/acl/AclService.java
@@ -16,6 +16,7 @@
  */
 package com.rocketmq.studio.instance.acl;
 
+import com.rocketmq.studio.common.exception.BusinessException;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -45,6 +46,16 @@ public class AclService {
         return aclRepository.saveRule(rule);
     }
 
+    public AclRuleVO updateRule(AclRuleVO rule) {
+        if (isBlank(rule.getId())) {
+            throw new BusinessException(400, "ACL rule id is required");
+        }
+        log.info("Updating ACL rule id={}, principal={}", rule.getId(), 
rule.getPrincipal());
+        if (rule.getCreatedAt() == null) {
+            rule.setCreatedAt(LocalDateTime.now());
+        }
+        return aclRepository.saveRule(rule);
+    }
 
     public void deleteRule(String id) {
         log.info("Deleting ACL rule id={}", id);
@@ -67,9 +78,23 @@ public class AclService {
         return aclRepository.saveUser(user);
     }
 
+    public AclUserVO updateUser(AclUserVO user) {
+        if (isBlank(user.getId())) {
+            throw new BusinessException(400, "ACL user id is required");
+        }
+        log.info("Updating ACL user id={}, username={}", user.getId(), 
user.getUsername());
+        if (user.getCreatedAt() == null) {
+            user.setCreatedAt(LocalDateTime.now());
+        }
+        return aclRepository.saveUser(user);
+    }
 
     public void deleteUser(String id) {
         log.info("Deleting ACL user id={}", id);
         aclRepository.deleteUser(id);
     }
+
+    private boolean isBlank(String value) {
+        return value == null || value.trim().isEmpty();
+    }
 }
diff --git 
a/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java 
b/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java
index 15250820..3db19851 100644
--- 
a/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java
+++ 
b/server/src/test/java/com/rocketmq/studio/instance/acl/AclControllerTest.java
@@ -116,6 +116,27 @@ class AclControllerTest {
                 .andExpect(jsonPath("$.data.principal").value("user1"));
     }
 
+    @Test
+    void updateRuleShouldReturnUpdatedRule() throws Exception {
+        AclRuleVO input = AclRuleVO.builder()
+                .id("rule-1")
+                .principal("user1")
+                .resource("topic-1")
+                .resourceType("TOPIC")
+                .decision("DENY")
+                .build();
+
+        when(aclService.updateRule(any(AclRuleVO.class))).thenReturn(input);
+
+        mockMvc.perform(post("/api/acl/rules/update")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("rule-1"))
+                .andExpect(jsonPath("$.data.decision").value("DENY"));
+    }
+
     @Test
     void listUsersShouldReturnAllUsers() throws Exception {
         AclUserVO user = AclUserVO.builder()
@@ -137,4 +158,25 @@ class AclControllerTest {
                 .andExpect(jsonPath("$.data[0].username").value("admin"))
                 .andExpect(jsonPath("$.data[0].admin").value(true));
     }
+
+    @Test
+    void updateUserShouldReturnUpdatedUser() throws Exception {
+        AclUserVO input = AclUserVO.builder()
+                .id("user-1")
+                .username("admin")
+                .accessKey("ak123")
+                .secretKey("sk456")
+                .admin(false)
+                .build();
+
+        when(aclService.updateUser(any(AclUserVO.class))).thenReturn(input);
+
+        mockMvc.perform(post("/api/acl/users/update")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(objectMapper.writeValueAsString(input)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value("user-1"))
+                .andExpect(jsonPath("$.data.admin").value(false));
+    }
 }
diff --git 
a/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java 
b/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java
index c24a7a23..c27008cb 100644
--- a/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java
+++ b/server/src/test/java/com/rocketmq/studio/instance/acl/AclServiceTest.java
@@ -26,6 +26,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import java.util.List;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -91,6 +92,36 @@ class AclServiceTest {
         verify(aclRepository).deleteRule("rule-1");
     }
 
+    @Test
+    void updateRuleShouldRequireId() {
+        AclRuleVO input = AclRuleVO.builder()
+                .principal("user1")
+                .resource("topic-1")
+                .build();
+
+        assertThatThrownBy(() -> aclService.updateRule(input))
+                .hasMessage("ACL rule id is required");
+    }
+
+    @Test
+    void updateRuleShouldSaveExistingRule() {
+        AclRuleVO input = AclRuleVO.builder()
+                .id("rule-1")
+                .principal("user1")
+                .resource("topic-1")
+                .decision("DENY")
+                .build();
+
+        when(aclRepository.saveRule(any(AclRuleVO.class))).thenAnswer(inv -> 
inv.getArgument(0));
+
+        AclRuleVO result = aclService.updateRule(input);
+
+        assertThat(result.getId()).isEqualTo("rule-1");
+        assertThat(result.getCreatedAt()).isNotNull();
+        assertThat(result.getDecision()).isEqualTo("DENY");
+        verify(aclRepository).saveRule(any(AclRuleVO.class));
+    }
+
     @Test
     void listUsersShouldReturnAllUsers() {
         List<AclUserVO> users = List.of(
@@ -133,4 +164,34 @@ class AclServiceTest {
 
         verify(aclRepository).deleteUser("user-1");
     }
+
+    @Test
+    void updateUserShouldRequireId() {
+        AclUserVO input = AclUserVO.builder()
+                .username("newuser")
+                .build();
+
+        assertThatThrownBy(() -> aclService.updateUser(input))
+                .hasMessage("ACL user id is required");
+    }
+
+    @Test
+    void updateUserShouldSaveExistingUser() {
+        AclUserVO input = AclUserVO.builder()
+                .id("user-1")
+                .username("newuser")
+                .accessKey("ak")
+                .secretKey("sk")
+                .admin(true)
+                .build();
+
+        when(aclRepository.saveUser(any(AclUserVO.class))).thenAnswer(inv -> 
inv.getArgument(0));
+
+        AclUserVO result = aclService.updateUser(input);
+
+        assertThat(result.getId()).isEqualTo("user-1");
+        assertThat(result.getCreatedAt()).isNotNull();
+        assertThat(result.isAdmin()).isTrue();
+        verify(aclRepository).saveUser(any(AclUserVO.class));
+    }
 }
diff --git a/web/src/api/acl.test.ts b/web/src/api/acl.test.ts
index 127a0322..499a6c4c 100644
--- a/web/src/api/acl.test.ts
+++ b/web/src/api/acl.test.ts
@@ -18,7 +18,15 @@
 import MockAdapter from 'axios-mock-adapter';
 import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
 import client from './client';
-import { createAclRule, createAclUser, listAclRules } from './acl';
+import {
+  createAclRule,
+  createAclUser,
+  deleteAclRule,
+  deleteAclUser,
+  listAclRules,
+  updateAclRule,
+  updateAclUser,
+} from './acl';
 
 const mock = new MockAdapter(client);
 
@@ -71,4 +79,49 @@ describe('ACL API contract', () => {
     await expect(createAclRule({ principal: rule.principal 
})).resolves.toEqual(rule);
     await expect(createAclUser({ username: user.username 
})).resolves.toEqual(user);
   });
+
+  it('uses backend update and delete endpoints for rules and users', async () 
=> {
+    const rule = {
+      id: 'rule-1',
+      principal: 'orders',
+      resource: 'orders-*',
+      resourceType: 'Topic',
+      resourcePattern: 'PREFIX',
+      actions: ['SUB'],
+      decision: 'DENY',
+      scope: 'cluster',
+      aclVersion: 2,
+      createdAt: '2026-07-17T00:00:00Z',
+    };
+    const user = {
+      id: 'user-1',
+      username: 'orders',
+      accessKey: 'ak',
+      secretKey: 'sk',
+      admin: true,
+      clusters: ['cluster-a'],
+      createdAt: '2026-07-17T00:00:00Z',
+    };
+    mock.onPost('/acl/rules/update').reply((config) => {
+      expect(JSON.parse(config.data)).toMatchObject({ id: rule.id, decision: 
'DENY' });
+      return [200, { code: 200, data: rule }];
+    });
+    mock.onPost('/acl/users/update').reply((config) => {
+      expect(JSON.parse(config.data)).toMatchObject({ id: user.id, admin: true 
});
+      return [200, { code: 200, data: user }];
+    });
+    mock.onPost('/acl/rules/delete').reply((config) => {
+      expect(JSON.parse(config.data)).toEqual({ id: rule.id });
+      return [200, { code: 200 }];
+    });
+    mock.onPost('/acl/users/delete').reply((config) => {
+      expect(JSON.parse(config.data)).toEqual({ id: user.id });
+      return [200, { code: 200 }];
+    });
+
+    await expect(updateAclRule({ id: rule.id, decision: 'DENY' 
})).resolves.toEqual(rule);
+    await expect(updateAclUser({ id: user.id, admin: true 
})).resolves.toEqual(user);
+    await expect(deleteAclRule(rule.id)).resolves.toBeUndefined();
+    await expect(deleteAclUser(user.id)).resolves.toBeUndefined();
+  });
 });
diff --git a/web/src/api/acl.ts b/web/src/api/acl.ts
index bc6ce955..14ab320c 100644
--- a/web/src/api/acl.ts
+++ b/web/src/api/acl.ts
@@ -10,7 +10,7 @@ export interface AclRule {
   actions: string[];
   decision: string;
   scope: string;
-  aclVersion: number;
+  aclVersion: number | string;
   createdAt: string;
 }
 
@@ -39,6 +39,11 @@ export async function createAclRule(data: Partial<AclRule>) {
   return res.data.data;
 }
 
+export async function updateAclRule(data: Partial<AclRule>) {
+  const res = await client.post<{ data: AclRule }>('/acl/rules/update', data);
+  return res.data.data;
+}
+
 export async function deleteAclRule(id: string) {
   await client.post('/acl/rules/delete', { id });
 }
@@ -53,6 +58,11 @@ export async function createAclUser(data: Partial<AclUser>) {
   return res.data.data;
 }
 
+export async function updateAclUser(data: Partial<AclUser>) {
+  const res = await client.post<{ data: AclUser }>('/acl/users/update', data);
+  return res.data.data;
+}
+
 export async function deleteAclUser(id: string) {
   await client.post('/acl/users/delete', { id });
 }
diff --git a/web/src/pages/instance/__tests__/AclPage.test.tsx 
b/web/src/pages/instance/__tests__/AclPage.test.tsx
new file mode 100644
index 00000000..e25de3d0
--- /dev/null
+++ b/web/src/pages/instance/__tests__/AclPage.test.tsx
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+
+import { App } from 'antd';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import type React from 'react';
+import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
+import { LangProvider } from '../../../i18n/LangContext';
+import * as aclService from '../../../services/aclService';
+import AclPage from '../acl';
+
+vi.mock('../../../services/aclService', () => ({
+  createAclRule: vi.fn(),
+  createAclUser: vi.fn(),
+  deleteAclRule: vi.fn(),
+  deleteAclUser: vi.fn(),
+  listAclRules: vi.fn(),
+  listAclUsers: vi.fn(),
+  updateAclRule: vi.fn(),
+  updateAclUser: vi.fn(),
+}));
+
+beforeAll(() => {
+  Object.defineProperty(window, 'matchMedia', {
+    writable: true,
+    value: vi.fn().mockImplementation((query: string) => ({
+      matches: false,
+      media: query,
+      onchange: null,
+      addListener: vi.fn(),
+      removeListener: vi.fn(),
+      addEventListener: vi.fn(),
+      removeEventListener: vi.fn(),
+      dispatchEvent: vi.fn(),
+    })),
+  });
+});
+
+const renderWithProviders = (ui: React.ReactElement) =>
+  render(
+    <App>
+      <LangProvider>{ui}</LangProvider>
+    </App>,
+  );
+
+describe('ACL page', () => {
+  beforeEach(() => {
+    vi.mocked(aclService.listAclRules).mockResolvedValue([
+      {
+        id: 'rule-remote',
+        principal: 'remote-user',
+        resource: 'remote-topic',
+        resourceType: 'Topic',
+        resourcePattern: 'LITERAL',
+        actions: ['PUB'],
+        decision: 'ALLOW',
+        scope: 'cluster',
+        aclVersion: 2,
+        createdAt: '2026-07-23T00:00:00Z',
+      },
+    ]);
+    vi.mocked(aclService.listAclUsers).mockResolvedValue([
+      {
+        id: 'user-remote',
+        username: 'remote-admin',
+        accessKey: 'ak-remote',
+        secretKey: 'sk-remote',
+        admin: true,
+        clusters: ['cluster-a'],
+        createdAt: '2026-07-23T00:00:00Z',
+      },
+    ]);
+  });
+
+  it('loads ACL rules and users through the service layer', async () => {
+    renderWithProviders(<AclPage />);
+
+    expect(await screen.findByText('remote-user')).toBeInTheDocument();
+    expect(screen.getByText('remote-topic')).toBeInTheDocument();
+    expect(aclService.listAclRules).toHaveBeenCalledTimes(1);
+    expect(aclService.listAclUsers).toHaveBeenCalledTimes(1);
+  });
+
+  it('renders backend users on the user tab', async () => {
+    const user = userEvent.setup();
+    renderWithProviders(<AclPage />);
+
+    await user.click(await screen.findByText('用户管理'));
+
+    expect(await screen.findByText('remote-admin')).toBeInTheDocument();
+    expect(screen.getByText('cluster-a')).toBeInTheDocument();
+  });
+});
diff --git a/web/src/pages/instance/acl.tsx b/web/src/pages/instance/acl.tsx
index a663ecd7..97295085 100644
--- a/web/src/pages/instance/acl.tsx
+++ b/web/src/pages/instance/acl.tsx
@@ -15,7 +15,7 @@
  * limitations under the License.
  */
 
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import {
   Table,
   Card,
@@ -40,8 +40,49 @@ import { EditOutlined, DeleteOutlined } from 
'@ant-design/icons';
 import type { ColumnsType } from 'antd/es/table';
 import PageHeader from '../../components/PageHeader';
 import { useLang } from '../../i18n/LangContext';
-import { aclRules, aclUsers } from '../../mock/acl';
-import type { AclRule, AclUser } from '../../mock/acl';
+import {
+  createAclRule,
+  createAclUser,
+  deleteAclRule,
+  deleteAclUser,
+  listAclRules,
+  listAclUsers,
+  updateAclRule,
+  updateAclUser,
+} from '../../services/aclService';
+import type { AclRule, AclUser } from '../../api/acl';
+
+type AclRuleFormValues = Pick<
+  AclRule,
+  'principal' | 'resource' | 'resourceType' | 'resourcePattern' | 'actions' | 
'decision' | 'scope'
+>;
+type AclUserFormValues = Pick<AclUser, 'username' | 'accessKey' | 'secretKey' 
| 'admin'>;
+
+const normalizeRule = (rule: AclRule): AclRule => ({
+  ...rule,
+  principal: rule.principal ?? '',
+  resource: rule.resource ?? '',
+  resourceType: rule.resourceType ?? '',
+  resourcePattern: rule.resourcePattern ?? '',
+  actions: rule.actions ?? [],
+  decision: rule.decision ?? '',
+  scope: rule.scope ?? '',
+  aclVersion: rule.aclVersion ?? '2.0',
+  createdAt: rule.createdAt ?? new Date().toISOString(),
+});
+
+const normalizeUser = (user: AclUser): AclUser => ({
+  ...user,
+  username: user.username ?? '',
+  accessKey: user.accessKey ?? '',
+  secretKey: user.secretKey ?? '',
+  admin: user.admin ?? false,
+  clusters: user.clusters ?? [],
+  createdAt: user.createdAt ?? new Date().toISOString(),
+});
+
+const isFormValidationError = (error: unknown) =>
+  typeof error === 'object' && error !== null && 'errorFields' in error;
 
 /* ═══════════════════════════════════════════
    ACL Management Page
@@ -50,8 +91,12 @@ const AclPage = () => {
   const { t } = useLang();
 
   /* ─── State ─── */
-  const [rules, setRules] = useState<AclRule[]>(aclRules);
-  const [users, setUsers] = useState<AclUser[]>(aclUsers);
+  const [rules, setRules] = useState<AclRule[]>([]);
+  const [users, setUsers] = useState<AclUser[]>([]);
+  const [rulesLoading, setRulesLoading] = useState(true);
+  const [usersLoading, setUsersLoading] = useState(true);
+  const [ruleSubmitting, setRuleSubmitting] = useState(false);
+  const [userSubmitting, setUserSubmitting] = useState(false);
   const [activeTab, setActiveTab] = useState('rules');
 
   // Rule filters
@@ -72,13 +117,37 @@ const AclPage = () => {
   // Secret key reveal
   const [revealedKeys, setRevealedKeys] = useState<Set<string>>(new Set());
 
+  useEffect(() => {
+    let mounted = true;
+
+    Promise.all([listAclRules(), listAclUsers()])
+      .then(([nextRules, nextUsers]) => {
+        if (!mounted) return;
+        setRules(nextRules.map(normalizeRule));
+        setUsers(nextUsers.map(normalizeUser));
+      })
+      .catch(() => {
+        if (mounted) message.error(t('common.fetchDataFailed'));
+      })
+      .finally(() => {
+        if (!mounted) return;
+        setRulesLoading(false);
+        setUsersLoading(false);
+      });
+
+    return () => {
+      mounted = false;
+    };
+  }, [t]);
+
   /* ─── Filtered rules ─── */
   const filteredRules = rules.filter((r) => {
+    const aclVersion = String(r.aclVersion);
     const matchSearch =
       !ruleSearch ||
       r.principal.toLowerCase().includes(ruleSearch.toLowerCase()) ||
       r.resource.toLowerCase().includes(ruleSearch.toLowerCase());
-    const matchVersion = ruleVersionFilter === 'all' || r.aclVersion === 
ruleVersionFilter;
+    const matchVersion = ruleVersionFilter === 'all' || aclVersion === 
ruleVersionFilter;
     const matchDecision = ruleDecisionFilter === 'all' || r.decision === 
ruleDecisionFilter;
     return matchSearch && matchVersion && matchDecision;
   });
@@ -125,28 +194,40 @@ const AclPage = () => {
     setRuleModalOpen(true);
   };
 
-  const handleRuleSubmit = () => {
-    ruleForm.validateFields().then((values) => {
+  const handleRuleSubmit = async () => {
+    try {
+      const values = (await ruleForm.validateFields()) as AclRuleFormValues;
+      setRuleSubmitting(true);
       if (editingRule) {
-        setRules((prev) => prev.map((r) => (r.id === editingRule.id ? { ...r, 
...values } : r)));
+        const updated = await updateAclRule({ ...editingRule, ...values });
+        const normalized = normalizeRule(updated);
+        setRules((prev) => prev.map((r) => (r.id === editingRule.id ? 
normalized : r)));
         message.success(t('acl.ruleUpdated'));
       } else {
-        const newRule: AclRule = {
-          id: `acl-${Date.now()}`,
+        const created = await createAclRule({
           ...values,
-          aclVersion: '2.0',
-          createdAt: new Date().toISOString(),
-        };
-        setRules((prev) => [newRule, ...prev]);
+          aclVersion: 2,
+        });
+        setRules((prev) => [normalizeRule(created), ...prev]);
         message.success(t('acl.ruleAdded'));
       }
       setRuleModalOpen(false);
-    });
+    } catch (error) {
+      if (isFormValidationError(error)) return;
+      message.error(t('common.operationFailed'));
+    } finally {
+      setRuleSubmitting(false);
+    }
   };
 
-  const handleDeleteRule = (id: string) => {
-    setRules((prev) => prev.filter((r) => r.id !== id));
-    message.success(t('acl.ruleDeleted'));
+  const handleDeleteRule = async (id: string) => {
+    try {
+      await deleteAclRule(id);
+      setRules((prev) => prev.filter((r) => r.id !== id));
+      message.success(t('acl.ruleDeleted'));
+    } catch {
+      message.error(t('common.operationFailed'));
+    }
   };
 
   /* ─── User helpers ─── */
@@ -180,42 +261,59 @@ const AclPage = () => {
     setUserModalOpen(true);
   };
 
-  const handleUserSubmit = () => {
-    userForm.validateFields().then((values) => {
+  const handleUserSubmit = async () => {
+    try {
+      const values = (await userForm.validateFields()) as AclUserFormValues;
+      setUserSubmitting(true);
       if (editingUser) {
-        setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? { ...u, 
...values } : u)));
+        const updated = await updateAclUser({ ...editingUser, ...values });
+        const normalized = normalizeUser(updated);
+        setUsers((prev) => prev.map((u) => (u.id === editingUser.id ? 
normalized : u)));
         message.success(t('acl.userUpdated'));
       } else {
-        const newUser: AclUser = {
-          id: `u-${Date.now()}`,
+        const created = await createAclUser({
           username: values.username,
-          accessKey: values.accessKey || 
`LTAI****${values.username.slice(-4)}`,
-          secretKey:
-            values.secretKey ||
-            `${Math.random().toString(36).slice(2, 
6)}****${Math.random().toString(36).slice(2, 6)}`,
+          accessKey: values.accessKey,
+          secretKey: values.secretKey,
           admin: values.admin ?? false,
           clusters: ['rmq-cn-v5-prod-01'],
-          createdAt: new Date().toISOString(),
-        };
-        setUsers((prev) => [newUser, ...prev]);
+        });
+        setUsers((prev) => [normalizeUser(created), ...prev]);
         message.success(t('acl.userAdded'));
       }
       setUserModalOpen(false);
-    });
+    } catch (error) {
+      if (isFormValidationError(error)) return;
+      message.error(t('common.operationFailed'));
+    } finally {
+      setUserSubmitting(false);
+    }
   };
 
-  const handleDeleteUser = (id: string) => {
-    setUsers((prev) => prev.filter((u) => u.id !== id));
-    message.success(t('acl.userDeleted'));
+  const handleDeleteUser = async (id: string) => {
+    try {
+      await deleteAclUser(id);
+      setUsers((prev) => prev.filter((u) => u.id !== id));
+      message.success(t('acl.userDeleted'));
+    } catch {
+      message.error(t('common.operationFailed'));
+    }
   };
 
-  const handleToggleAdmin = (userId: string, checked: boolean) => {
-    setUsers((prev) => prev.map((u) => (u.id === userId ? { ...u, admin: 
checked } : u)));
-    message.success(checked ? t('acl.adminSet') : t('acl.adminRemoved'));
+  const handleToggleAdmin = async (user: AclUser, checked: boolean) => {
+    try {
+      const updated = await updateAclUser({ ...user, admin: checked });
+      const normalized = normalizeUser(updated);
+      setUsers((prev) => prev.map((u) => (u.id === user.id ? normalized : u)));
+      message.success(checked ? t('acl.adminSet') : t('acl.adminRemoved'));
+    } catch {
+      message.error(t('common.operationFailed'));
+    }
   };
 
   const formatDate = (iso: string) => {
     const d = new Date(iso);
+    if (Number.isNaN(d.getTime())) return '-';
     return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, 
'0')}-${String(d.getDate()).padStart(2, '0')} 
${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, 
'0')}`;
   };
 
@@ -291,9 +389,9 @@ const AclPage = () => {
       dataIndex: 'aclVersion',
       key: 'aclVersion',
       width: 100,
-      sorter: (a, b) => a.aclVersion.localeCompare(b.aclVersion),
-      render: (version: string) => (
-        <Tag color={version === '2.0' ? 'geekblue' : 'default'}>{version}</Tag>
+      sorter: (a, b) => 
String(a.aclVersion).localeCompare(String(b.aclVersion)),
+      render: (version: AclRule['aclVersion']) => (
+        <Tag color={String(version) === '2.0' ? 'geekblue' : 
'default'}>{version}</Tag>
       ),
     },
     {
@@ -409,7 +507,7 @@ const AclPage = () => {
         <Switch
           checked={val}
           size="small"
-          onChange={(checked) => handleToggleAdmin(record.id, checked)}
+          onChange={(checked) => handleToggleAdmin(record, checked)}
         />
       ),
     },
@@ -554,6 +652,7 @@ const AclPage = () => {
                     columns={ruleColumns}
                     dataSource={filteredRules}
                     rowKey="id"
+                    loading={rulesLoading}
                     pagination={{
                       pageSize: 20,
                       showSizeChanger: true,
@@ -590,6 +689,7 @@ const AclPage = () => {
                     columns={userColumns}
                     dataSource={users}
                     rowKey="id"
+                    loading={usersLoading}
                     pagination={{
                       pageSize: 20,
                       showSizeChanger: true,
@@ -612,6 +712,7 @@ const AclPage = () => {
         onOk={handleRuleSubmit}
         okText={editingRule ? t('acl.save') : t('acl.add')}
         cancelText={t('common.cancel')}
+        confirmLoading={ruleSubmitting}
         width={560}
         destroyOnClose
       >
@@ -713,6 +814,7 @@ const AclPage = () => {
         onOk={handleUserSubmit}
         okText={editingUser ? t('acl.save') : t('acl.add')}
         cancelText={t('common.cancel')}
+        confirmLoading={userSubmitting}
         width={520}
         destroyOnClose
       >
diff --git a/web/src/services/aclService.ts b/web/src/services/aclService.ts
index d8962d20..3ef7b6e0 100644
--- a/web/src/services/aclService.ts
+++ b/web/src/services/aclService.ts
@@ -51,6 +51,19 @@ export async function createAclRule(data: Partial<AclRule>): 
Promise<AclRule> {
   return aclApi.createAclRule(data);
 }
 
+export async function updateAclRule(data: Partial<AclRule>): Promise<AclRule> {
+  if (USE_MOCK) {
+    const idx = aclRulesState.findIndex((rule) => rule.id === data.id);
+    if (idx < 0) throw new Error(`ACL rule not found: ${data.id}`);
+    aclRulesState[idx] = {
+      ...aclRulesState[idx],
+      ...data,
+    };
+    return aclRulesState[idx];
+  }
+  return aclApi.updateAclRule(data);
+}
+
 export async function deleteAclRule(id: string): Promise<void> {
   if (USE_MOCK) {
     const idx = aclRulesState.findIndex((rule) => rule.id === id);
@@ -78,6 +91,19 @@ export async function createAclUser(data: Partial<AclUser>): 
Promise<AclUser> {
   return aclApi.createAclUser(data);
 }
 
+export async function updateAclUser(data: Partial<AclUser>): Promise<AclUser> {
+  if (USE_MOCK) {
+    const idx = aclUsersState.findIndex((user) => user.id === data.id);
+    if (idx < 0) throw new Error(`ACL user not found: ${data.id}`);
+    aclUsersState[idx] = {
+      ...aclUsersState[idx],
+      ...data,
+    };
+    return aclUsersState[idx];
+  }
+  return aclApi.updateAclUser(data);
+}
+
 export async function deleteAclUser(id: string): Promise<void> {
   if (USE_MOCK) {
     const idx = aclUsersState.findIndex((user) => user.id === id);

Reply via email to