plugin-cluster-manager 1.1.16 → 1.1.17
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.
- package/dist/client/index.js +1 -1
- package/dist/client-v2/376.cd1d86e85a50088e.js +10 -0
- package/dist/client-v2/index.js +1 -1
- package/dist/externalVersion.js +6 -6
- package/dist/locale/en-US.json +16 -6
- package/dist/locale/vi-VN.json +16 -6
- package/dist/locale/zh-CN.json +16 -6
- package/dist/server/actions/cluster-nodes.js +43 -10
- package/dist/server/actions/doctor.js +69 -4
- package/dist/server/actions/event-queue-monitor.js +33 -3
- package/dist/server/actions/orchestrator.js +48 -32
- package/dist/server/actions/queue-mappings.js +1 -0
- package/dist/server/actions/tasks.js +8 -8
- package/dist/server/adapters/redis-event-queue-adapter.js +188 -0
- package/dist/server/adapters/redis-node-registry.js +42 -3
- package/dist/server/collections/orchestrator-stacks.js +6 -0
- package/dist/server/collections/worker-queue-mappings.js +1 -1
- package/dist/server/orchestrator/PackageManager.js +47 -12
- package/dist/server/plugin.js +36 -5
- package/dist/server/queue-scanner.js +54 -34
- package/dist/server/utils/node.js +3 -7
- package/dist/server/utils/redis.js +37 -9
- package/dist/shared/worker-processes.js +233 -0
- package/package.json +1 -1
- package/src/client/ClusterNodes.tsx +76 -10
- package/src/client/ContainerOrchestrator.tsx +146 -8
- package/src/client/QueueAssignment.tsx +10 -2
- package/src/locale/en-US.json +16 -6
- package/src/locale/vi-VN.json +16 -6
- package/src/locale/zh-CN.json +16 -6
- package/src/server/__tests__/worker-processes.test.ts +42 -0
- package/src/server/actions/cluster-nodes.ts +43 -8
- package/src/server/actions/doctor.ts +77 -0
- package/src/server/actions/event-queue-monitor.ts +34 -3
- package/src/server/actions/orchestrator.ts +58 -38
- package/src/server/actions/queue-mappings.ts +1 -0
- package/src/server/actions/tasks.ts +142 -142
- package/src/server/adapters/redis-event-queue-adapter.ts +189 -0
- package/src/server/adapters/redis-node-registry.ts +44 -4
- package/src/server/collections/orchestrator-stacks.ts +6 -0
- package/src/server/collections/worker-queue-mappings.ts +3 -3
- package/src/server/orchestrator/PackageManager.ts +48 -11
- package/src/server/orchestrator/types.ts +5 -4
- package/src/server/plugin.ts +40 -6
- package/src/server/queue-scanner.ts +65 -51
- package/src/server/utils/node.ts +3 -10
- package/src/server/utils/redis.ts +39 -4
- package/src/shared/worker-processes.ts +216 -0
- package/dist/client-v2/914.c0bce51908fd81d7.js +0 -10
|
@@ -27,21 +27,54 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
27
27
|
var redis_exports = {};
|
|
28
28
|
__export(redis_exports, {
|
|
29
29
|
deleteKeysChunked: () => deleteKeysChunked,
|
|
30
|
+
getClusterRedisUrl: () => getClusterRedisUrl,
|
|
30
31
|
getRedis: () => getRedis,
|
|
31
32
|
getRedisClient: () => getRedisClient,
|
|
32
33
|
getRedisOrThrow: () => getRedisOrThrow,
|
|
34
|
+
isClusterRedisConfigured: () => isClusterRedisConfigured,
|
|
33
35
|
scanKeys: () => scanKeys
|
|
34
36
|
});
|
|
35
37
|
module.exports = __toCommonJS(redis_exports);
|
|
36
38
|
var import_redis = require("redis");
|
|
37
39
|
let globalRedisClient = null;
|
|
40
|
+
const CLUSTER_REDIS_CONNECTION = "cluster-manager:registry";
|
|
41
|
+
const CLUSTER_REDIS_ENV_CANDIDATES = [
|
|
42
|
+
"NODE_REGISTRY_REDIS_URL",
|
|
43
|
+
"CLUSTER_MANAGER_REDIS_URL",
|
|
44
|
+
"REDIS_URL",
|
|
45
|
+
"CACHE_REDIS_URL",
|
|
46
|
+
"PUBSUB_ADAPTER_REDIS_URL",
|
|
47
|
+
"QUEUE_ADAPTER_REDIS_URL",
|
|
48
|
+
"LOCK_ADAPTER_REDIS_URL"
|
|
49
|
+
];
|
|
50
|
+
function getClusterRedisUrl() {
|
|
51
|
+
for (const key of CLUSTER_REDIS_ENV_CANDIDATES) {
|
|
52
|
+
const value = process.env[key];
|
|
53
|
+
if (value == null ? void 0 : value.trim()) {
|
|
54
|
+
return value.trim();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
function isClusterRedisConfigured(app) {
|
|
60
|
+
var _a;
|
|
61
|
+
if (getClusterRedisUrl()) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const manager = app == null ? void 0 : app.redisConnectionManager;
|
|
65
|
+
return Boolean((_a = manager == null ? void 0 : manager.getConnection) == null ? void 0 : _a.call(manager));
|
|
66
|
+
}
|
|
38
67
|
function getRedisClient(app) {
|
|
39
68
|
if (globalRedisClient) return globalRedisClient;
|
|
69
|
+
const url = getClusterRedisUrl();
|
|
40
70
|
if (app == null ? void 0 : app.redisConnectionManager) {
|
|
71
|
+
if (url) {
|
|
72
|
+
const conn2 = app.redisConnectionManager.getConnection(CLUSTER_REDIS_CONNECTION, { connectionString: url });
|
|
73
|
+
if (conn2) return conn2;
|
|
74
|
+
}
|
|
41
75
|
const conn = app.redisConnectionManager.getConnection();
|
|
42
76
|
if (conn) return conn;
|
|
43
77
|
}
|
|
44
|
-
const url = process.env.REDIS_URL || process.env.CACHE_REDIS_URL || process.env.PUBSUB_ADAPTER_REDIS_URL;
|
|
45
78
|
if (!url) return null;
|
|
46
79
|
globalRedisClient = (0, import_redis.createClient)({ url });
|
|
47
80
|
globalRedisClient.connect().catch((err) => {
|
|
@@ -64,14 +97,7 @@ async function scanKeys(redis, pattern, batchSize = 200) {
|
|
|
64
97
|
const keys = [];
|
|
65
98
|
let cursor = "0";
|
|
66
99
|
do {
|
|
67
|
-
const result = await redis.sendCommand([
|
|
68
|
-
"SCAN",
|
|
69
|
-
cursor,
|
|
70
|
-
"MATCH",
|
|
71
|
-
pattern,
|
|
72
|
-
"COUNT",
|
|
73
|
-
String(batchSize)
|
|
74
|
-
]);
|
|
100
|
+
const result = await redis.sendCommand(["SCAN", cursor, "MATCH", pattern, "COUNT", String(batchSize)]);
|
|
75
101
|
cursor = String(result[0]);
|
|
76
102
|
if (Array.isArray(result[1])) {
|
|
77
103
|
keys.push(...result[1]);
|
|
@@ -96,8 +122,10 @@ async function deleteKeysChunked(redis, keys, chunkSize = 500) {
|
|
|
96
122
|
// Annotate the CommonJS export names for ESM import in node:
|
|
97
123
|
0 && (module.exports = {
|
|
98
124
|
deleteKeysChunked,
|
|
125
|
+
getClusterRedisUrl,
|
|
99
126
|
getRedis,
|
|
100
127
|
getRedisClient,
|
|
101
128
|
getRedisOrThrow,
|
|
129
|
+
isClusterRedisConfigured,
|
|
102
130
|
scanKeys
|
|
103
131
|
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var worker_processes_exports = {};
|
|
28
|
+
__export(worker_processes_exports, {
|
|
29
|
+
WORKER_PROCESS_DEFINITIONS: () => WORKER_PROCESS_DEFINITIONS,
|
|
30
|
+
getCommonWorkerProcesses: () => getCommonWorkerProcesses,
|
|
31
|
+
getWorkerProcessByChannel: () => getWorkerProcessByChannel,
|
|
32
|
+
getWorkerProcessDefinition: () => getWorkerProcessDefinition,
|
|
33
|
+
isAppServingMode: () => isAppServingMode,
|
|
34
|
+
isWorkerOnlyMode: () => isWorkerOnlyMode,
|
|
35
|
+
normalizeWorkerMode: () => normalizeWorkerMode,
|
|
36
|
+
resolveWorkerProcessName: () => resolveWorkerProcessName,
|
|
37
|
+
workerModeServesProcess: () => workerModeServesProcess,
|
|
38
|
+
workerModeTokens: () => workerModeTokens
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(worker_processes_exports);
|
|
41
|
+
const WORKER_PROCESS_DEFINITIONS = [
|
|
42
|
+
{
|
|
43
|
+
name: "workflow:process",
|
|
44
|
+
label: "Workflow",
|
|
45
|
+
description: "Process workflow executions",
|
|
46
|
+
kind: "event-queue",
|
|
47
|
+
aliases: ["workflow.pendingExecution", "@nocobase/plugin-workflow.pendingExecution"],
|
|
48
|
+
common: true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "async-task:process",
|
|
52
|
+
label: "Async Tasks",
|
|
53
|
+
description: "Execute async tasks",
|
|
54
|
+
kind: "event-queue",
|
|
55
|
+
aliases: ["async-task-manager.task", "@nocobase/plugin-async-task-manager.task"],
|
|
56
|
+
common: true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "notification:send",
|
|
60
|
+
label: "Notifications",
|
|
61
|
+
description: "Send notification messages",
|
|
62
|
+
kind: "event-queue",
|
|
63
|
+
aliases: ["notification-manager.send", "@nocobase/plugin-notification-manager.send"],
|
|
64
|
+
common: true
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "knowledge-base:document-vectorize",
|
|
68
|
+
label: "Document Vectorization",
|
|
69
|
+
description: "Vectorize knowledge base documents",
|
|
70
|
+
kind: "event-queue",
|
|
71
|
+
aliases: ["knowledge-base:document-vectorize"],
|
|
72
|
+
pluginName: "plugin-knowledge-base",
|
|
73
|
+
common: true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "git-review:process",
|
|
77
|
+
label: "Git Review",
|
|
78
|
+
description: "AI code review jobs",
|
|
79
|
+
kind: "redis-list",
|
|
80
|
+
aliases: ["plugin-git-manager.review"],
|
|
81
|
+
redisKeySuffixes: [":plugin-git-manager:review:queue"],
|
|
82
|
+
pluginName: "plugin-git-manager",
|
|
83
|
+
common: true
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "build-guide:process",
|
|
87
|
+
label: "Build Guide",
|
|
88
|
+
description: "Build user guide pages",
|
|
89
|
+
kind: "redis-list",
|
|
90
|
+
aliases: ["plugin-build-guide-block.build"],
|
|
91
|
+
redisKeySuffixes: [":plugin-build-guide-block:build:queue"],
|
|
92
|
+
pluginName: "plugin-build-guide-block",
|
|
93
|
+
common: true
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "build-visualization:process",
|
|
97
|
+
label: "Build Visualization",
|
|
98
|
+
description: "Build visualization blocks",
|
|
99
|
+
kind: "redis-list",
|
|
100
|
+
aliases: ["plugin-build-visualization-block.build"],
|
|
101
|
+
redisKeySuffixes: [":plugin-build-visualization-block:build:queue"],
|
|
102
|
+
pluginName: "plugin-build-visualization-block",
|
|
103
|
+
common: true
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "build-ui-template:process",
|
|
107
|
+
label: "Build UI Template",
|
|
108
|
+
description: "Build UI template pages",
|
|
109
|
+
kind: "event-queue",
|
|
110
|
+
aliases: ["plugin-build-ui-template.build"],
|
|
111
|
+
pluginName: "plugin-build-ui-template",
|
|
112
|
+
common: true
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "file-preview-auth:ocr",
|
|
116
|
+
label: "File Preview OCR",
|
|
117
|
+
description: "Run OCR extraction for authenticated file previews",
|
|
118
|
+
kind: "redis-list",
|
|
119
|
+
aliases: ["file-preview-auth.ocr.queue"],
|
|
120
|
+
redisKeySuffixes: ["file-preview-auth.ocr.queue"],
|
|
121
|
+
pluginName: "plugin-file-preview-auth",
|
|
122
|
+
common: true
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "skill-hub:sandbox",
|
|
126
|
+
label: "Skill Sandbox",
|
|
127
|
+
description: "Execute Skill Hub sandbox tasks",
|
|
128
|
+
kind: "pubsub",
|
|
129
|
+
aliases: ["skill-hub.task"],
|
|
130
|
+
pluginName: "plugin-agent-orchestrator",
|
|
131
|
+
sandbox: true
|
|
132
|
+
}
|
|
133
|
+
];
|
|
134
|
+
const PROCESS_BY_NAME = new Map(WORKER_PROCESS_DEFINITIONS.map((definition) => [definition.name, definition]));
|
|
135
|
+
const ALIAS_TO_PROCESS = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const definition of WORKER_PROCESS_DEFINITIONS) {
|
|
137
|
+
ALIAS_TO_PROCESS.set(definition.name, definition.name);
|
|
138
|
+
for (const alias of definition.aliases || []) {
|
|
139
|
+
ALIAS_TO_PROCESS.set(alias, definition.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function splitWorkerModeTokens(value) {
|
|
143
|
+
const rawTokens = Array.isArray(value) ? value : String(value || "").split(",");
|
|
144
|
+
return rawTokens.map((token) => String(token).trim()).filter(Boolean);
|
|
145
|
+
}
|
|
146
|
+
function stripKnownPrefixes(token) {
|
|
147
|
+
const candidates = [token];
|
|
148
|
+
const dotIndex = token.indexOf(".");
|
|
149
|
+
if (dotIndex > 0) {
|
|
150
|
+
candidates.push(token.slice(dotIndex + 1));
|
|
151
|
+
}
|
|
152
|
+
const colonIndex = token.indexOf(":");
|
|
153
|
+
if (colonIndex > 0) {
|
|
154
|
+
candidates.push(token.slice(colonIndex + 1));
|
|
155
|
+
}
|
|
156
|
+
return Array.from(new Set(candidates));
|
|
157
|
+
}
|
|
158
|
+
function resolveWorkerProcessName(token) {
|
|
159
|
+
const value = String(token || "").trim();
|
|
160
|
+
if (!value || value === "*" || value === "!" || value === "-") {
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
const directProcessName = ALIAS_TO_PROCESS.get(value);
|
|
164
|
+
if (directProcessName) {
|
|
165
|
+
return directProcessName;
|
|
166
|
+
}
|
|
167
|
+
for (const candidate of stripKnownPrefixes(value)) {
|
|
168
|
+
const processName = ALIAS_TO_PROCESS.get(candidate);
|
|
169
|
+
if (processName) {
|
|
170
|
+
return processName;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const definition of WORKER_PROCESS_DEFINITIONS) {
|
|
174
|
+
if ((definition.redisKeySuffixes || []).some((suffix) => value.endsWith(suffix))) {
|
|
175
|
+
return definition.name;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function normalizeWorkerMode(value) {
|
|
181
|
+
const resolved = splitWorkerModeTokens(value).map(resolveWorkerProcessName);
|
|
182
|
+
if (resolved.includes("*")) {
|
|
183
|
+
return "*";
|
|
184
|
+
}
|
|
185
|
+
const unique = Array.from(new Set(resolved.filter(Boolean)));
|
|
186
|
+
return unique.length ? unique.join(",") : void 0;
|
|
187
|
+
}
|
|
188
|
+
function workerModeTokens(value) {
|
|
189
|
+
const normalized = normalizeWorkerMode(value);
|
|
190
|
+
return normalized ? normalized.split(",") : [];
|
|
191
|
+
}
|
|
192
|
+
function workerModeServesProcess(workerMode, processNameOrAlias) {
|
|
193
|
+
const mode = normalizeWorkerMode(workerMode);
|
|
194
|
+
if (!mode) return true;
|
|
195
|
+
if (mode === "main" || mode === "app") return true;
|
|
196
|
+
if (mode === "-") return false;
|
|
197
|
+
if (mode === "*") return true;
|
|
198
|
+
const processName = resolveWorkerProcessName(processNameOrAlias);
|
|
199
|
+
return mode.split(",").includes(processName);
|
|
200
|
+
}
|
|
201
|
+
function isWorkerOnlyMode(workerMode) {
|
|
202
|
+
const mode = normalizeWorkerMode(workerMode);
|
|
203
|
+
if (!mode || mode === "main" || mode === "app" || mode === "-") return false;
|
|
204
|
+
return !mode.split(",").includes("!");
|
|
205
|
+
}
|
|
206
|
+
function isAppServingMode(workerMode) {
|
|
207
|
+
const mode = normalizeWorkerMode(workerMode);
|
|
208
|
+
if (!mode || mode === "main" || mode === "app") return true;
|
|
209
|
+
return mode.split(",").includes("!");
|
|
210
|
+
}
|
|
211
|
+
function getWorkerProcessDefinition(nameOrAlias) {
|
|
212
|
+
return PROCESS_BY_NAME.get(resolveWorkerProcessName(nameOrAlias));
|
|
213
|
+
}
|
|
214
|
+
function getCommonWorkerProcesses() {
|
|
215
|
+
return WORKER_PROCESS_DEFINITIONS.filter((definition) => definition.common && !definition.sandbox);
|
|
216
|
+
}
|
|
217
|
+
function getWorkerProcessByChannel(channel) {
|
|
218
|
+
const processName = resolveWorkerProcessName(channel);
|
|
219
|
+
return PROCESS_BY_NAME.get(processName);
|
|
220
|
+
}
|
|
221
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
222
|
+
0 && (module.exports = {
|
|
223
|
+
WORKER_PROCESS_DEFINITIONS,
|
|
224
|
+
getCommonWorkerProcesses,
|
|
225
|
+
getWorkerProcessByChannel,
|
|
226
|
+
getWorkerProcessDefinition,
|
|
227
|
+
isAppServingMode,
|
|
228
|
+
isWorkerOnlyMode,
|
|
229
|
+
normalizeWorkerMode,
|
|
230
|
+
resolveWorkerProcessName,
|
|
231
|
+
workerModeServesProcess,
|
|
232
|
+
workerModeTokens
|
|
233
|
+
});
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"displayName": "Cluster Manager",
|
|
4
4
|
"displayName.zh-CN": "Cluster Manager",
|
|
5
5
|
"description": "Cluster node tracking, task orchestration, worker management, and package distribution",
|
|
6
|
-
"version": "1.1.
|
|
6
|
+
"version": "1.1.17",
|
|
7
7
|
"license": "Apache-2.0",
|
|
8
8
|
"main": "dist/server/index.js",
|
|
9
9
|
"keywords": [
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '@ant-design/icons';
|
|
32
32
|
import { useApp } from '@nocobase/client-v2';
|
|
33
33
|
import { useT, formatBytes, formatUptime } from './utils';
|
|
34
|
+
import { isWorkerOnlyMode } from '../shared/worker-processes';
|
|
34
35
|
|
|
35
36
|
function LogViewerModal({ open, node, onClose }: { open: boolean; node: any; onClose: () => void }) {
|
|
36
37
|
const t = useT();
|
|
@@ -195,12 +196,46 @@ function countPackages(packages?: { apt?: string[]; npm?: string[]; python?: str
|
|
|
195
196
|
return (packages?.apt?.length || 0) + (packages?.npm?.length || 0) + (packages?.python?.length || 0);
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function getNodeRole(record: any): 'app' | 'worker' | 'sandbox' {
|
|
200
|
+
if (record?.appRole === 'app' || record?.appRole === 'worker' || record?.appRole === 'sandbox') {
|
|
201
|
+
return record.appRole;
|
|
202
|
+
}
|
|
203
|
+
if (record?.isSandbox) {
|
|
204
|
+
return 'sandbox';
|
|
205
|
+
}
|
|
206
|
+
return isWorkerOnlyMode(record?.workerMode) ? 'worker' : 'app';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function servesWorkerQueues(record: any) {
|
|
210
|
+
return record?.appRole === 'worker' || isWorkerOnlyMode(record?.workerMode);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readClusterListPayload(response: any) {
|
|
214
|
+
const body = response?.data;
|
|
215
|
+
const wrapped = body?.data;
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(wrapped)) {
|
|
218
|
+
return { rows: wrapped, meta: body?.meta || {} };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (Array.isArray(wrapped?.data)) {
|
|
222
|
+
return { rows: wrapped.data, meta: wrapped.meta || {} };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (Array.isArray(body?.rows)) {
|
|
226
|
+
return { rows: body.rows, meta: body?.meta || {} };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { rows: [], meta: {} };
|
|
230
|
+
}
|
|
231
|
+
|
|
198
232
|
export function ClusterNodes() {
|
|
199
233
|
const t = useT();
|
|
200
234
|
const api = useApp().apiClient;
|
|
201
235
|
const [loading, setLoading] = useState(false);
|
|
202
236
|
const [currentNode, setCurrentNode] = useState<any>(null);
|
|
203
237
|
const [environments, setEnvironments] = useState<any[]>([]);
|
|
238
|
+
const [clusterListMeta, setClusterListMeta] = useState<any>(null);
|
|
204
239
|
const [health, setHealth] = useState<any>(null);
|
|
205
240
|
const [drift, setDrift] = useState<any>(null);
|
|
206
241
|
const [legacyDiagnostics, setLegacyDiagnostics] = useState<any>(null);
|
|
@@ -221,8 +256,10 @@ export function ClusterNodes() {
|
|
|
221
256
|
api.request({ url: 'clusterManagerCluster:drift' }),
|
|
222
257
|
api.request({ url: 'clusterManagerCluster:legacyDiagnostics' }),
|
|
223
258
|
]);
|
|
259
|
+
const listPayload = readClusterListPayload(listRes);
|
|
224
260
|
setCurrentNode(currentRes?.data?.data);
|
|
225
|
-
setEnvironments(
|
|
261
|
+
setEnvironments(listPayload.rows);
|
|
262
|
+
setClusterListMeta(listPayload.meta);
|
|
226
263
|
setHealth(healthRes?.data?.data);
|
|
227
264
|
setDrift(driftRes?.data?.data);
|
|
228
265
|
setLegacyDiagnostics(legacyRes?.data?.data);
|
|
@@ -309,13 +346,21 @@ export function ClusterNodes() {
|
|
|
309
346
|
title: t('Type'),
|
|
310
347
|
dataIndex: 'workerMode',
|
|
311
348
|
key: 'workerMode',
|
|
312
|
-
width:
|
|
349
|
+
width: 150,
|
|
313
350
|
render: (mode: string, record: any) => {
|
|
314
|
-
|
|
351
|
+
const role = getNodeRole(record);
|
|
352
|
+
if (role === 'sandbox') {
|
|
315
353
|
return <Tag color="purple">SANDBOX</Tag>;
|
|
316
354
|
}
|
|
317
|
-
|
|
318
|
-
|
|
355
|
+
if (role === 'app' && isWorkerOnlyMode(mode)) {
|
|
356
|
+
return (
|
|
357
|
+
<Space size={4}>
|
|
358
|
+
<Tag color="green">APP</Tag>
|
|
359
|
+
<Tag color="blue">QUEUES</Tag>
|
|
360
|
+
</Space>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return <Tag color={role === 'worker' ? 'blue' : 'green'}>{role === 'worker' ? 'WORKER' : 'APP'}</Tag>;
|
|
319
364
|
},
|
|
320
365
|
},
|
|
321
366
|
{ title: t('PID'), dataIndex: 'pid', key: 'pid', width: 80 },
|
|
@@ -346,6 +391,10 @@ export function ClusterNodes() {
|
|
|
346
391
|
const versionDrifts = drift?.versionDrifts || [];
|
|
347
392
|
const runtimeDrifts = drift?.runtimeDrifts || [];
|
|
348
393
|
const legacyFindings = legacyDiagnostics?.findings || [];
|
|
394
|
+
const registryStatus = clusterListMeta?.registry || {};
|
|
395
|
+
const showRegistryNotice = Boolean(
|
|
396
|
+
clusterListMeta?.fallback || registryStatus.lastHeartbeatError || registryStatus.lastReadError,
|
|
397
|
+
);
|
|
349
398
|
const renderFindingMessage = (finding: any) => {
|
|
350
399
|
const template = finding.messageKey ? t(finding.messageKey) : finding.message;
|
|
351
400
|
if (!finding.messageArgs) {
|
|
@@ -416,11 +465,7 @@ export function ClusterNodes() {
|
|
|
416
465
|
<Card size="small">
|
|
417
466
|
<Statistic
|
|
418
467
|
title={t('Worker Nodes')}
|
|
419
|
-
value={
|
|
420
|
-
environments.filter(
|
|
421
|
-
(e) => e.workerMode === 'worker' || e.workerMode === 'task' || e.workerMode === '*',
|
|
422
|
-
).length
|
|
423
|
-
}
|
|
468
|
+
value={environments.filter((e) => servesWorkerQueues(e)).length}
|
|
424
469
|
prefix={<ClusterOutlined />}
|
|
425
470
|
/>
|
|
426
471
|
</Card>
|
|
@@ -643,6 +688,27 @@ export function ClusterNodes() {
|
|
|
643
688
|
|
|
644
689
|
{/* Cluster nodes table */}
|
|
645
690
|
<Card title={t('Cluster Nodes')} size="small">
|
|
691
|
+
{showRegistryNotice && (
|
|
692
|
+
<Alert
|
|
693
|
+
type={registryStatus.configured ? 'warning' : 'info'}
|
|
694
|
+
showIcon
|
|
695
|
+
style={{ marginBottom: 12 }}
|
|
696
|
+
message={
|
|
697
|
+
registryStatus.configured
|
|
698
|
+
? t('Cluster registry has no worker heartbeats')
|
|
699
|
+
: t('Cluster registry Redis is not configured')
|
|
700
|
+
}
|
|
701
|
+
description={
|
|
702
|
+
registryStatus.configured
|
|
703
|
+
? t(
|
|
704
|
+
'Cluster Nodes reads Redis heartbeats, not the container runtime. Check worker boot, plugin-cluster-manager, and shared Redis configuration.',
|
|
705
|
+
)
|
|
706
|
+
: t(
|
|
707
|
+
'Set REDIS_URL or CLUSTER_MANAGER_REDIS_URL on every app and worker to enable cluster node discovery.',
|
|
708
|
+
)
|
|
709
|
+
}
|
|
710
|
+
/>
|
|
711
|
+
)}
|
|
646
712
|
<Table
|
|
647
713
|
dataSource={environments}
|
|
648
714
|
columns={nodeColumns}
|