plugin-cluster-manager 1.1.11 → 1.1.15

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 (116) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/914.c0bce51908fd81d7.js +10 -0
  5. package/dist/client-v2/index.js +10 -0
  6. package/dist/externalVersion.js +6 -5
  7. package/dist/locale/en-US.json +138 -124
  8. package/dist/locale/vi-VN.json +139 -125
  9. package/dist/locale/zh-CN.json +140 -125
  10. package/dist/server/actions/cluster-nodes.js +2 -6
  11. package/dist/server/actions/doctor.js +1 -5
  12. package/dist/server/actions/orchestrator.js +37 -0
  13. package/dist/server/actions/queue-mappings.js +107 -0
  14. package/dist/server/collections/worker-queue-mappings.js +106 -0
  15. package/dist/server/orchestrator/PackageManager.js +1 -8
  16. package/dist/server/orchestrator/docker-adapter.js +49 -27
  17. package/dist/server/plugin.js +10 -8
  18. package/dist/server/queue-scanner.js +141 -0
  19. package/dist/server/utils/node.js +30 -2
  20. package/package.json +46 -42
  21. package/src/client/AclCacheManager.tsx +292 -287
  22. package/src/client/CacheMonitor.tsx +2 -2
  23. package/src/client/ClusterManagerLayout.tsx +6 -0
  24. package/src/client/ClusterNodes.tsx +11 -4
  25. package/src/client/ContainerOrchestrator.tsx +186 -104
  26. package/src/client/Doctor.tsx +2 -2
  27. package/src/client/EventQueueMonitor.tsx +2 -2
  28. package/src/client/LockMonitor.tsx +2 -2
  29. package/src/client/NginxCacheManager.tsx +2 -2
  30. package/src/client/PackageInstaller.tsx +2 -2
  31. package/src/client/PluginOperations.tsx +2 -2
  32. package/src/client/QueueAssignment.tsx +355 -0
  33. package/src/client/RedisMonitor.tsx +3 -3
  34. package/src/client/TaskManager.tsx +194 -187
  35. package/src/client/WorkflowExecutions.tsx +243 -238
  36. package/src/client/utils.ts +1 -1
  37. package/src/client-v2/plugin.tsx +24 -0
  38. package/src/locale/en-US.json +138 -124
  39. package/src/locale/vi-VN.json +139 -125
  40. package/src/locale/zh-CN.json +140 -125
  41. package/src/server/actions/cluster-nodes.ts +4 -7
  42. package/src/server/actions/doctor.ts +11 -9
  43. package/src/server/actions/orchestrator.ts +54 -2
  44. package/src/server/actions/queue-mappings.ts +94 -0
  45. package/src/server/adapters/redis-node-registry.ts +126 -131
  46. package/src/server/collections/worker-queue-mappings.ts +85 -0
  47. package/src/server/orchestrator/PackageManager.ts +2 -10
  48. package/src/server/orchestrator/docker-adapter.ts +74 -37
  49. package/src/server/plugin.ts +15 -12
  50. package/src/server/queue-scanner.ts +154 -0
  51. package/src/server/utils/node.ts +48 -0
  52. package/dist/client/AclCacheManager.d.ts +0 -2
  53. package/dist/client/CacheMonitor.d.ts +0 -2
  54. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  55. package/dist/client/ClusterNodes.d.ts +0 -2
  56. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  57. package/dist/client/Doctor.d.ts +0 -2
  58. package/dist/client/EventQueueMonitor.d.ts +0 -2
  59. package/dist/client/LockMonitor.d.ts +0 -2
  60. package/dist/client/NginxCacheManager.d.ts +0 -2
  61. package/dist/client/PackageInstaller.d.ts +0 -2
  62. package/dist/client/PluginOperations.d.ts +0 -2
  63. package/dist/client/RedisMonitor.d.ts +0 -2
  64. package/dist/client/TaskManager.d.ts +0 -2
  65. package/dist/client/WorkflowExecutions.d.ts +0 -2
  66. package/dist/client/index.d.ts +0 -5
  67. package/dist/client/utils/clientSafeCache.d.ts +0 -3
  68. package/dist/client/utils/requestDedupInterceptor.d.ts +0 -2
  69. package/dist/client/utils.d.ts +0 -12
  70. package/dist/index.d.ts +0 -2
  71. package/dist/server/actions/acl-cache.d.ts +0 -53
  72. package/dist/server/actions/cache-monitor.d.ts +0 -33
  73. package/dist/server/actions/cluster-nodes.d.ts +0 -64
  74. package/dist/server/actions/doctor.d.ts +0 -82
  75. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  76. package/dist/server/actions/lock-monitor.d.ts +0 -19
  77. package/dist/server/actions/orchestrator.d.ts +0 -58
  78. package/dist/server/actions/package-manager.d.ts +0 -6
  79. package/dist/server/actions/plugin-operations.d.ts +0 -6
  80. package/dist/server/actions/redis-monitor.d.ts +0 -12
  81. package/dist/server/actions/tasks.d.ts +0 -7
  82. package/dist/server/actions/workflow-executions.d.ts +0 -7
  83. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  84. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  85. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  86. package/dist/server/collections/app.d.ts +0 -8
  87. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  88. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  89. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  90. package/dist/server/collections/cluster-manager-doctor-runs.d.ts +0 -3
  91. package/dist/server/collections/cluster-manager-doctor.d.ts +0 -18
  92. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  93. package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
  94. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  95. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  96. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  97. package/dist/server/collections/cluster-manager.d.ts +0 -22
  98. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  99. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  100. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  101. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  102. package/dist/server/collections/worker-packages.d.ts +0 -22
  103. package/dist/server/hooks/cacheInvalidationHooks.d.ts +0 -1
  104. package/dist/server/middlewares/listMetaCacheMiddleware.d.ts +0 -2
  105. package/dist/server/orchestrator/PackageManager.d.ts +0 -39
  106. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  107. package/dist/server/orchestrator/index.d.ts +0 -4
  108. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  109. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  110. package/dist/server/orchestrator/types.d.ts +0 -84
  111. package/dist/server/plugin.d.ts +0 -26
  112. package/dist/server/utils/node.d.ts +0 -6
  113. package/dist/server/utils/redis.d.ts +0 -29
  114. package/dist/server/utils/versionManager.d.ts +0 -10
  115. package/dist/shared/packages.d.ts +0 -23
  116. /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -1,238 +1,243 @@
