plugin-cluster-manager 1.1.5 → 1.1.7

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 (76) hide show
  1. package/README.md +17 -5
  2. package/dist/client/index.js +1 -1
  3. package/dist/externalVersion.js +5 -5
  4. package/dist/locale/en-US.json +28 -4
  5. package/dist/locale/vi-VN.json +28 -4
  6. package/dist/locale/zh-CN.json +28 -4
  7. package/dist/server/actions/event-queue-monitor.js +123 -1
  8. package/dist/server/actions/orchestrator.js +1 -1
  9. package/dist/server/actions/plugin-operations.js +171 -0
  10. package/dist/server/collections/cluster-manager-plugins.js +44 -0
  11. package/dist/server/plugin.js +8 -2
  12. package/package.json +9 -4
  13. package/src/client/ClusterManagerLayout.tsx +16 -10
  14. package/src/client/EventQueueMonitor.tsx +349 -202
  15. package/src/client/PluginOperations.tsx +234 -0
  16. package/src/locale/en-US.json +28 -4
  17. package/src/locale/vi-VN.json +28 -4
  18. package/src/locale/zh-CN.json +28 -4
  19. package/src/server/actions/event-queue-monitor.ts +234 -95
  20. package/src/server/actions/orchestrator.ts +1 -1
  21. package/src/server/actions/plugin-operations.ts +151 -0
  22. package/src/server/collections/cluster-manager-plugins.ts +19 -0
  23. package/src/server/orchestrator/PackageManager.ts +84 -17
  24. package/src/server/plugin.ts +28 -20
  25. package/dist/client/AclCacheManager.d.ts +0 -2
  26. package/dist/client/CacheMonitor.d.ts +0 -2
  27. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  28. package/dist/client/ClusterNodes.d.ts +0 -2
  29. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  30. package/dist/client/EventQueueMonitor.d.ts +0 -2
  31. package/dist/client/LockMonitor.d.ts +0 -2
  32. package/dist/client/PackageInstaller.d.ts +0 -2
  33. package/dist/client/RedisMonitor.d.ts +0 -2
  34. package/dist/client/TaskManager.d.ts +0 -2
  35. package/dist/client/WorkflowExecutions.d.ts +0 -2
  36. package/dist/client/index.d.ts +0 -5
  37. package/dist/client/utils.d.ts +0 -12
  38. package/dist/index.d.ts +0 -2
  39. package/dist/server/actions/acl-cache.d.ts +0 -53
  40. package/dist/server/actions/cache-monitor.d.ts +0 -23
  41. package/dist/server/actions/cluster-nodes.d.ts +0 -49
  42. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  43. package/dist/server/actions/lock-monitor.d.ts +0 -19
  44. package/dist/server/actions/orchestrator.d.ts +0 -58
  45. package/dist/server/actions/package-manager.d.ts +0 -6
  46. package/dist/server/actions/redis-monitor.d.ts +0 -12
  47. package/dist/server/actions/tasks.d.ts +0 -7
  48. package/dist/server/actions/workflow-executions.d.ts +0 -7
  49. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  50. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  51. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  52. package/dist/server/collections/app.d.ts +0 -8
  53. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  54. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  55. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  56. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  57. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  58. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  59. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  60. package/dist/server/collections/cluster-manager.d.ts +0 -22
  61. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  62. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  63. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  64. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  65. package/dist/server/collections/worker-packages.d.ts +0 -22
  66. package/dist/server/index.d.ts +0 -1
  67. package/dist/server/orchestrator/PackageManager.d.ts +0 -37
  68. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  69. package/dist/server/orchestrator/index.d.ts +0 -4
  70. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  71. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  72. package/dist/server/orchestrator/types.d.ts +0 -84
  73. package/dist/server/plugin.d.ts +0 -26
  74. package/dist/server/utils/node.d.ts +0 -6
  75. package/dist/server/utils/redis.d.ts +0 -29
  76. package/dist/shared/packages.d.ts +0 -23
