plugin-cluster-manager 1.1.10 → 1.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/client.js +1 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/client-v2/914.5dc1105cf3ada6a6.js +10 -0
  6. package/dist/client-v2/index.js +10 -0
  7. package/dist/externalVersion.js +6 -5
  8. package/dist/locale/en-US.json +138 -28
  9. package/dist/locale/vi-VN.json +139 -28
  10. package/dist/locale/zh-CN.json +140 -28
  11. package/dist/server/actions/cache-monitor.js +301 -0
  12. package/dist/server/actions/cluster-nodes.js +391 -11
  13. package/dist/server/actions/doctor.js +1246 -0
  14. package/dist/server/actions/orchestrator.js +37 -0
  15. package/dist/server/actions/queue-mappings.js +107 -0
  16. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  17. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  18. package/dist/server/collections/worker-queue-mappings.js +106 -0
  19. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  20. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  21. package/dist/server/orchestrator/PackageManager.js +21 -24
  22. package/dist/server/orchestrator/docker-adapter.js +49 -27
  23. package/dist/server/plugin.js +71 -16
  24. package/dist/server/queue-scanner.js +141 -0
  25. package/dist/server/utils/node.js +30 -2
  26. package/dist/server/utils/versionManager.js +91 -0
  27. package/package.json +9 -5
  28. package/server.js +1 -0
  29. package/src/client/AclCacheManager.tsx +292 -287
  30. package/src/client/CacheMonitor.tsx +166 -179
  31. package/src/client/ClusterManagerLayout.tsx +54 -42
  32. package/src/client/ClusterNodes.tsx +698 -418
  33. package/src/client/ContainerOrchestrator.tsx +184 -102
  34. package/src/client/Doctor.tsx +559 -0
  35. package/src/client/NginxCacheManager.tsx +415 -0
  36. package/src/client/PluginOperations.tsx +234 -234
  37. package/src/client/QueueAssignment.tsx +355 -0
  38. package/src/client/TaskManager.tsx +194 -187
  39. package/src/client/WorkflowExecutions.tsx +243 -238
  40. package/src/client/index.tsx +22 -14
  41. package/src/client/utils/clientSafeCache.ts +41 -0
  42. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  43. package/src/client-v2/plugin.tsx +24 -0
  44. package/src/locale/en-US.json +138 -28
  45. package/src/locale/vi-VN.json +139 -28
  46. package/src/locale/zh-CN.json +140 -28
  47. package/src/server/__tests__/doctor.test.ts +53 -0
  48. package/src/server/actions/acl-cache.ts +272 -272
  49. package/src/server/actions/cache-monitor.ts +453 -116
  50. package/src/server/actions/cluster-nodes.ts +878 -378
  51. package/src/server/actions/doctor.ts +1536 -0
  52. package/src/server/actions/orchestrator.ts +54 -2
  53. package/src/server/actions/queue-mappings.ts +94 -0
  54. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  55. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  56. package/src/server/collections/worker-queue-mappings.ts +85 -0
  57. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  58. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  59. package/src/server/orchestrator/PackageManager.ts +20 -24
  60. package/src/server/orchestrator/docker-adapter.ts +74 -37
  61. package/src/server/plugin.ts +347 -270
  62. package/src/server/queue-scanner.ts +154 -0
  63. package/src/server/utils/node.ts +48 -0
  64. package/src/server/utils/versionManager.ts +69 -0
  65. package/dist/client/AclCacheManager.d.ts +0 -2
  66. package/dist/client/CacheMonitor.d.ts +0 -2
  67. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  68. package/dist/client/ClusterNodes.d.ts +0 -2
  69. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  70. package/dist/client/EventQueueMonitor.d.ts +0 -2
  71. package/dist/client/LockMonitor.d.ts +0 -2
  72. package/dist/client/PackageInstaller.d.ts +0 -2
  73. package/dist/client/PluginOperations.d.ts +0 -2
  74. package/dist/client/RedisMonitor.d.ts +0 -2
  75. package/dist/client/TaskManager.d.ts +0 -2
  76. package/dist/client/WorkflowExecutions.d.ts +0 -2
  77. package/dist/client/index.d.ts +0 -5
  78. package/dist/client/utils.d.ts +0 -12
  79. package/dist/index.d.ts +0 -2
  80. package/dist/server/actions/acl-cache.d.ts +0 -53
  81. package/dist/server/actions/cache-monitor.d.ts +0 -23
  82. package/dist/server/actions/cluster-nodes.d.ts +0 -49
  83. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  84. package/dist/server/actions/lock-monitor.d.ts +0 -19
  85. package/dist/server/actions/orchestrator.d.ts +0 -58
  86. package/dist/server/actions/package-manager.d.ts +0 -6
  87. package/dist/server/actions/plugin-operations.d.ts +0 -6
  88. package/dist/server/actions/redis-monitor.d.ts +0 -12
  89. package/dist/server/actions/tasks.d.ts +0 -7
  90. package/dist/server/actions/workflow-executions.d.ts +0 -7
  91. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  92. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  93. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  94. package/dist/server/collections/app.d.ts +0 -8
  95. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  96. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  97. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  98. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  99. package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
  100. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  101. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  102. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  103. package/dist/server/collections/cluster-manager.d.ts +0 -22
  104. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  105. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  106. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  107. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  108. package/dist/server/collections/worker-packages.d.ts +0 -22
  109. package/dist/server/orchestrator/PackageManager.d.ts +0 -39
  110. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  111. package/dist/server/orchestrator/index.d.ts +0 -4
  112. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  113. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  114. package/dist/server/orchestrator/types.d.ts +0 -84
  115. package/dist/server/plugin.d.ts +0 -26
  116. package/dist/server/utils/node.d.ts +0 -6
  117. package/dist/server/utils/redis.d.ts +0 -29
  118. package/dist/shared/packages.d.ts +0 -23
  119. /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -1,287 +1,292 @@
