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,234 +1,234 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
- import { Alert, Button, Input, Popconfirm, Space, Spin, Table, Tag, Typography, message } from 'antd';
3
- import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
5
- import { useT } from './utils';
6
-
7
- interface PluginRecord {
8
- name: string;
9
- packageName?: string;
10
- displayName?: string;
11
- description?: string;
12
- version?: string;
13
- enabled?: boolean;
14
- installed?: boolean;
15
- loaded?: boolean;
16
- protected?: boolean;
17
- }
18
-
19
- function getErrorMessage(err: any, fallback: string) {
20
- return err?.response?.data?.errors?.[0]?.message || err?.message || fallback;
21
- }
22
-
23
- export function PluginOperations() {
24
- const t = useT();
25
- const api = useAPIClient();
26
- const [loading, setLoading] = useState(false);
27
- const [actionKey, setActionKey] = useState<string | null>(null);
28
- const [search, setSearch] = useState('');
29
- const [plugins, setPlugins] = useState<PluginRecord[]>([]);
30
-
31
- const fetchData = useCallback(async () => {
32
- setLoading(true);
33
- try {
34
- const res = await api.request({ url: 'clusterManagerPlugins:list' });
35
- const data = Array.isArray(res?.data?.data?.data)
36
- ? res.data.data.data
37
- : Array.isArray(res?.data?.data)
38
- ? res.data.data
39
- : Array.isArray(res?.data)
40
- ? res.data
41
- : [];
42
- setPlugins(data);
43
- } catch (err: any) {
44
- message.error(getErrorMessage(err, t('Failed to load plugins')));
45
- } finally {
46
- setLoading(false);
47
- }
48
- }, [api, t]);
49
-
50
- useEffect(() => {
51
- fetchData();
52
- }, [fetchData]);
53
-
54
- const filteredPlugins = useMemo(() => {
55
- const keyword = search.trim().toLowerCase();
56
- const list = Array.isArray(plugins) ? plugins : [];
57
- if (!keyword) return list;
58
- return list.filter((plugin) =>
59
- [plugin.name, plugin.packageName, plugin.displayName, plugin.description]
60
- .filter(Boolean)
61
- .some((value) => String(value).toLowerCase().includes(keyword)),
62
- );
63
- }, [plugins, search]);
64
-
65
- const handleForceDisable = async (record: PluginRecord) => {
66
- const key = `${record.name}:disable`;
67
- setActionKey(key);
68
- try {
69
- const res = await api.request({
70
- url: 'clusterManagerPlugins:forceDisable',
71
- method: 'post',
72
- data: { name: record.name },
73
- });
74
- message.success(res?.data?.data?.message || res?.data?.message || t('Plugin force disabled'));
75
- await fetchData();
76
- } catch (err: any) {
77
- message.error(getErrorMessage(err, t('Failed to force disable plugin')));
78
- } finally {
79
- setActionKey(null);
80
- }
81
- };
82
-
83
- const handleForceRemove = async (record: PluginRecord) => {
84
- const key = `${record.name}:remove`;
85
- setActionKey(key);
86
- try {
87
- const res = await api.request({
88
- url: 'clusterManagerPlugins:forceRemove',
89
- method: 'post',
90
- data: { name: record.name },
91
- });
92
- message.success(res?.data?.data?.message || res?.data?.message || t('Plugin force removed'));
93
- await fetchData();
94
- } catch (err: any) {
95
- message.error(getErrorMessage(err, t('Failed to force remove plugin')));
96
- } finally {
97
- setActionKey(null);
98
- }
99
- };
100
-
101
- const columns = [
102
- {
103
- title: t('Plugin'),
104
- dataIndex: 'displayName',
105
- key: 'displayName',
106
- render: (_: string, record: PluginRecord) => (
107
- <Space direction="vertical" size={0}>
108
- <Typography.Text strong>{record.displayName || record.name}</Typography.Text>
109
- <Typography.Text type="secondary" style={{ fontSize: 12 }}>
110
- {record.packageName || record.name}
111
- </Typography.Text>
112
- </Space>
113
- ),
114
- },
115
- {
116
- title: t('Name'),
117
- dataIndex: 'name',
118
- key: 'name',
119
- width: 220,
120
- render: (name: string) => <code style={{ fontSize: 12 }}>{name}</code>,
121
- },
122
- {
123
- title: t('Status'),
124
- key: 'status',
125
- width: 260,
126
- render: (_: any, record: PluginRecord) => (
127
- <Space wrap size={[4, 4]}>
128
- <Tag color={record.enabled ? 'green' : 'default'}>
129
- {record.enabled ? t('Enabled') : t('Disabled')}
130
- </Tag>
131
- <Tag color={record.installed ? 'blue' : 'default'}>
132
- {record.installed ? t('Installed') : t('Not installed')}
133
- </Tag>
134
- <Tag color={record.loaded ? 'processing' : 'default'}>
135
- {record.loaded ? t('Loaded') : t('Not loaded')}
136
- </Tag>
137
- {record.protected && <Tag color="red">{t('Protected')}</Tag>}
138
- </Space>
139
- ),
140
- },
141
- {
142
- title: t('Version'),
143
- dataIndex: 'version',
144
- key: 'version',
145
- width: 120,
146
- render: (version: string) => version || '-',
147
- },
148
- {
149
- title: t('Description'),
150
- dataIndex: 'description',
151
- key: 'description',
152
- ellipsis: true,
153
- render: (description: string) => description || '-',
154
- },
155
- {
156
- title: t('Actions'),
157
- key: 'actions',
158
- width: 250,
159
- render: (_: any, record: PluginRecord) => (
160
- <Space>
161
- <Popconfirm
162
- title={t('Force disable this plugin?')}
163
- description={t('This updates the plugin registry directly. Restart or reload is required to fully unload runtime hooks.')}
164
- disabled={record.protected || !record.enabled}
165
- onConfirm={() => handleForceDisable(record)}
166
- okText={t('Force disable')}
167
- cancelText={t('Cancel')}
168
- >
169
- <Button
170
- size="small"
171
- icon={<StopOutlined />}
172
- disabled={record.protected || !record.enabled}
173
- loading={actionKey === `${record.name}:disable`}
174
- >
175
- {t('Force disable')}
176
- </Button>
177
- </Popconfirm>
178
- <Popconfirm
179
- title={t('Force remove this plugin?')}
180
- description={t('This removes the plugin registry record. Package files are not deleted. Restart or reload is required.')}
181
- disabled={record.protected}
182
- onConfirm={() => handleForceRemove(record)}
183
- okText={t('Force remove')}
184
- cancelText={t('Cancel')}
185
- >
186
- <Button
187
- size="small"
188
- danger
189
- icon={<DeleteOutlined />}
190
- disabled={record.protected}
191
- loading={actionKey === `${record.name}:remove`}
192
- >
193
- {t('Force remove')}
194
- </Button>
195
- </Popconfirm>
196
- </Space>
197
- ),
198
- },
199
- ];
200
-
201
- return (
202
- <Spin spinning={loading}>
203
- <Space direction="vertical" style={{ width: '100%' }} size="middle">
204
- <Alert
205
- type="warning"
206
- showIcon
207
- icon={<ExclamationCircleOutlined />}
208
- message={t('Force operations bypass plugin lifecycle hooks')}
209
- description={t('Use this only when the normal plugin manager cannot disable or remove a broken plugin. Restart or reload the app after a successful operation.')}
210
- />
211
- <Space>
212
- <Button icon={<ReloadOutlined />} onClick={fetchData}>
213
- {t('Refresh')}
214
- </Button>
215
- <Input.Search
216
- allowClear
217
- placeholder={t('Search plugins')}
218
- value={search}
219
- onChange={(event) => setSearch(event.target.value)}
220
- style={{ width: 320 }}
221
- />
222
- </Space>
223
- <Table
224
- rowKey="name"
225
- size="small"
226
- dataSource={filteredPlugins}
227
- columns={columns}
228
- pagination={{ pageSize: 20 }}
229
- scroll={{ x: 1100 }}
230
- />
231
- </Space>
232
- </Spin>
233
- );
234
- }
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { Alert, Button, Input, Popconfirm, Space, Spin, Table, Tag, Typography, message } from 'antd';
3
+ import { DeleteOutlined, ExclamationCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
4
+ import { useAPIClient } from '@nocobase/client';
5
+ import { useT } from './utils';
6
+
7
+ interface PluginRecord {
8
+ name: string;
9
+ packageName?: string;
10
+ displayName?: string;
11
+ description?: string;
12
+ version?: string;
13
+ enabled?: boolean;
14
+ installed?: boolean;
15
+ loaded?: boolean;
16
+ protected?: boolean;
17
+ }
18
+
19
+ function getErrorMessage(err: any, fallback: string) {
20
+ return err?.response?.data?.errors?.[0]?.message || err?.message || fallback;
21
+ }
22
+
23
+ export function PluginOperations() {
24
+ const t = useT();
25
+ const api = useAPIClient();
26
+ const [loading, setLoading] = useState(false);
27
+ const [actionKey, setActionKey] = useState<string | null>(null);
28
+ const [search, setSearch] = useState('');
29
+ const [plugins, setPlugins] = useState<PluginRecord[]>([]);
30
+
31
+ const fetchData = useCallback(async () => {
32
+ setLoading(true);
33
+ try {
34
+ const res = await api.request({ url: 'clusterManagerPlugins:list' });
35
+ const data = Array.isArray(res?.data?.data?.data)
36
+ ? res.data.data.data
37
+ : Array.isArray(res?.data?.data)
38
+ ? res.data.data
39
+ : Array.isArray(res?.data)
40
+ ? res.data
41
+ : [];
42
+ setPlugins(data);
43
+ } catch (err: any) {
44
+ message.error(getErrorMessage(err, t('Failed to load plugins')));
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ }, [api, t]);
49
+
50
+ useEffect(() => {
51
+ fetchData();
52
+ }, [fetchData]);
53
+
54
+ const filteredPlugins = useMemo(() => {
55
+ const keyword = search.trim().toLowerCase();
56
+ const list = Array.isArray(plugins) ? plugins : [];
57
+ if (!keyword) return list;
58
+ return list.filter((plugin) =>
59
+ [plugin.name, plugin.packageName, plugin.displayName, plugin.description]
60
+ .filter(Boolean)
61
+ .some((value) => String(value).toLowerCase().includes(keyword)),
62
+ );
63
+ }, [plugins, search]);
64
+
65
+ const handleForceDisable = async (record: PluginRecord) => {
66
+ const key = `${record.name}:disable`;
67
+ setActionKey(key);
68
+ try {
69
+ const res = await api.request({
70
+ url: 'clusterManagerPlugins:forceDisable',
71
+ method: 'post',
72
+ data: { name: record.name },
73
+ });
74
+ message.success(res?.data?.data?.message || res?.data?.message || t('Plugin force disabled'));
75
+ await fetchData();
76
+ } catch (err: any) {
77
+ message.error(getErrorMessage(err, t('Failed to force disable plugin')));
78
+ } finally {
79
+ setActionKey(null);
80
+ }
81
+ };
82
+
83
+ const handleForceRemove = async (record: PluginRecord) => {
84
+ const key = `${record.name}:remove`;
85
+ setActionKey(key);
86
+ try {
87
+ const res = await api.request({
88
+ url: 'clusterManagerPlugins:forceRemove',
89
+ method: 'post',
90
+ data: { name: record.name },
91
+ });
92
+ message.success(res?.data?.data?.message || res?.data?.message || t('Plugin force removed'));
93
+ await fetchData();
94
+ } catch (err: any) {
95
+ message.error(getErrorMessage(err, t('Failed to force remove plugin')));
96
+ } finally {
97
+ setActionKey(null);
98
+ }
99
+ };
100
+
101
+ const columns = [
102
+ {
103
+ title: t('Plugin'),
104
+ dataIndex: 'displayName',
105
+ key: 'displayName',
106
+ render: (_: string, record: PluginRecord) => (
107
+ <Space direction="vertical" size={0}>
108
+ <Typography.Text strong>{record.displayName || record.name}</Typography.Text>
109
+ <Typography.Text type="secondary" style={{ fontSize: 12 }}>
110
+ {record.packageName || record.name}
111
+ </Typography.Text>
112
+ </Space>
113
+ ),
114
+ },
115
+ {
116
+ title: t('Name'),
117
+ dataIndex: 'name',
118
+ key: 'name',
119
+ width: 220,
120
+ render: (name: string) => <code style={{ fontSize: 12 }}>{name}</code>,
121
+ },
122
+ {
123
+ title: t('Status'),
124
+ key: 'status',
125
+ width: 260,
126
+ render: (_: any, record: PluginRecord) => (
127
+ <Space wrap size={[4, 4]}>
128
+ <Tag color={record.enabled ? 'green' : 'default'}>
129
+ {record.enabled ? t('Enabled') : t('Disabled')}
130
+ </Tag>
131
+ <Tag color={record.installed ? 'blue' : 'default'}>
132
+ {record.installed ? t('Installed') : t('Not installed')}
133
+ </Tag>
134
+ <Tag color={record.loaded ? 'processing' : 'default'}>
135
+ {record.loaded ? t('Loaded') : t('Not loaded')}
136
+ </Tag>
137
+ {record.protected && <Tag color="red">{t('Protected')}</Tag>}
138
+ </Space>
139
+ ),
140
+ },
141
+ {
142
+ title: t('Version'),
143
+ dataIndex: 'version',
144
+ key: 'version',
145
+ width: 120,
146
+ render: (version: string) => version || '-',
147
+ },
148
+ {
149
+ title: t('Description'),
150
+ dataIndex: 'description',
151
+ key: 'description',
152
+ ellipsis: true,
153
+ render: (description: string) => description || '-',
154
+ },
155
+ {
156
+ title: t('Actions'),
157
+ key: 'actions',
158
+ width: 250,
159
+ render: (_: any, record: PluginRecord) => (
160
+ <Space>
161
+ <Popconfirm
162
+ title={t('Force disable this plugin?')}
163
+ description={t('This updates the plugin registry directly. Restart or reload is required to fully unload runtime hooks.')}
164
+ disabled={record.protected || !record.enabled}
165
+ onConfirm={() => handleForceDisable(record)}
166
+ okText={t('Force disable')}
167
+ cancelText={t('Cancel')}
168
+ >
169
+ <Button
170
+ size="small"
171
+ icon={<StopOutlined />}
172
+ disabled={record.protected || !record.enabled}
173
+ loading={actionKey === `${record.name}:disable`}
174
+ >
175
+ {t('Force disable')}
176
+ </Button>
177
+ </Popconfirm>
178
+ <Popconfirm
179
+ title={t('Force remove this plugin?')}
180
+ description={t('This removes the plugin registry record. Package files are not deleted. Restart or reload is required.')}
181
+ disabled={record.protected}
182
+ onConfirm={() => handleForceRemove(record)}
183
+ okText={t('Force remove')}
184
+ cancelText={t('Cancel')}
185
+ >
186
+ <Button
187
+ size="small"
188
+ danger
189
+ icon={<DeleteOutlined />}
190
+ disabled={record.protected}
191
+ loading={actionKey === `${record.name}:remove`}
192
+ >
193
+ {t('Force remove')}
194
+ </Button>
195
+ </Popconfirm>
196
+ </Space>
197
+ ),
198
+ },
199
+ ];
200
+
201
+ return (
202
+ <Spin spinning={loading}>
203
+ <Space direction="vertical" style={{ width: '100%' }} size="middle">
204
+ <Alert
205
+ type="warning"
206
+ showIcon
207
+ icon={<ExclamationCircleOutlined />}
208
+ message={t('Force operations bypass plugin lifecycle hooks')}
209
+ description={t('Use this only when the normal plugin manager cannot disable or remove a broken plugin. Restart or reload the app after a successful operation.')}
210
+ />
211
+ <Space>
212
+ <Button icon={<ReloadOutlined />} onClick={fetchData}>
213
+ {t('Refresh')}
214
+ </Button>
215
+ <Input.Search
216
+ allowClear
217
+ placeholder={t('Search plugins')}
218
+ value={search}
219
+ onChange={(event) => setSearch(event.target.value)}
220
+ style={{ width: 320 }}
221
+ />
222
+ </Space>
223
+ <Table
224
+ rowKey="name"
225
+ size="small"
226
+ dataSource={filteredPlugins}
227
+ columns={columns}
228
+ pagination={{ pageSize: 20 }}
229
+ scroll={{ x: 1100 }}
230
+ />
231
+ </Space>
232
+ </Spin>
233
+ );
234
+ }