1
- import React, { useEffect, useState, useCallback } from 'react';
2
- import { Table, Tag, Button, Space, Popconfirm, message, Select, Dropdown } from 'antd';
3
- import { ReloadOutlined, StopOutlined, UnorderedListOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
5
- import dayjs from 'dayjs';
6
-
7
- const EXEC_STATUS: Record<string, { label: string; color: string }> = {
8
- null: { label: 'Queueing', color: 'default' },
9
- '0': { label: 'Started', color: 'processing' },
10
- '1': { label: 'Resolved', color: 'success' },
11
- '-1': { label: 'Failed', color: 'error' },
12
- '-2': { label: 'Error', color: 'error' },
13
- '-3': { label: 'Aborted', color: 'warning' },
14
- '-4': { label: 'Canceled', color: 'warning' },
15
- '-5': { label: 'Rejected', color: 'warning' },
16
- '-6': { label: 'Retry Needed', color: 'orange' },
17
- };
18
-
19
- const JOB_STATUS: Record<string, { label: string; color: string }> = {
20
- '0': { label: 'Pending', color: 'default' },
21
- '1': { label: 'Resolved', color: 'success' },
22
- '-1': { label: 'Failed', color: 'error' },
23
- '-2': { label: 'Error', color: 'error' },
24
- '-3': { label: 'Aborted', color: 'warning' },
25
- '-4': { label: 'Canceled', color: 'warning' },
26
- };
27
-
28
- export function WorkflowExecutions() {
29
- const api = useAPIClient();
30
- const [data, setData] = useState<any[]>([]);
31
- const [loading, setLoading] = useState(false);
32
- const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
33
- const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
34
- const [expandedJobs, setExpandedJobs] = useState<Record<string, any[]>>({});
35
- const [loadingJobs, setLoadingJobs] = useState<Record<string, boolean>>({});
36
-
37
- const fetchData = useCallback(
38
- async (page = pagination.current, pageSize = pagination.pageSize) => {
39
- setLoading(true);
40
- try {
41
- const res = await api.request({
42
- url: 'clusterManagerWorkflow:list',
43
- params: { page, pageSize, statusFilter: statusFilter === undefined ? '' : statusFilter },
44
- });
45
- const body = res.data;
46
- const rows = Array.isArray(body?.data?.data) ? body.data.data : Array.isArray(body?.data) ? body.data : Array.isArray(body) ? body : [];
47
- setData(rows);
48
- setPagination((prev) => ({
49
- ...prev,
50
- current: body.meta?.page || page,
51
- total: body.meta?.count || 0,
52
- }));
53
- } catch {
54
- message.error('Failed to load executions');
55
- } finally {
56
- setLoading(false);
57
- }
58
- },
59
- [api, pagination.current, pagination.pageSize, statusFilter],
60
- );
61
-
62
- useEffect(() => {
63
- fetchData();
64
- }, [statusFilter]);
65
-
66
- const fetchJobs = async (executionId: string) => {
67
- if (expandedJobs[executionId]) return;
68
- setLoadingJobs((prev) => ({ ...prev, [executionId]: true }));
69
- try {
70
- const res = await api.request({
71
- url: 'clusterManagerWorkflow:getJobs',
72
- params: { filterByTk: executionId },
73
- });
74
- const jobs = Array.isArray(res?.data?.data?.data) ? res.data.data.data : Array.isArray(res?.data?.data) ? res.data.data : Array.isArray(res?.data) ? res.data : [];
75
- setExpandedJobs((prev) => ({ ...prev, [executionId]: jobs }));
76
- } catch {
77
- message.error('Failed to load jobs');
78
- } finally {
79
- setLoadingJobs((prev) => ({ ...prev, [executionId]: false }));
80
- }
81
- };
82
-
83
- const handleCancel = async (id: string) => {
84
- await api.request({ url: 'clusterManagerWorkflow:cancel', params: { filterByTk: id } });
85
- message.success('Execution canceled');
86
- fetchData();
87
- };
88
-
89
- const handlePurge = async (days: number) => {
90
- try {
91
- const res = await api.request({ url: `clusterManagerWorkflow:purge`, method: 'post', data: { days } });
92
- message.success(`Purged ${res?.data?.deletedCount || 0} executions`);
93
- fetchData();
94
- } catch {
95
- message.error('Failed to purge executions');
96
- }
97
- };
98
-
99
- const purgeItems = [
100
- { key: '7', label: 'Older than 7 days', onClick: () => handlePurge(7) },
101
- { key: '30', label: 'Older than 30 days', onClick: () => handlePurge(30) },
102
- { key: '0', label: 'All completed/failed', danger: true, onClick: () => handlePurge(0) },
103
- ];
104
-
105
- const jobColumns = [
106
- { title: 'Job ID', dataIndex: 'id', width: 100 },
107
- {
108
- title: 'Node',
109
- key: 'node',
110
- width: 200,
111
- render: (_: any, r: any) => r.node?.title || r.nodeKey || '-',
112
- },
113
- {
114
- title: 'Status',
115
- dataIndex: 'status',
116
- width: 100,
117
- render: (val: number) => {
118
- const s = JOB_STATUS[String(val)] || { label: String(val), color: 'default' };
119
- return <Tag color={s.color}>{s.label}</Tag>;
120
- },
121
- },
122
- {
123
- title: 'Result',
124
- dataIndex: 'result',
125
- ellipsis: true,
126
- render: (val: any) => (val ? JSON.stringify(val).slice(0, 120) : '-'),
127
- },
128
- ];
129
-
130
- const columns = [
131
- { title: 'ID', dataIndex: 'id', width: 100 },
132
- {
133
- title: 'Workflow',
134
- key: 'workflow',
135
- width: 200,
136
- render: (_: any, r: any) => r.workflow?.title || '-',
137
- },
138
- {
139
- title: 'Status',
140
- dataIndex: 'status',
141
- width: 120,
142
- render: (val: number | null) => {
143
- const s = EXEC_STATUS[String(val)] || { label: String(val), color: 'default' };
144
- return <Tag color={s.color}>{s.label}</Tag>;
145
- },
146
- },
147
- {
148
- title: 'Executing Node',
149
- dataIndex: 'workerNode',
150
- width: 150,
151
- render: (val: string) => (val && val !== '-' ? <Tag color="blue">{val}</Tag> : <Tag>{val || '-'}</Tag>),
152
- },
153
- {
154
- title: 'Manual',
155
- dataIndex: 'manually',
156
- width: 80,
157
- render: (val: boolean) => (val ? <Tag color="blue">Yes</Tag> : '-'),
158
- },
159
- {
160
- title: 'Triggered At',
161
- dataIndex: 'createdAt',
162
- width: 160,
163
- render: (val: string) => (val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : '-'),
164
- },
165
- {
166
- title: 'Actions',
167
- key: 'actions',
168
- width: 120,
169
- render: (_: any, record: any) => (
170
- <Space>
171
- <Button
172
- type="link"
173
- size="small"
174
- icon={<UnorderedListOutlined />}
175
- onClick={() => fetchJobs(record.id)}
176
- loading={loadingJobs[record.id]}
177
- >
178
- Jobs
179
- </Button>
180
- {(record.status === 0 || record.status === null) && (
181
- <Popconfirm title="Cancel execution?" onConfirm={() => handleCancel(record.id)}>
182
- <Button type="link" size="small" icon={<StopOutlined />} danger />
183
- </Popconfirm>
184
- )}
185
- </Space>
186
- ),
187
- },
188
- ];
189
-
190
- return (
191
- <div>
192
- <Space style={{ marginBottom: 16 }}>
193
- <Select
194
- placeholder="Filter by status"
195
- allowClear
196
- style={{ width: 160 }}
197
- value={statusFilter}
198
- onChange={setStatusFilter}
199
- options={Object.entries(EXEC_STATUS).map(([k, v]) => ({ value: k, label: v.label }))}
200
- />
201
- <Button icon={<ReloadOutlined />} onClick={() => fetchData()}>
202
- Refresh
203
- </Button>
204
- <Dropdown menu={{ items: purgeItems }} trigger={['click']}>
205
- <Button danger>Clear History</Button>
206
- </Dropdown>
207
- </Space>
208
- <Table
209
- rowKey="id"
210
- columns={columns}
211
- dataSource={data}
212
- loading={loading}
213
- size="small"
214
- expandable={{
215
- expandedRowRender: (record) => {
216
- const jobs = expandedJobs[record.id];
217
- if (!jobs) return <div style={{ padding: 8 }}>Click "Jobs" to load</div>;
218
- return (
219
- <Table
220
- rowKey="id"
221
- columns={jobColumns}
222
- dataSource={jobs}
223
- size="small"
224
- pagination={false}
225
- />
226
- );
227
- },
228
- }}
229
- pagination={{
230
- ...pagination,
231
- showSizeChanger: true,
232
- showTotal: (total) => `Total ${total}`,
233
- onChange: (page, pageSize) => fetchData(page, pageSize),
234
- }}
235
- />
236
- </div>
237
- );
238
- }
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
+ import { Table, Tag, Button, Space, Popconfirm, message, Select, Dropdown } from 'antd';
3
+ import { ReloadOutlined, StopOutlined, UnorderedListOutlined } from '@ant-design/icons';
4
+ import { useApp } from '@nocobase/client-v2';
5
+ import dayjs from 'dayjs';
6
+
7
+ const EXEC_STATUS: Record<string, { label: string; color: string }> = {
8
+ null: { label: 'Queueing', color: 'default' },
9
+ '0': { label: 'Started', color: 'processing' },
10
+ '1': { label: 'Resolved', color: 'success' },
11
+ '-1': { label: 'Failed', color: 'error' },
12
+ '-2': { label: 'Error', color: 'error' },
13
+ '-3': { label: 'Aborted', color: 'warning' },
14
+ '-4': { label: 'Canceled', color: 'warning' },
15
+ '-5': { label: 'Rejected', color: 'warning' },
16
+ '-6': { label: 'Retry Needed', color: 'orange' },
17
+ };
18
+
19
+ const JOB_STATUS: Record<string, { label: string; color: string }> = {
20
+ '0': { label: 'Pending', color: 'default' },
21
+ '1': { label: 'Resolved', color: 'success' },
22
+ '-1': { label: 'Failed', color: 'error' },
23
+ '-2': { label: 'Error', color: 'error' },
24
+ '-3': { label: 'Aborted', color: 'warning' },
25
+ '-4': { label: 'Canceled', color: 'warning' },
26
+ };
27
+
28
+ export function WorkflowExecutions() {
29
+ const api = useApp().apiClient;
30
+ const [data, setData] = useState<any[]>([]);
31
+ const [loading, setLoading] = useState(false);
32
+ const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
33
+ const [statusFilter, setStatusFilter] = useState<string | undefined>(undefined);
34
+ const [expandedJobs, setExpandedJobs] = useState<Record<string, any[]>>({});
35
+ const [loadingJobs, setLoadingJobs] = useState<Record<string, boolean>>({});
36
+
37
+ const fetchData = useCallback(
38
+ async (page = pagination.current, pageSize = pagination.pageSize) => {
39
+ setLoading(true);
40
+ try {
41
+ const res = await api.request({
42
+ url: 'clusterManagerWorkflow:list',
43
+ params: { page, pageSize, statusFilter: statusFilter === undefined ? '' : statusFilter },
44
+ });
45
+ const body = res.data;
46
+ const rows = Array.isArray(body?.data?.data)
47
+ ? body.data.data
48
+ : Array.isArray(body?.data)
49
+ ? body.data
50
+ : Array.isArray(body)
51
+ ? body
52
+ : [];
53
+ setData(rows);
54
+ setPagination((prev) => ({
55
+ ...prev,
56
+ current: body.meta?.page || page,
57
+ total: body.meta?.count || 0,
58
+ }));
59
+ } catch {
60
+ message.error('Failed to load executions');
61
+ } finally {
62
+ setLoading(false);
63
+ }
64
+ },
65
+ [api, pagination.current, pagination.pageSize, statusFilter],
66
+ );
67
+
68
+ useEffect(() => {
69
+ fetchData();
70
+ }, [statusFilter]);
71
+
72
+ const fetchJobs = async (executionId: string) => {
73
+ if (expandedJobs[executionId]) return;
74
+ setLoadingJobs((prev) => ({ ...prev, [executionId]: true }));
75
+ try {
76
+ const res = await api.request({
77
+ url: 'clusterManagerWorkflow:getJobs',
78
+ params: { filterByTk: executionId },
79
+ });
80
+ const jobs = Array.isArray(res?.data?.data?.data)
81
+ ? res.data.data.data
82
+ : Array.isArray(res?.data?.data)
83
+ ? res.data.data
84
+ : Array.isArray(res?.data)
85
+ ? res.data
86
+ : [];
87
+ setExpandedJobs((prev) => ({ ...prev, [executionId]: jobs }));
88
+ } catch {
89
+ message.error('Failed to load jobs');
90
+ } finally {
91
+ setLoadingJobs((prev) => ({ ...prev, [executionId]: false }));
92
+ }
93
+ };
94
+
95
+ const handleCancel = async (id: string) => {
96
+ await api.request({ url: 'clusterManagerWorkflow:cancel', params: { filterByTk: id } });
97
+ message.success('Execution canceled');
98
+ fetchData();
99
+ };
100
+
101
+ const handlePurge = async (days: number) => {
102
+ try {
103
+ const res = await api.request({ url: `clusterManagerWorkflow:purge`, method: 'post', data: { days } });
104
+ message.success(`Purged ${res?.data?.deletedCount || 0} executions`);
105
+ fetchData();
106
+ } catch {
107
+ message.error('Failed to purge executions');
108
+ }
109
+ };
110
+
111
+ const purgeItems = [
112
+ { key: '7', label: 'Older than 7 days', onClick: () => handlePurge(7) },
113
+ { key: '30', label: 'Older than 30 days', onClick: () => handlePurge(30) },
114
+ { key: '0', label: 'All completed/failed', danger: true, onClick: () => handlePurge(0) },
115
+ ];
116
+
117
+ const jobColumns = [
118
+ { title: 'Job ID', dataIndex: 'id', width: 100 },
119
+ {
120
+ title: 'Node',
121
+ key: 'node',
122
+ width: 200,
123
+ render: (_: any, r: any) => r.node?.title || r.nodeKey || '-',
124
+ },
125
+ {
126
+ title: 'Status',
127
+ dataIndex: 'status',
128
+ width: 100,
129
+ render: (val: number) => {
130
+ const s = JOB_STATUS[String(val)] || { label: String(val), color: 'default' };
131
+ return <Tag color={s.color}>{s.label}</Tag>;
132
+ },
133
+ },
134
+ {
135
+ title: 'Result',
136
+ dataIndex: 'result',
137
+ ellipsis: true,
138
+ render: (val: any) => (val ? JSON.stringify(val).slice(0, 120) : '-'),
139
+ },
140
+ ];
141
+
142
+ const columns = [
143
+ { title: 'ID', dataIndex: 'id', width: 100 },
144
+ {
145
+ title: 'Workflow',
146
+ key: 'workflow',
147
+ width: 200,
148
+ render: (_: any, r: any) => r.workflow?.title || '-',
149
+ },
150
+ {
151
+ title: 'Status',
152
+ dataIndex: 'status',
153
+ width: 120,
154
+ render: (val: number | null) => {
155
+ const s = EXEC_STATUS[String(val)] || { label: String(val), color: 'default' };
156
+ return <Tag color={s.color}>{s.label}</Tag>;
157
+ },
158
+ },
159
+ {
160
+ title: 'Executing Node',
161
+ dataIndex: 'workerNode',
162
+ width: 150,
163
+ render: (val: string) => (val && val !== '-' ? <Tag color="blue">{val}</Tag> : <Tag>{val || '-'}</Tag>),
164
+ },
165
+ {
166
+ title: 'Manual',
167
+ dataIndex: 'manually',
168
+ width: 80,
169
+ render: (val: boolean) => (val ? <Tag color="blue">Yes</Tag> : '-'),
170
+ },
171
+ {
172
+ title: 'Triggered At',
173
+ dataIndex: 'createdAt',
174
+ width: 160,
175
+ render: (val: string) => (val ? dayjs(val).format('YYYY-MM-DD HH:mm:ss') : '-'),
176
+ },
177
+ {
178
+ title: 'Actions',
179
+ key: 'actions',
180
+ width: 120,
181
+ render: (_: any, record: any) => (
182
+ <Space>
183
+ <Button
184
+ type="link"
185
+ size="small"
186
+ icon={<UnorderedListOutlined />}
187
+ onClick={() => fetchJobs(record.id)}
188
+ loading={loadingJobs[record.id]}
189
+ >
190
+ Jobs
191
+ </Button>
192
+ {(record.status === 0 || record.status === null) && (
193
+ <Popconfirm title="Cancel execution?" onConfirm={() => handleCancel(record.id)}>
194
+ <Button type="link" size="small" icon={<StopOutlined />} danger />
195
+ </Popconfirm>
196
+ )}
197
+ </Space>
198
+ ),
199
+ },
200
+ ];
201
+
202
+ return (
203
+ <div>
204
+ <Space style={{ marginBottom: 16 }}>
205
+ <Select
206
+ placeholder="Filter by status"
207
+ allowClear
208
+ style={{ width: 160 }}
209
+ value={statusFilter}
210
+ onChange={setStatusFilter}
211
+ options={Object.entries(EXEC_STATUS).map(([k, v]) => ({ value: k, label: v.label }))}
212
+ />
213
+ <Button icon={<ReloadOutlined />} onClick={() => fetchData()}>
214
+ Refresh
215
+ </Button>
216
+ <Dropdown menu={{ items: purgeItems }} trigger={['click']}>
217
+ <Button danger>Clear History</Button>
218
+ </Dropdown>
219
+ </Space>
220
+ <Table
221
+ rowKey="id"
222
+ columns={columns}
223
+ dataSource={data}
224
+ loading={loading}
225
+ size="small"
226
+ scroll={{ x: 'max-content' }}
227
+ expandable={{
228
+ expandedRowRender: (record) => {
229
+ const jobs = expandedJobs[record.id];
230
+ if (!jobs) return <div style={{ padding: 8 }}>Click &quot;Jobs&quot; to load</div>;
231
+ return <Table rowKey="id" columns={jobColumns} dataSource={jobs} size="small" pagination={false} />;
232
+ },
233
+ }}
234
+ pagination={{
235
+ ...pagination,
236
+ showSizeChanger: true,
237
+ showTotal: (total) => `Total ${total}`,
238
+ onChange: (page, pageSize) => fetchData(page, pageSize),
239
+ }}
240
+ />
241
+ </div>
242
+ );
243
+ }
@@ -1,5 +1,5 @@
1
1
  import { useCallback } from 'react';
2
- import { useApp } from '@nocobase/client';
2
+ import { useApp } from '@nocobase/client-v2';
3
3
 
4
4
  const namespace = 'cluster-manager';
5
5
 
@@ -0,0 +1,24 @@
1
+ import { Plugin, Application } from '@nocobase/client-v2';
2
+ import React from 'react';
3
+
4
+ export class PluginClusterManagerClient extends Plugin<Record<string, never>, Application> {
5
+ async load() {
6
+ this.pluginSettingsManager.addMenuItem({
7
+ key: 'plugin-cluster-manager',
8
+ title: this.t('Cluster Manager'),
9
+ icon: 'DashboardOutlined',
10
+
11
+ });
12
+
13
+ this.pluginSettingsManager.addPageTabItem({
14
+ menuKey: 'plugin-cluster-manager',
15
+ key: 'index',
16
+ title: this.t('Cluster Manager'),
17
+
18
+ componentLoader: () => import('../client/ClusterManagerLayout').then(m => ({ default: m.ClusterManagerLayout })),
19
+ });
20
+
21
+ }
22
+ }
23
+
24
+ export default PluginClusterManagerClient;