1
- import React, { useEffect, useState, useCallback } from 'react';
2
- import {
3
- Card,
4
- Row,
5
- Col,
6
- Statistic,
7
- Table,
8
- Tag,
9
- Button,
10
- Space,
11
- Popconfirm,
12
- message,
13
- Typography,
14
- Select,
15
- } from 'antd';
16
- import {
17
- ReloadOutlined,
18
- DeleteOutlined,
19
- SafetyCertificateOutlined,
20
- ThunderboltOutlined,
21
- CheckCircleOutlined,
22
- CloseCircleOutlined,
23
- } from '@ant-design/icons';
24
- import { useAPIClient } from '@nocobase/client';
25
- import { useT } from './utils';
26
-
27
- const { Text } = Typography;
28
-
29
- interface AclStats {
30
- totalChecks: number;
31
- cacheHits: number;
32
- cacheMisses: number;
33
- hitRate: number;
34
- cachedKeys: number;
35
- ttlSeconds: number;
36
- startedAt: string;
37
- detailByRole: Record<string, { checks: number; hits: number; misses: number }>;
38
- }
39
-
40
- interface CachedKey {
41
- key: string;
42
- role: string;
43
- resource: string;
44
- action: string;
45
- }
46
-
47
- export function AclCacheManager() {
48
- const api = useAPIClient();
49
- const t = useT();
50
- const [stats, setStats] = useState<AclStats | null>(null);
51
- const [keys, setKeys] = useState<CachedKey[]>([]);
52
- const [loading, setLoading] = useState(false);
53
- const [autoRefresh, setAutoRefresh] = useState<number | null>(null);
54
-
55
- const fetchAll = useCallback(async () => {
56
- setLoading(true);
57
- try {
58
- const [statsRes, keysRes] = await Promise.all([
59
- api.request({ url: 'clusterManagerAclCache:stats' }),
60
- api.request({ url: 'clusterManagerAclCache:listKeys' }),
61
- ]);
62
- const statsData = statsRes?.data?.data || statsRes?.data || {};
63
- setStats(statsData);
64
- const keysArray = Array.isArray(keysRes?.data?.data?.data) ? keysRes.data.data.data : Array.isArray(keysRes?.data?.data) ? keysRes.data.data : Array.isArray(keysRes?.data) ? keysRes.data : [];
65
- setKeys(keysArray);
66
- } catch {
67
- message.error('Failed to load ACL cache data');
68
- } finally {
69
- setLoading(false);
70
- }
71
- }, [api]);
72
-
73
- useEffect(() => {
74
- fetchAll();
75
- }, []);
76
-
77
- useEffect(() => {
78
- if (!autoRefresh) return;
79
- const timer = setInterval(fetchAll, autoRefresh * 1000);
80
- return () => clearInterval(timer);
81
- }, [autoRefresh, fetchAll]);
82
-
83
- const handleClearAll = async () => {
84
- try {
85
- const res = await api.request({ url: 'clusterManagerAclCache:clear', method: 'post' });
86
- message.success(`Cleared ${res.data?.deletedCount || 0} cache entries`);
87
- fetchAll();
88
- } catch {
89
- message.error('Failed to clear cache');
90
- }
91
- };
92
-
93
- const handleResetStats = async () => {
94
- try {
95
- await api.request({ url: 'clusterManagerAclCache:resetStats', method: 'post' });
96
- message.success('Stats reset');
97
- fetchAll();
98
- } catch {
99
- message.error('Failed to reset stats');
100
- }
101
- };
102
-
103
- const handleClearRole = async (roleName: string) => {
104
- try {
105
- const res = await api.request({
106
- url: 'clusterManagerAclCache:clearRole',
107
- method: 'post',
108
- data: { roleName },
109
- });
110
- message.success(`Cleared ${res.data?.deletedCount || 0} entries for role "${roleName}"`);
111
- fetchAll();
112
- } catch {
113
- message.error('Failed to clear role cache');
114
- }
115
- };
116
-
117
- if (!stats) return null;
118
-
119
- const detailByRole = stats.detailByRole || {};
120
- const roleData = Object.entries(detailByRole).map(([role, data]) => ({
121
- role,
122
- ...data,
123
- hitRate: data.checks > 0 ? Math.round((data.hits / data.checks) * 10000) / 100 : 0,
124
- }));
125
-
126
- const keyColumns = [
127
- { title: t('Role'), dataIndex: 'role', width: 120, filters: [...new Set(keys.map((k) => k.role))].map((r) => ({ text: r, value: r })), onFilter: (value: any, record: CachedKey) => record.role === value },
128
- { title: t('Resource'), dataIndex: 'resource', width: 200 },
129
- { title: t('Action'), dataIndex: 'action', width: 120 },
130
- {
131
- title: t('Cache Key'),
132
- dataIndex: 'key',
133
- ellipsis: true,
134
- render: (val: string) => <Text code style={{ fontSize: 11 }}>{val}</Text>,
135
- },
136
- ];
137
-
138
- const roleColumns = [
139
- { title: t('Role'), dataIndex: 'role', width: 150 },
140
- { title: t('Checks'), dataIndex: 'checks', width: 100, sorter: (a: any, b: any) => a.checks - b.checks },
141
- {
142
- title: t('Hits'),
143
- dataIndex: 'hits',
144
- width: 100,
145
- render: (val: number) => <Text type="success">{val}</Text>,
146
- },
147
- {
148
- title: t('Misses'),
149
- dataIndex: 'misses',
150
- width: 100,
151
- render: (val: number) => <Text type="danger">{val}</Text>,
152
- },
153
- {
154
- title: t('Hit Rate'),
155
- dataIndex: 'hitRate',
156
- width: 100,
157
- render: (val: number) => (
158
- <Tag color={val > 80 ? 'green' : val > 50 ? 'orange' : 'red'}>{val}%</Tag>
159
- ),
160
- sorter: (a: any, b: any) => a.hitRate - b.hitRate,
161
- },
162
- {
163
- title: t('Actions'),
164
- width: 80,
165
- render: (_: any, record: any) => (
166
- <Popconfirm title={`${t('Clear cache for role')} "${record.role}"?`} onConfirm={() => handleClearRole(record.role)}>
167
- <Button type="link" size="small" icon={<DeleteOutlined />} danger />
168
- </Popconfirm>
169
- ),
170
- },
171
- ];
172
-
173
- return (
174
- <div>
175
- <Space style={{ marginBottom: 16 }}>
176
- <Button icon={<ReloadOutlined />} onClick={fetchAll} loading={loading}>
177
- {t('Refresh')}
178
- </Button>
179
- <Select
180
- placeholder={t('Auto refresh')}
181
- allowClear
182
- style={{ width: 160 }}
183
- value={autoRefresh}
184
- onChange={setAutoRefresh}
185
- options={[
186
- { value: 5, label: t('Every 5s') },
187
- { value: 10, label: t('Every 10s') },
188
- { value: 30, label: t('Every 30s') },
189
- ]}
190
- />
191
- <Popconfirm title={t('Clear all ACL cache entries?')} onConfirm={handleClearAll}>
192
- <Button icon={<DeleteOutlined />} danger>
193
- {t('Clear All Cache')}
194
- </Button>
195
- </Popconfirm>
196
- <Popconfirm title={t('Reset ACL stats counters?')} onConfirm={handleResetStats}>
197
- <Button>{t('Reset Stats')}</Button>
198
- </Popconfirm>
199
- <Tag>TTL: {stats.ttlSeconds}s</Tag>
200
- <Tag>{t('Since')}: {new Date(stats.startedAt).toLocaleString()}</Tag>
201
- </Space>
202
-
203
- {/* Overview stats */}
204
- <Row gutter={[16, 16]}>
205
- <Col span={6}>
206
- <Card size="small">
207
- <Statistic
208
- title={t('Total ACL Checks')}
209
- value={stats.totalChecks}
210
- prefix={<SafetyCertificateOutlined />}
211
- />
212
- </Card>
213
- </Col>
214
- <Col span={6}>
215
- <Card size="small">
216
- <Statistic
217
- title={t('Cache Hits')}
218
- value={stats.cacheHits}
219
- prefix={<CheckCircleOutlined />}
220
- valueStyle={{ color: '#3f8600' }}
221
- />
222
- </Card>
223
- </Col>
224
- <Col span={6}>
225
- <Card size="small">
226
- <Statistic
227
- title={t('Cache Misses')}
228
- value={stats.cacheMisses}
229
- prefix={<CloseCircleOutlined />}
230
- valueStyle={{ color: '#cf1322' }}
231
- />
232
- </Card>
233
- </Col>
234
- <Col span={6}>
235
- <Card size="small">
236
- <Statistic
237
- title={t('Hit Rate')}
238
- value={stats.hitRate}
239
- suffix="%"
240
- prefix={<ThunderboltOutlined />}
241
- valueStyle={{
242
- color: stats.hitRate > 80 ? '#3f8600' : stats.hitRate > 50 ? '#faad14' : '#cf1322',
243
- }}
244
- />
245
- <Text type="secondary">{t('Cached keys')}: {stats.cachedKeys}</Text>
246
- </Card>
247
- </Col>
248
- </Row>
249
-
250
- {/* Per-role breakdown */}
251
- <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
252
- <Col span={12}>
253
- <Card title={t('Stats by Role')} size="small">
254
- {roleData.length === 0 ? (
255
- <Text type="secondary">{t('No ACL checks recorded yet')}</Text>
256
- ) : (
257
- <Table
258
- rowKey="role"
259
- size="small"
260
- pagination={false}
261
- dataSource={roleData}
262
- columns={roleColumns}
263
- />
264
- )}
265
- </Card>
266
- </Col>
267
-
268
- {/* Cached keys */}
269
- <Col span={12}>
270
- <Card title={`${t('Cached Permission Keys')} (${keys.length})`} size="small">
271
- {keys.length === 0 ? (
272
- <Text type="secondary">{t('No cached keys')}</Text>
273
- ) : (
274
- <Table
275
- rowKey="key"
276
- size="small"
277
- pagination={{ pageSize: 10, size: 'small' }}
278
- dataSource={keys}
279
- columns={keyColumns}
280
- />
281
- )}
282
- </Card>
283
- </Col>
284
- </Row>
285
- </div>
286
- );
287
- }
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { Card, Row, Col, Statistic, Table, Tag, Button, Space, Popconfirm, message, Typography, Select } from 'antd';
3
+ import {
4
+ ReloadOutlined,
5
+ DeleteOutlined,
6
+ SafetyCertificateOutlined,
7
+ ThunderboltOutlined,
8
+ CheckCircleOutlined,
9
+ CloseCircleOutlined,
10
+ } from '@ant-design/icons';
11
+ import { useAPIClient } from '@nocobase/client';
12
+ import { useT } from './utils';
13
+
14
+ const { Text } = Typography;
15
+
16
+ interface AclStats {
17
+ totalChecks: number;
18
+ cacheHits: number;
19
+ cacheMisses: number;
20
+ hitRate: number;
21
+ cachedKeys: number;
22
+ ttlSeconds: number;
23
+ startedAt: string;
24
+ detailByRole: Record<string, { checks: number; hits: number; misses: number }>;
25
+ }
26
+
27
+ interface CachedKey {
28
+ key: string;
29
+ role: string;
30
+ resource: string;
31
+ action: string;
32
+ }
33
+
34
+ export function AclCacheManager() {
35
+ const api = useAPIClient();
36
+ const t = useT();
37
+ const [stats, setStats] = useState<AclStats | null>(null);
38
+ const [keys, setKeys] = useState<CachedKey[]>([]);
39
+ const [loading, setLoading] = useState(false);
40
+ const [autoRefresh, setAutoRefresh] = useState<number | null>(null);
41
+
42
+ const fetchAll = useCallback(async () => {
43
+ setLoading(true);
44
+ try {
45
+ const [statsRes, keysRes] = await Promise.all([
46
+ api.request({ url: 'clusterManagerAclCache:stats' }),
47
+ api.request({ url: 'clusterManagerAclCache:listKeys' }),
48
+ ]);
49
+ const statsData = statsRes?.data?.data || statsRes?.data || {};
50
+ setStats(statsData);
51
+ const keysArray = Array.isArray(keysRes?.data?.data?.data)
52
+ ? keysRes.data.data.data
53
+ : Array.isArray(keysRes?.data?.data)
54
+ ? keysRes.data.data
55
+ : Array.isArray(keysRes?.data)
56
+ ? keysRes.data
57
+ : [];
58
+ setKeys(keysArray);
59
+ } catch {
60
+ message.error('Failed to load ACL cache data');
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ }, [api]);
65
+
66
+ useEffect(() => {
67
+ fetchAll();
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ if (!autoRefresh) return;
72
+ const timer = setInterval(fetchAll, autoRefresh * 1000);
73
+ return () => clearInterval(timer);
74
+ }, [autoRefresh, fetchAll]);
75
+
76
+ const handleClearAll = async () => {
77
+ try {
78
+ const res = await api.request({ url: 'clusterManagerAclCache:clear', method: 'post' });
79
+ message.success(`Cleared ${res.data?.deletedCount || 0} cache entries`);
80
+ fetchAll();
81
+ } catch {
82
+ message.error('Failed to clear cache');
83
+ }
84
+ };
85
+
86
+ const handleResetStats = async () => {
87
+ try {
88
+ await api.request({ url: 'clusterManagerAclCache:resetStats', method: 'post' });
89
+ message.success('Stats reset');
90
+ fetchAll();
91
+ } catch {
92
+ message.error('Failed to reset stats');
93
+ }
94
+ };
95
+
96
+ const handleClearRole = async (roleName: string) => {
97
+ try {
98
+ const res = await api.request({
99
+ url: 'clusterManagerAclCache:clearRole',
100
+ method: 'post',
101
+ data: { roleName },
102
+ });
103
+ message.success(`Cleared ${res.data?.deletedCount || 0} entries for role "${roleName}"`);
104
+ fetchAll();
105
+ } catch {
106
+ message.error('Failed to clear role cache');
107
+ }
108
+ };
109
+
110
+ if (!stats) return null;
111
+
112
+ const detailByRole = stats.detailByRole || {};
113
+ const roleData = Object.entries(detailByRole).map(([role, data]) => ({
114
+ role,
115
+ ...data,
116
+ hitRate: data.checks > 0 ? Math.round((data.hits / data.checks) * 10000) / 100 : 0,
117
+ }));
118
+
119
+ const keyColumns = [
120
+ {
121
+ title: t('Role'),
122
+ dataIndex: 'role',
123
+ width: 120,
124
+ filters: [...new Set(keys.map((k) => k.role))].map((r) => ({ text: r, value: r })),
125
+ onFilter: (value: any, record: CachedKey) => record.role === value,
126
+ },
127
+ { title: t('Resource'), dataIndex: 'resource', width: 200 },
128
+ { title: t('Action'), dataIndex: 'action', width: 120 },
129
+ {
130
+ title: t('Cache Key'),
131
+ dataIndex: 'key',
132
+ ellipsis: true,
133
+ render: (val: string) => (
134
+ <Text code style={{ fontSize: 11 }}>
135
+ {val}
136
+ </Text>
137
+ ),
138
+ },
139
+ ];
140
+
141
+ const roleColumns = [
142
+ { title: t('Role'), dataIndex: 'role', width: 150 },
143
+ { title: t('Checks'), dataIndex: 'checks', width: 100, sorter: (a: any, b: any) => a.checks - b.checks },
144
+ {
145
+ title: t('Hits'),
146
+ dataIndex: 'hits',
147
+ width: 100,
148
+ render: (val: number) => <Text type="success">{val}</Text>,
149
+ },
150
+ {
151
+ title: t('Misses'),
152
+ dataIndex: 'misses',
153
+ width: 100,
154
+ render: (val: number) => <Text type="danger">{val}</Text>,
155
+ },
156
+ {
157
+ title: t('Hit Rate'),
158
+ dataIndex: 'hitRate',
159
+ width: 100,
160
+ render: (val: number) => <Tag color={val > 80 ? 'green' : val > 50 ? 'orange' : 'red'}>{val}%</Tag>,
161
+ sorter: (a: any, b: any) => a.hitRate - b.hitRate,
162
+ },
163
+ {
164
+ title: t('Actions'),
165
+ width: 80,
166
+ render: (_: any, record: any) => (
167
+ <Popconfirm
168
+ title={`${t('Clear cache for role')} "${record.role}"?`}
169
+ onConfirm={() => handleClearRole(record.role)}
170
+ >
171
+ <Button type="link" size="small" icon={<DeleteOutlined />} danger />
172
+ </Popconfirm>
173
+ ),
174
+ },
175
+ ];
176
+
177
+ return (
178
+ <div>
179
+ <Space style={{ marginBottom: 16 }}>
180
+ <Button icon={<ReloadOutlined />} onClick={fetchAll} loading={loading}>
181
+ {t('Refresh')}
182
+ </Button>
183
+ <Select
184
+ placeholder={t('Auto refresh')}
185
+ allowClear
186
+ style={{ width: 160 }}
187
+ value={autoRefresh}
188
+ onChange={setAutoRefresh}
189
+ options={[
190
+ { value: 5, label: t('Every 5s') },
191
+ { value: 10, label: t('Every 10s') },
192
+ { value: 30, label: t('Every 30s') },
193
+ ]}
194
+ />
195
+ <Popconfirm title={t('Clear all ACL cache entries?')} onConfirm={handleClearAll}>
196
+ <Button icon={<DeleteOutlined />} danger>
197
+ {t('Clear All Cache')}
198
+ </Button>
199
+ </Popconfirm>
200
+ <Popconfirm title={t('Reset ACL stats counters?')} onConfirm={handleResetStats}>
201
+ <Button>{t('Reset Stats')}</Button>
202
+ </Popconfirm>
203
+ <Tag>TTL: {stats.ttlSeconds}s</Tag>
204
+ <Tag>
205
+ {t('Since')}: {new Date(stats.startedAt).toLocaleString()}
206
+ </Tag>
207
+ </Space>
208
+
209
+ {/* Overview stats */}
210
+ <Row gutter={[16, 16]}>
211
+ <Col span={6}>
212
+ <Card size="small">
213
+ <Statistic title={t('Total ACL Checks')} value={stats.totalChecks} prefix={<SafetyCertificateOutlined />} />
214
+ </Card>
215
+ </Col>
216
+ <Col span={6}>
217
+ <Card size="small">
218
+ <Statistic
219
+ title={t('Cache Hits')}
220
+ value={stats.cacheHits}
221
+ prefix={<CheckCircleOutlined />}
222
+ valueStyle={{ color: '#3f8600' }}
223
+ />
224
+ </Card>
225
+ </Col>
226
+ <Col span={6}>
227
+ <Card size="small">
228
+ <Statistic
229
+ title={t('Cache Misses')}
230
+ value={stats.cacheMisses}
231
+ prefix={<CloseCircleOutlined />}
232
+ valueStyle={{ color: '#cf1322' }}
233
+ />
234
+ </Card>
235
+ </Col>
236
+ <Col span={6}>
237
+ <Card size="small">
238
+ <Statistic
239
+ title={t('Hit Rate')}
240
+ value={stats.hitRate}
241
+ suffix="%"
242
+ prefix={<ThunderboltOutlined />}
243
+ valueStyle={{
244
+ color: stats.hitRate > 80 ? '#3f8600' : stats.hitRate > 50 ? '#faad14' : '#cf1322',
245
+ }}
246
+ />
247
+ <Text type="secondary">
248
+ {t('Cached keys')}: {stats.cachedKeys}
249
+ </Text>
250
+ </Card>
251
+ </Col>
252
+ </Row>
253
+
254
+ {/* Per-role breakdown */}
255
+ <Row gutter={[16, 16]} style={{ marginTop: 16 }}>
256
+ <Col span={12}>
257
+ <Card title={t('Stats by Role')} size="small">
258
+ {roleData.length === 0 ? (
259
+ <Text type="secondary">{t('No ACL checks recorded yet')}</Text>
260
+ ) : (
261
+ <Table
262
+ rowKey="role"
263
+ size="small"
264
+ pagination={false}
265
+ dataSource={roleData}
266
+ columns={roleColumns}
267
+ scroll={{ x: 'max-content' }}
268
+ />
269
+ )}
270
+ </Card>
271
+ </Col>
272
+
273
+ {/* Cached keys */}
274
+ <Col span={12}>
275
+ <Card title={`${t('Cached Permission Keys')} (${keys.length})`} size="small">
276
+ {keys.length === 0 ? (
277
+ <Text type="secondary">{t('No cached keys')}</Text>
278
+ ) : (
279
+ <Table
280
+ rowKey="key"
281
+ size="small"
282
+ pagination={{ pageSize: 10, size: 'small' }}
283
+ dataSource={keys}
284
+ columns={keyColumns}
285
+ />
286
+ )}
287
+ </Card>
288
+ </Col>
289
+ </Row>
290
+ </div>
291
+ );
292
+ }