@@ -1,202 +1,349 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import { Card, Table, Tag, Button, Space, Row, Col, Statistic, Select, Spin } from 'antd';
3
- import {
4
- ReloadOutlined,
5
- ThunderboltOutlined,
6
- ClockCircleOutlined,
7
- UnorderedListOutlined,
8
- ApiOutlined,
9
- } from '@ant-design/icons';
10
- import { useAPIClient } from '@nocobase/client';
11
- import { useT } from './utils';
12
-
13
- export function EventQueueMonitor() {
14
- const t = useT();
15
- const api = useAPIClient();
16
- const [loading, setLoading] = useState(false);
17
- const [stats, setStats] = useState<any>(null);
18
- const [autoRefresh, setAutoRefresh] = useState<number | null>(null);
19
- const [selectedChannel, setSelectedChannel] = useState<string | null>(null);
20
- const [messages, setMessages] = useState<any[]>([]);
21
- const [messagesMeta, setMessagesMeta] = useState<any>({});
22
-
23
- const fetchStats = useCallback(async () => {
24
- setLoading(true);
25
- try {
26
- const res = await api.request({ url: 'clusterManagerQueue:stats' });
27
- setStats(res?.data?.data);
28
- } catch {
29
- // Ignore
30
- } finally {
31
- setLoading(false);
32
- }
33
- }, [api]);
34
-
35
- const fetchMessages = useCallback(async (channel: string) => {
36
- try {
37
- const res = await api.request({ url: 'clusterManagerQueue:messages', params: { channel } });
38
- setMessages(res?.data?.data?.data || []);
39
- setMessagesMeta(res?.data?.data?.meta || {});
40
- } catch {
41
- // Ignore
42
- }
43
- }, [api]);
44
-
45
- useEffect(() => {
46
- fetchStats();
47
- }, [fetchStats]);
48
-
49
- useEffect(() => {
50
- if (!autoRefresh) return;
51
- const timer = setInterval(fetchStats, autoRefresh * 1000);
52
- return () => clearInterval(timer);
53
- }, [autoRefresh, fetchStats]);
54
-
55
- const channelColumns = [
56
- { title: t('Channel'), dataIndex: 'channel', key: 'channel' },
57
- {
58
- title: t('Pending'),
59
- dataIndex: 'pending',
60
- key: 'pending',
61
- width: 100,
62
- render: (v: number | null) =>
63
- v === null ? <Tag>N/A</Tag> : <Tag color={v > 0 ? 'orange' : 'green'}>{v}</Tag>,
64
- },
65
- { title: t('Concurrency'), dataIndex: 'concurrency', key: 'concurrency', width: 120 },
66
- {
67
- title: t('Interval'),
68
- dataIndex: 'interval',
69
- key: 'interval',
70
- width: 100,
71
- render: (v: number) => `${v}ms`,
72
- },
73
- {
74
- title: t('Actions'),
75
- key: 'actions',
76
- width: 100,
77
- render: (_: any, record: any) => (
78
- <Button
79
- size="small"
80
- type="link"
81
- onClick={() => {
82
- setSelectedChannel(record.channel);
83
- fetchMessages(record.channel);
84
- }}
85
- >
86
- {t('Messages')}
87
- </Button>
88
- ),
89
- },
90
- ];
91
-
92
- const messageColumns = [
93
- { title: 'ID', dataIndex: 'id', key: 'id', width: 280, ellipsis: true },
94
- {
95
- title: t('Content'),
96
- dataIndex: 'content',
97
- key: 'content',
98
- ellipsis: true,
99
- render: (v: any) => <code style={{ fontSize: 11 }}>{JSON.stringify(v)}</code>,
100
- },
101
- { title: t('Retried'), dataIndex: 'retried', key: 'retried', width: 80 },
102
- {
103
- title: t('Timestamp'),
104
- dataIndex: 'timestamp',
105
- key: 'timestamp',
106
- width: 180,
107
- render: (v: number) => (v ? new Date(v).toLocaleString() : '-'),
108
- },
109
- ];
110
-
111
- return (
112
- <Spin spinning={loading}>
113
- <Space direction="vertical" style={{ width: '100%' }} size="middle">
114
- <Space>
115
- <Button icon={<ReloadOutlined />} onClick={fetchStats}>{t('Refresh')}</Button>
116
- <Select
117
- placeholder={t('Auto Refresh')}
118
- allowClear
119
- value={autoRefresh}
120
- onChange={setAutoRefresh}
121
- style={{ width: 140 }}
122
- options={[
123
- { value: 5, label: '5s' },
124
- { value: 10, label: '10s' },
125
- { value: 30, label: '30s' },
126
- ]}
127
- />
128
- {stats && <Tag color={stats.connected ? 'green' : 'red'}>{stats.adapter}</Tag>}
129
- </Space>
130
-
131
- {stats && (
132
- <Row gutter={16}>
133
- <Col span={6}>
134
- <Card size="small">
135
- <Statistic
136
- title={t('Adapter')}
137
- value={stats.adapter}
138
- prefix={<ApiOutlined />}
139
- valueStyle={{ fontSize: 16 }}
140
- />
141
- </Card>
142
- </Col>
143
- <Col span={6}>
144
- <Card size="small">
145
- <Statistic
146
- title={t('Total Channels')}
147
- value={stats.totalChannels}
148
- prefix={<UnorderedListOutlined />}
149
- />
150
- </Card>
151
- </Col>
152
- <Col span={6}>
153
- <Card size="small">
154
- <Statistic
155
- title={t('Total Pending')}
156
- value={stats.totalPending}
157
- prefix={<ClockCircleOutlined />}
158
- valueStyle={{ color: stats.totalPending > 0 ? '#cf1322' : '#3f8600' }}
159
- />
160
- </Card>
161
- </Col>
162
- <Col span={6}>
163
- <Card size="small">
164
- <Statistic
165
- title={t('Connected')}
166
- value={stats.connected ? 'Yes' : 'No'}
167
- prefix={<ThunderboltOutlined />}
168
- valueStyle={{ color: stats.connected ? '#3f8600' : '#cf1322' }}
169
- />
170
- </Card>
171
- </Col>
172
- </Row>
173
- )}
174
-
175
- <Card title={t('Event Channels')} size="small">
176
- <Table
177
- dataSource={stats?.channels || []}
178
- columns={channelColumns}
179
- rowKey="channel"
180
- size="small"
181
- pagination={false}
182
- />
183
- </Card>
184
-
185
- {selectedChannel && (
186
- <Card title={`${t('Messages')}: ${selectedChannel}`} size="small"
187
- extra={<Button size="small" onClick={() => setSelectedChannel(null)}>Close</Button>}
188
- >
189
- {messagesMeta.note && <Tag style={{ marginBottom: 8 }}>{messagesMeta.note}</Tag>}
190
- <Table
191
- dataSource={messages}
192
- columns={messageColumns}
193
- rowKey="id"
194
- size="small"
195
- pagination={{ pageSize: 10, total: messagesMeta.count }}
196
- />
197
- </Card>
198
- )}
199
- </Space>
200
- </Spin>
201
- );
202
- }
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { Card, Table, Tag, Button, Space, Row, Col, Statistic, Select, Spin } from 'antd';
3
+ import {
4
+ ReloadOutlined,
5
+ ThunderboltOutlined,
6
+ ClockCircleOutlined,
7
+ UnorderedListOutlined,
8
+ ApiOutlined,
9
+ DatabaseOutlined,
10
+ } from '@ant-design/icons';
11
+ import { useAPIClient } from '@nocobase/client';
12
+ import { useT } from './utils';
13
+
14
+ export function EventQueueMonitor() {
15
+ const t = useT();
16
+ const api = useAPIClient();
17
+ const [loading, setLoading] = useState(false);
18
+ const [stats, setStats] = useState<any>(null);
19
+ const [autoRefresh, setAutoRefresh] = useState<number | null>(null);
20
+ const [selectedChannel, setSelectedChannel] = useState<string | null>(null);
21
+ const [selectedRedisQueue, setSelectedRedisQueue] = useState<any>(null);
22
+ const [messages, setMessages] = useState<any[]>([]);
23
+ const [messagesMeta, setMessagesMeta] = useState<any>({});
24
+ const [redisMessages, setRedisMessages] = useState<any[]>([]);
25
+ const [redisMessagesMeta, setRedisMessagesMeta] = useState<any>({});
26
+
27
+ const fetchStats = useCallback(async () => {
28
+ setLoading(true);
29
+ try {
30
+ const res = await api.request({ url: 'clusterManagerQueue:stats' });
31
+ setStats(res?.data?.data);
32
+ } catch {
33
+ // Ignore
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, [api]);
38
+
39
+ const fetchMessages = useCallback(
40
+ async (channel: string) => {
41
+ try {
42
+ const res = await api.request({ url: 'clusterManagerQueue:messages', params: { channel } });
43
+ setMessages(res?.data?.data?.data || []);
44
+ setMessagesMeta(res?.data?.data?.meta || {});
45
+ } catch {
46
+ // Ignore
47
+ }
48
+ },
49
+ [api],
50
+ );
51
+
52
+ const fetchRedisMessages = useCallback(
53
+ async (queue: any) => {
54
+ if (!queue?.key) return;
55
+ try {
56
+ const res = await api.request({
57
+ url: 'clusterManagerQueue:messages',
58
+ params: { source: 'redis', key: queue.key },
59
+ });
60
+ setRedisMessages(res?.data?.data?.data || []);
61
+ setRedisMessagesMeta(res?.data?.data?.meta || {});
62
+ } catch {
63
+ // Ignore
64
+ }
65
+ },
66
+ [api],
67
+ );
68
+
69
+ useEffect(() => {
70
+ fetchStats();
71
+ }, [fetchStats]);
72
+
73
+ useEffect(() => {
74
+ if (!autoRefresh) return;
75
+ const timer = setInterval(fetchStats, autoRefresh * 1000);
76
+ return () => clearInterval(timer);
77
+ }, [autoRefresh, fetchStats]);
78
+
79
+ const channelColumns = [
80
+ { title: t('Channel'), dataIndex: 'channel', key: 'channel' },
81
+ {
82
+ title: t('Pending'),
83
+ dataIndex: 'pending',
84
+ key: 'pending',
85
+ width: 100,
86
+ render: (v: number | null) => (v === null ? <Tag>N/A</Tag> : <Tag color={v > 0 ? 'orange' : 'green'}>{v}</Tag>),
87
+ },
88
+ { title: t('Concurrency'), dataIndex: 'concurrency', key: 'concurrency', width: 120 },
89
+ {
90
+ title: t('Interval'),
91
+ dataIndex: 'interval',
92
+ key: 'interval',
93
+ width: 100,
94
+ render: (v: number) => `${v}ms`,
95
+ },
96
+ {
97
+ title: t('Actions'),
98
+ key: 'actions',
99
+ width: 100,
100
+ render: (_: any, record: any) => (
101
+ <Button
102
+ size="small"
103
+ type="link"
104
+ onClick={() => {
105
+ setSelectedChannel(record.channel);
106
+ fetchMessages(record.channel);
107
+ }}
108
+ >
109
+ {t('Messages')}
110
+ </Button>
111
+ ),
112
+ },
113
+ ];
114
+
115
+ const messageColumns = [
116
+ { title: 'ID', dataIndex: 'id', key: 'id', width: 280, ellipsis: true },
117
+ {
118
+ title: t('Content'),
119
+ dataIndex: 'content',
120
+ key: 'content',
121
+ ellipsis: true,
122
+ render: (v: any) => <code style={{ fontSize: 11 }}>{JSON.stringify(v)}</code>,
123
+ },
124
+ { title: t('Retried'), dataIndex: 'retried', key: 'retried', width: 80 },
125
+ {
126
+ title: t('Timestamp'),
127
+ dataIndex: 'timestamp',
128
+ key: 'timestamp',
129
+ width: 180,
130
+ render: (v: number) => (v ? new Date(v).toLocaleString() : '-'),
131
+ },
132
+ ];
133
+
134
+ const redisQueueColumns = [
135
+ { title: t('Queue'), dataIndex: 'channel', key: 'channel', width: 240 },
136
+ { title: t('Redis Key'), dataIndex: 'key', key: 'key', ellipsis: true },
137
+ { title: t('App'), dataIndex: 'appName', key: 'appName', width: 120 },
138
+ {
139
+ title: t('Pending'),
140
+ dataIndex: 'pending',
141
+ key: 'pending',
142
+ width: 100,
143
+ render: (v: number) => <Tag color={v > 0 ? 'orange' : 'green'}>{v}</Tag>,
144
+ },
145
+ {
146
+ title: t('Actions'),
147
+ key: 'actions',
148
+ width: 100,
149
+ render: (_: any, record: any) => (
150
+ <Button
151
+ size="small"
152
+ type="link"
153
+ onClick={() => {
154
+ setSelectedRedisQueue(record);
155
+ fetchRedisMessages(record);
156
+ }}
157
+ >
158
+ {t('Items')}
159
+ </Button>
160
+ ),
161
+ },
162
+ ];
163
+
164
+ const redisMessageColumns = [
165
+ { title: '#', dataIndex: 'index', key: 'index', width: 80 },
166
+ {
167
+ title: t('Content'),
168
+ dataIndex: 'content',
169
+ key: 'content',
170
+ ellipsis: true,
171
+ render: (v: any) => <code style={{ fontSize: 11 }}>{JSON.stringify(v)}</code>,
172
+ },
173
+ {
174
+ title: t('Queued At'),
175
+ dataIndex: 'timestamp',
176
+ key: 'timestamp',
177
+ width: 180,
178
+ render: (v: number) => (v ? new Date(v).toLocaleString() : '-'),
179
+ },
180
+ ];
181
+
182
+ return (
183
+ <Spin spinning={loading}>
184
+ <Space direction="vertical" style={{ width: '100%' }} size="middle">
185
+ <Space>
186
+ <Button icon={<ReloadOutlined />} onClick={fetchStats}>
187
+ {t('Refresh')}
188
+ </Button>
189
+ <Select
190
+ placeholder={t('Auto Refresh')}
191
+ allowClear
192
+ value={autoRefresh}
193
+ onChange={setAutoRefresh}
194
+ style={{ width: 140 }}
195
+ options={[
196
+ { value: 5, label: '5s' },
197
+ { value: 10, label: '10s' },
198
+ { value: 30, label: '30s' },
199
+ ]}
200
+ />
201
+ {stats && <Tag color={stats.connected ? 'green' : 'red'}>{stats.adapter}</Tag>}
202
+ </Space>
203
+
204
+ {stats && (
205
+ <Row gutter={16}>
206
+ <Col span={6}>
207
+ <Card size="small">
208
+ <Statistic
209
+ title={t('Adapter')}
210
+ value={stats.adapter}
211
+ prefix={<ApiOutlined />}
212
+ valueStyle={{ fontSize: 16 }}
213
+ />
214
+ </Card>
215
+ </Col>
216
+ <Col span={6}>
217
+ <Card size="small">
218
+ <Statistic title={t('Total Channels')} value={stats.totalChannels} prefix={<UnorderedListOutlined />} />
219
+ </Card>
220
+ </Col>
221
+ <Col span={6}>
222
+ <Card size="small">
223
+ <Statistic
224
+ title={t('Total Pending')}
225
+ value={stats.totalPending}
226
+ prefix={<ClockCircleOutlined />}
227
+ valueStyle={{ color: stats.totalPending > 0 ? '#cf1322' : '#3f8600' }}
228
+ />
229
+ </Card>
230
+ </Col>
231
+ <Col span={6}>
232
+ <Card size="small">
233
+ <Statistic
234
+ title={t('Connected')}
235
+ value={stats.connected ? 'Yes' : 'No'}
236
+ prefix={<ThunderboltOutlined />}
237
+ valueStyle={{ color: stats.connected ? '#3f8600' : '#cf1322' }}
238
+ />
239
+ </Card>
240
+ </Col>
241
+ </Row>
242
+ )}
243
+
244
+ {stats?.redisQueues && (
245
+ <Row gutter={16}>
246
+ <Col span={8}>
247
+ <Card size="small">
248
+ <Statistic
249
+ title={t('Redis Queue Pending')}
250
+ value={stats.redisQueues.totalPending || 0}
251
+ prefix={<DatabaseOutlined />}
252
+ valueStyle={{ color: (stats.redisQueues.totalPending || 0) > 0 ? '#cf1322' : '#3f8600' }}
253
+ />
254
+ </Card>
255
+ </Col>
256
+ <Col span={8}>
257
+ <Card size="small">
258
+ <Statistic
259
+ title={t('Redis Queues')}
260
+ value={stats.redisQueues.queues?.length || 0}
261
+ prefix={<UnorderedListOutlined />}
262
+ />
263
+ </Card>
264
+ </Col>
265
+ <Col span={8}>
266
+ <Card size="small">
267
+ <Statistic
268
+ title={t('Redis Connected')}
269
+ value={stats.redisQueues.connected ? 'Yes' : 'No'}
270
+ prefix={<ThunderboltOutlined />}
271
+ valueStyle={{ color: stats.redisQueues.connected ? '#3f8600' : '#cf1322' }}
272
+ />
273
+ </Card>
274
+ </Col>
275
+ </Row>
276
+ )}
277
+
278
+ <Card title={t('Event Channels')} size="small">
279
+ <Table
280
+ dataSource={stats?.channels || []}
281
+ columns={channelColumns}
282
+ rowKey="channel"
283
+ size="small"
284
+ pagination={false}
285
+ />
286
+ </Card>
287
+
288
+ <Card title={t('Redis Queues')} size="small">
289
+ {stats?.redisQueues?.note && <Tag style={{ marginBottom: 8 }}>{stats.redisQueues.note}</Tag>}
290
+ <Table
291
+ dataSource={stats?.redisQueues?.queues || []}
292
+ columns={redisQueueColumns}
293
+ rowKey="key"
294
+ size="small"
295
+ pagination={false}
296
+ />
297
+ </Card>
298
+
299
+ {selectedChannel && (
300
+ <Card
301
+ title={`${t('Messages')}: ${selectedChannel}`}
302
+ size="small"
303
+ extra={
304
+ <Button size="small" onClick={() => setSelectedChannel(null)}>
305
+ Close
306
+ </Button>
307
+ }
308
+ >
309
+ {messagesMeta.note && <Tag style={{ marginBottom: 8 }}>{messagesMeta.note}</Tag>}
310
+ <Table
311
+ dataSource={messages}
312
+ columns={messageColumns}
313
+ rowKey="id"
314
+ size="small"
315
+ pagination={{ pageSize: 10, total: messagesMeta.count }}
316
+ />
317
+ </Card>
318
+ )}
319
+
320
+ {selectedRedisQueue && (
321
+ <Card
322
+ title={`${t('Redis Queue Items')}: ${selectedRedisQueue.channel}`}
323
+ size="small"
324
+ extra={
325
+ <Space>
326
+ <Button size="small" onClick={() => fetchRedisMessages(selectedRedisQueue)}>
327
+ {t('Refresh')}
328
+ </Button>
329
+ <Button size="small" onClick={() => setSelectedRedisQueue(null)}>
330
+ {t('Close')}
331
+ </Button>
332
+ </Space>
333
+ }
334
+ >
335
+ <Tag style={{ marginBottom: 8 }}>{selectedRedisQueue.key}</Tag>
336
+ {redisMessagesMeta.note && <Tag style={{ marginBottom: 8 }}>{redisMessagesMeta.note}</Tag>}
337
+ <Table
338
+ dataSource={redisMessages}
339
+ columns={redisMessageColumns}
340
+ rowKey="id"
341
+ size="small"
342
+ pagination={{ pageSize: 10, total: redisMessagesMeta.count }}
343
+ />
344
+ </Card>
345
+ )}
346
+ </Space>
347
+ </Spin>
348
+ );
349
+ }