@kwirthmagnify/kwirth-plugin-status 0.1.0 → 0.2.0
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/back.js +62 -6
- package/front.js +363 -22
- package/package.json +1 -1
package/back.js
CHANGED
|
@@ -45,6 +45,18 @@ __export(index_exports, {
|
|
|
45
45
|
});
|
|
46
46
|
module.exports = __toCommonJS(index_exports);
|
|
47
47
|
var import_kwirth_common_back = __toESM(require_kwirth_common_back(), 1);
|
|
48
|
+
var statsOf = (p) => {
|
|
49
|
+
if (!p.getStats) return void 0;
|
|
50
|
+
try {
|
|
51
|
+
const s = p.getStats();
|
|
52
|
+
return {
|
|
53
|
+
subscribers: typeof s?.subscribers === "number" ? s.subscribers : void 0,
|
|
54
|
+
events: typeof s?.events === "number" ? s.events : void 0
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return void 0;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
48
60
|
var StatusChannel = class {
|
|
49
61
|
constructor(clusterInfo, backChannelObject) {
|
|
50
62
|
this.channelId = "status";
|
|
@@ -146,7 +158,15 @@ var StatusChannel = class {
|
|
|
146
158
|
cuántos suscriptores tiene, y ese contrato todavía no existe (llega en S2). Inventar el dato sería
|
|
147
159
|
peor que no darlo — un administrador que lea "ocioso" va a ir a desinstalar algo.
|
|
148
160
|
*/
|
|
149
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Cuántos consumidores tiene, o undefined si no lo dice.
|
|
163
|
+
*
|
|
164
|
+
* Se protege con try/catch porque esto es código de una extensión de terceros: un provider que
|
|
165
|
+
* reviente al preguntarle no puede llevarse por delante la pantalla entera. Si falla, no informa.
|
|
166
|
+
*/
|
|
167
|
+
this.subscribersOf = (p) => statsOf(p)?.subscribers;
|
|
168
|
+
this.eventsOf = (p) => statsOf(p)?.events;
|
|
169
|
+
this.healthOfProvider = (p, subscribers) => {
|
|
150
170
|
if (p.started !== true) {
|
|
151
171
|
return {
|
|
152
172
|
health: "not-instantiated" /* NOT_INSTANTIATED */,
|
|
@@ -160,20 +180,43 @@ var StatusChannel = class {
|
|
|
160
180
|
reason: "Its configuration endpoint is not mounted \u2014 the server has not been restarted since it was installed"
|
|
161
181
|
};
|
|
162
182
|
}
|
|
163
|
-
return { health: "instantiated" /* INSTANTIATED */ };
|
|
183
|
+
if (subscribers === void 0) return { health: "instantiated" /* INSTANTIATED */ };
|
|
184
|
+
if (subscribers > 0) return { health: "active" /* ACTIVE */ };
|
|
185
|
+
return {
|
|
186
|
+
health: "idle" /* IDLE */,
|
|
187
|
+
reason: "Running, but nothing is consuming it right now"
|
|
188
|
+
};
|
|
164
189
|
};
|
|
165
190
|
this.buildInventory = () => {
|
|
166
191
|
const components = [];
|
|
192
|
+
const edges = this.subscriptionsOf();
|
|
193
|
+
const conocidos = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const e of edges) conocidos.set(e.providerId, (conocidos.get(e.providerId) ?? 0) + 1);
|
|
167
195
|
for (const p of this.clusterInfo.providers ?? []) {
|
|
168
|
-
const
|
|
169
|
-
|
|
196
|
+
const subscribers = this.subscribersOf(p);
|
|
197
|
+
const eventos = this.eventsOf(p);
|
|
198
|
+
const { health, reason } = this.healthOfProvider(p, subscribers);
|
|
199
|
+
components.push({
|
|
200
|
+
kind: "provider" /* PROVIDER */,
|
|
201
|
+
id: p.id,
|
|
202
|
+
displayName: p.id,
|
|
203
|
+
health,
|
|
204
|
+
...reason ? { reason } : {},
|
|
205
|
+
...subscribers === void 0 ? {} : { subscribers },
|
|
206
|
+
...eventos === void 0 ? {} : { events: eventos },
|
|
207
|
+
knownConsumers: conocidos.get(p.id) ?? 0
|
|
208
|
+
});
|
|
170
209
|
}
|
|
171
210
|
for (const pluviderId of (this.clusterInfo.pluviders ?? /* @__PURE__ */ new Map()).keys()) {
|
|
211
|
+
const suyas = conocidos.get(pluviderId) ?? 0;
|
|
172
212
|
components.push({
|
|
173
213
|
kind: "pluvider" /* PLUVIDER */,
|
|
174
214
|
id: pluviderId,
|
|
175
215
|
displayName: pluviderId,
|
|
176
|
-
health: "
|
|
216
|
+
health: suyas > 0 ? "active" /* ACTIVE */ : "idle" /* IDLE */,
|
|
217
|
+
...suyas > 0 ? {} : { reason: "Running, but nothing is consuming it right now" },
|
|
218
|
+
subscribers: suyas,
|
|
219
|
+
knownConsumers: suyas
|
|
177
220
|
});
|
|
178
221
|
}
|
|
179
222
|
for (const s of this.clusterInfo.senders?.listSenders() ?? []) {
|
|
@@ -197,9 +240,22 @@ var StatusChannel = class {
|
|
|
197
240
|
return {
|
|
198
241
|
cluster: this.clusterInfo.name ?? "",
|
|
199
242
|
takenAt: Date.now(),
|
|
200
|
-
components
|
|
243
|
+
components,
|
|
244
|
+
edges
|
|
201
245
|
};
|
|
202
246
|
};
|
|
247
|
+
/**
|
|
248
|
+
* Las aristas que el core conoce. Protegido igual que getStats: si el core es anterior a esto o
|
|
249
|
+
* revienta, se devuelve vacío y la pantalla enseña el inventario sin grafo.
|
|
250
|
+
*/
|
|
251
|
+
this.subscriptionsOf = () => {
|
|
252
|
+
if (!this.clusterInfo.getSubscriptions) return [];
|
|
253
|
+
try {
|
|
254
|
+
return this.clusterInfo.getSubscriptions() ?? [];
|
|
255
|
+
} catch {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
};
|
|
203
259
|
this.sendInventory = (socket, instanceId) => {
|
|
204
260
|
const msg = {
|
|
205
261
|
msgtype: "statusmessageresponse",
|
package/front.js
CHANGED
|
@@ -76,8 +76,18 @@
|
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
+
// kwirth-globals:@xyflow/react
|
|
80
|
+
var require_react2 = __commonJS({
|
|
81
|
+
"kwirth-globals:@xyflow/react"(exports, module) {
|
|
82
|
+
var _m = window.__kwirth__.reactFlow;
|
|
83
|
+
var _d = _m != null && "default" in Object(_m) ? _m.default : _m;
|
|
84
|
+
if (typeof _d !== "function" && _d != null && typeof _d.default !== "undefined") _d = _d.default;
|
|
85
|
+
module.exports = Object.assign({}, typeof _m === "object" && _m !== null ? _m : {}, { default: _d, __esModule: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
79
89
|
// src/front/StatusChannel.ts
|
|
80
|
-
var
|
|
90
|
+
var import_react5 = __toESM(require_react(), 1);
|
|
81
91
|
var import_kwirth_common2 = __toESM(require_kwirth_common(), 1);
|
|
82
92
|
var import_kwirth_common_front = __toESM(require_kwirth_common_front(), 1);
|
|
83
93
|
|
|
@@ -89,6 +99,9 @@
|
|
|
89
99
|
// src/front/StatusData.ts
|
|
90
100
|
var StatusData = class {
|
|
91
101
|
constructor() {
|
|
102
|
+
this.view = "table";
|
|
103
|
+
this.filter = "";
|
|
104
|
+
this.autoRefresh = 0;
|
|
92
105
|
this.signals = [];
|
|
93
106
|
this.configAccepted = false;
|
|
94
107
|
this.started = false;
|
|
@@ -96,11 +109,231 @@
|
|
|
96
109
|
};
|
|
97
110
|
|
|
98
111
|
// src/front/StatusTabContent.tsx
|
|
99
|
-
var
|
|
100
|
-
var
|
|
112
|
+
var import_react4 = __toESM(require_react(), 1);
|
|
113
|
+
var import_material3 = __toESM(require_material(), 1);
|
|
101
114
|
var import_icons = __toESM(require_icons(), 1);
|
|
102
115
|
var import_kwirth_common = __toESM(require_kwirth_common(), 1);
|
|
116
|
+
|
|
117
|
+
// src/front/StatusDiagram.tsx
|
|
118
|
+
var import_react2 = __toESM(require_react(), 1);
|
|
119
|
+
var import_material2 = __toESM(require_material(), 1);
|
|
120
|
+
var import_react3 = __toESM(require_react2(), 1);
|
|
121
|
+
var COLOR = {
|
|
122
|
+
["active" /* ACTIVE */]: "#2e7d32",
|
|
123
|
+
["idle" /* IDLE */]: "#616161",
|
|
124
|
+
["instantiated" /* INSTANTIATED */]: "#2e7d32",
|
|
125
|
+
["not-instantiated" /* NOT_INSTANTIATED */]: "#ed6c02",
|
|
126
|
+
["pending-restart" /* PENDING_RESTART */]: "#ed6c02",
|
|
127
|
+
["failed" /* FAILED */]: "#d32f2f",
|
|
128
|
+
["unknown" /* UNKNOWN */]: "#616161"
|
|
129
|
+
};
|
|
130
|
+
var colocar = async (nodos, aristas) => {
|
|
131
|
+
const filas = () => {
|
|
132
|
+
const pos = {};
|
|
133
|
+
const productores = nodos.filter((n) => n.data.esProductor);
|
|
134
|
+
const consumidores = nodos.filter((n) => !n.data.esProductor);
|
|
135
|
+
productores.forEach((n, i) => {
|
|
136
|
+
pos[n.id] = { x: i * 260, y: 0 };
|
|
137
|
+
});
|
|
138
|
+
consumidores.forEach((n, i) => {
|
|
139
|
+
pos[n.id] = { x: i * 260, y: 220 };
|
|
140
|
+
});
|
|
141
|
+
return pos;
|
|
142
|
+
};
|
|
143
|
+
const loadElk = window.__kwirth__?.loadElk;
|
|
144
|
+
if (!loadElk) return filas();
|
|
145
|
+
try {
|
|
146
|
+
const ELK = await loadElk();
|
|
147
|
+
const elk = new ELK();
|
|
148
|
+
const g = await elk.layout({
|
|
149
|
+
id: "root",
|
|
150
|
+
layoutOptions: {
|
|
151
|
+
"elk.algorithm": "layered",
|
|
152
|
+
/*
|
|
153
|
+
De arriba abajo: los productores en la capa de arriba y los consumidores debajo.
|
|
154
|
+
Se lee como un diagrama de flujo —el dato cae— y aprovecha el ancho de la pantalla,
|
|
155
|
+
que es donde sobra sitio cuando hay muchos nodos.
|
|
156
|
+
*/
|
|
157
|
+
"elk.direction": "DOWN",
|
|
158
|
+
"elk.spacing.nodeNode": "40",
|
|
159
|
+
"elk.layered.spacing.nodeNodeBetweenLayers": "110"
|
|
160
|
+
},
|
|
161
|
+
children: nodos.map((n) => ({ id: n.id, width: 230, height: 56 })),
|
|
162
|
+
edges: aristas.map((e) => ({ id: e.id, sources: [e.source], targets: [e.target] }))
|
|
163
|
+
});
|
|
164
|
+
const pos = {};
|
|
165
|
+
for (const c of g.children ?? []) pos[c.id] = { x: c.x, y: c.y };
|
|
166
|
+
return Object.keys(pos).length === nodos.length ? pos : filas();
|
|
167
|
+
} catch {
|
|
168
|
+
return filas();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var StatusDiagram = ({ inventory, active }) => {
|
|
172
|
+
const theme = (0, import_material2.useTheme)();
|
|
173
|
+
const [posiciones, setPosiciones] = import_react2.default.useState(void 0);
|
|
174
|
+
const [seleccionado, setSeleccionado] = import_react2.default.useState(void 0);
|
|
175
|
+
const colores = {
|
|
176
|
+
fondoNodo: theme.palette.background.paper,
|
|
177
|
+
fondoCanal: theme.palette.mode === "dark" ? "#12233a" : "#e8f0fb",
|
|
178
|
+
texto: theme.palette.text.primary,
|
|
179
|
+
bordeCanal: theme.palette.primary.main
|
|
180
|
+
};
|
|
181
|
+
const { nodos, aristas, canalesSueltos } = import_react2.default.useMemo(() => {
|
|
182
|
+
const productores = inventory.components.filter((c) => c.kind === "provider" /* PROVIDER */ || c.kind === "pluvider" /* PLUVIDER */);
|
|
183
|
+
const idsProductores = new Set(productores.map((p) => p.id));
|
|
184
|
+
const canales = [...new Set(inventory.edges.map((e) => e.channelId))];
|
|
185
|
+
const vecinos = /* @__PURE__ */ new Set();
|
|
186
|
+
if (seleccionado) {
|
|
187
|
+
vecinos.add(seleccionado);
|
|
188
|
+
for (const e of inventory.edges) {
|
|
189
|
+
const origen = e.providerId;
|
|
190
|
+
const destino = `channel:${e.channelId}`;
|
|
191
|
+
if (origen === seleccionado) vecinos.add(destino);
|
|
192
|
+
if (destino === seleccionado) vecinos.add(origen);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const apagado = (id) => !seleccionado || vecinos.has(id) ? 1 : 0.25;
|
|
196
|
+
const nodos2 = [
|
|
197
|
+
...productores.map((p) => ({
|
|
198
|
+
id: p.id,
|
|
199
|
+
position: { x: 0, y: 0 },
|
|
200
|
+
data: { label: p.displayName, esProductor: true, health: p.health, subscribers: p.subscribers, known: p.knownConsumers },
|
|
201
|
+
// Con el grafo en vertical, la arista tiene que salir por ABAJO y entrar por ARRIBA; si
|
|
202
|
+
// no, React Flow las saca por los lados y los cables dan un rodeo absurdo.
|
|
203
|
+
sourcePosition: import_react3.Position.Bottom,
|
|
204
|
+
targetPosition: import_react3.Position.Top,
|
|
205
|
+
style: {
|
|
206
|
+
background: colores.fondoNodo,
|
|
207
|
+
color: colores.texto,
|
|
208
|
+
border: `${seleccionado === p.id ? 3 : 2}px solid ${COLOR[p.health]}`,
|
|
209
|
+
borderRadius: 8,
|
|
210
|
+
width: 230,
|
|
211
|
+
fontSize: 12,
|
|
212
|
+
padding: 8,
|
|
213
|
+
opacity: apagado(p.id),
|
|
214
|
+
boxShadow: seleccionado === p.id ? `0 0 10px ${COLOR[p.health]}` : void 0
|
|
215
|
+
}
|
|
216
|
+
})),
|
|
217
|
+
...canales.map((id) => ({
|
|
218
|
+
id: `channel:${id}`,
|
|
219
|
+
position: { x: 0, y: 0 },
|
|
220
|
+
data: { label: id, esProductor: false },
|
|
221
|
+
sourcePosition: import_react3.Position.Bottom,
|
|
222
|
+
targetPosition: import_react3.Position.Top,
|
|
223
|
+
style: {
|
|
224
|
+
background: colores.fondoCanal,
|
|
225
|
+
color: colores.texto,
|
|
226
|
+
border: `${seleccionado === `channel:${id}` ? 3 : 2}px solid ${colores.bordeCanal}`,
|
|
227
|
+
borderRadius: 8,
|
|
228
|
+
width: 230,
|
|
229
|
+
fontSize: 12,
|
|
230
|
+
padding: 8,
|
|
231
|
+
opacity: apagado(`channel:${id}`),
|
|
232
|
+
boxShadow: seleccionado === `channel:${id}` ? `0 0 10px ${colores.bordeCanal}` : void 0
|
|
233
|
+
}
|
|
234
|
+
}))
|
|
235
|
+
];
|
|
236
|
+
const aristas2 = inventory.edges.map((e) => ({
|
|
237
|
+
id: `${e.providerId}->${e.channelId}`,
|
|
238
|
+
source: e.providerId,
|
|
239
|
+
target: `channel:${e.channelId}`,
|
|
240
|
+
/*
|
|
241
|
+
QUIETA a propósito, aunque React Flow sepa animarlas.
|
|
242
|
+
|
|
243
|
+
Una línea en movimiento se lee como "por aquí está pasando algo ahora mismo", y eso no
|
|
244
|
+
se sabe: lo único que dice esta arista es que la suscripción existe. Animarla sería el
|
|
245
|
+
mismo error que poner un 0 donde no hay dato — parecería información y sería decoración.
|
|
246
|
+
|
|
247
|
+
Cuando los contadores de S4 midan caudal de verdad, el movimiento (o el grosor) podrá
|
|
248
|
+
significar algo, y entonces se pone.
|
|
249
|
+
*/
|
|
250
|
+
...(() => {
|
|
251
|
+
const tocaAlSeleccionado = Boolean(seleccionado) && (e.providerId === seleccionado || `channel:${e.channelId}` === seleccionado);
|
|
252
|
+
const viva = active.has(e.providerId);
|
|
253
|
+
const color = tocaAlSeleccionado ? "#7fd8b0" : viva ? "#5fc79a" : "#4a8";
|
|
254
|
+
const ancho = tocaAlSeleccionado ? 3 : viva ? 2 : 1;
|
|
255
|
+
const punta = 16 / ancho;
|
|
256
|
+
return {
|
|
257
|
+
// Mismo realce que el mapa de Iter: mas grosor, sombra y por delante de las demás.
|
|
258
|
+
style: tocaAlSeleccionado ? { stroke: color, strokeWidth: ancho, filter: `drop-shadow(0 0 3px ${color})`, opacity: 1 } : { stroke: color, strokeWidth: ancho, opacity: seleccionado ? 0.2 : 1 },
|
|
259
|
+
animated: viva,
|
|
260
|
+
zIndex: tocaAlSeleccionado ? 1e3 : viva ? 500 : 0,
|
|
261
|
+
markerEnd: { type: import_react3.MarkerType.ArrowClosed, color, width: punta, height: punta }
|
|
262
|
+
};
|
|
263
|
+
})()
|
|
264
|
+
})).filter((e) => idsProductores.has(e.source));
|
|
265
|
+
const canalesSueltos2 = inventory.edges.length - aristas2.length;
|
|
266
|
+
return { nodos: nodos2, aristas: aristas2, canalesSueltos: canalesSueltos2 };
|
|
267
|
+
}, [inventory, active, seleccionado, colores.fondoNodo, colores.fondoCanal, colores.texto, colores.bordeCanal]);
|
|
268
|
+
const firmaGrafo = nodos.map((n) => n.id).join("|") + "#" + aristas.map((a) => a.id).join("|");
|
|
269
|
+
import_react2.default.useEffect(() => {
|
|
270
|
+
let vigente = true;
|
|
271
|
+
colocar(nodos, aristas).then((pos) => {
|
|
272
|
+
if (vigente) setPosiciones(pos);
|
|
273
|
+
});
|
|
274
|
+
return () => {
|
|
275
|
+
vigente = false;
|
|
276
|
+
};
|
|
277
|
+
}, [firmaGrafo]);
|
|
278
|
+
if (inventory.edges.length === 0) {
|
|
279
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { sx: { p: 3 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "Nothing is subscribed to anything right now, so there is no graph to draw."));
|
|
280
|
+
}
|
|
281
|
+
if (!posiciones) {
|
|
282
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { sx: { p: 3 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "body2", color: "text.secondary" }, "Laying out the graph\u2026"));
|
|
283
|
+
}
|
|
284
|
+
const colocados = nodos.map((n) => ({ ...n, position: posiciones[n.id] ?? { x: 0, y: 0 } }));
|
|
285
|
+
const anonimos = inventory.components.reduce((n, c) => {
|
|
286
|
+
if (c.subscribers === void 0 || c.knownConsumers === void 0) return n;
|
|
287
|
+
return n + Math.max(0, c.subscribers - c.knownConsumers);
|
|
288
|
+
}, 0);
|
|
289
|
+
return /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { sx: { height: "100%", display: "flex", flexDirection: "column", minHeight: 0 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Stack, { direction: "row", spacing: 1, alignItems: "center", sx: { mb: 1 } }, /* @__PURE__ */ import_react2.default.createElement(import_material2.Typography, { variant: "caption", color: "text.secondary" }, "A line means ", /* @__PURE__ */ import_react2.default.createElement("b", null, "an active subscription"), ". A ", /* @__PURE__ */ import_react2.default.createElement("b", null, "moving line"), " means its producer is delivering right now \u2014 but not how much goes to each consumer: that is measured per component, not per line.", " ", "Click a node to highlight what it is connected to; click the background to clear.")), (anonimos > 0 || canalesSueltos > 0) && /* @__PURE__ */ import_react2.default.createElement(import_material2.Stack, { direction: "row", spacing: 1, sx: { mb: 1 } }, anonimos > 0 && /* @__PURE__ */ import_react2.default.createElement(
|
|
290
|
+
import_material2.Chip,
|
|
291
|
+
{
|
|
292
|
+
size: "small",
|
|
293
|
+
variant: "outlined",
|
|
294
|
+
color: "default",
|
|
295
|
+
label: `${anonimos} consumer${anonimos > 1 ? "s" : ""} not shown \u2014 they subscribed without going through the core`
|
|
296
|
+
}
|
|
297
|
+
), canalesSueltos > 0 && /* @__PURE__ */ import_react2.default.createElement(import_material2.Chip, { size: "small", variant: "outlined", label: `${canalesSueltos} subscription${canalesSueltos > 1 ? "s" : ""} to something no longer installed` })), /* @__PURE__ */ import_react2.default.createElement(import_material2.Box, { sx: {
|
|
298
|
+
flex: 1,
|
|
299
|
+
minHeight: 0,
|
|
300
|
+
border: 1,
|
|
301
|
+
borderColor: "divider",
|
|
302
|
+
borderRadius: 1,
|
|
303
|
+
"& .react-flow__controls": { boxShadow: "none" },
|
|
304
|
+
"& .react-flow__controls-button": {
|
|
305
|
+
background: theme.palette.background.paper,
|
|
306
|
+
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
307
|
+
fill: theme.palette.text.primary
|
|
308
|
+
},
|
|
309
|
+
"& .react-flow__controls-button:hover": { background: theme.palette.action.hover },
|
|
310
|
+
"& .react-flow__controls-button svg": { fill: theme.palette.text.primary },
|
|
311
|
+
"& .react-flow__attribution": { display: "none" }
|
|
312
|
+
} }, /* @__PURE__ */ import_react2.default.createElement(
|
|
313
|
+
import_react3.ReactFlow,
|
|
314
|
+
{
|
|
315
|
+
nodes: colocados,
|
|
316
|
+
edges: aristas,
|
|
317
|
+
fitView: true,
|
|
318
|
+
proOptions: { hideAttribution: true },
|
|
319
|
+
nodesConnectable: false,
|
|
320
|
+
edgesReconnectable: false,
|
|
321
|
+
connectOnClick: false,
|
|
322
|
+
deleteKeyCode: null,
|
|
323
|
+
onNodeClick: (_e, nodo) => setSeleccionado(nodo.id),
|
|
324
|
+
onPaneClick: () => setSeleccionado(void 0)
|
|
325
|
+
},
|
|
326
|
+
/* @__PURE__ */ import_react2.default.createElement(import_react3.Background, null),
|
|
327
|
+
/* @__PURE__ */ import_react2.default.createElement(import_react3.Controls, { showInteractive: false })
|
|
328
|
+
)));
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// src/front/StatusTabContent.tsx
|
|
103
332
|
var HEALTH_LABEL = {
|
|
333
|
+
["active" /* ACTIVE */]: { label: "Active", color: "success" },
|
|
334
|
+
// Ocioso NO es un error, es información: funciona, pero no le sirve a nadie. De ahí 'default' y no
|
|
335
|
+
// 'warning' — quien mire tiene que poder distinguir "hay que arreglar esto" de "esto sobra".
|
|
336
|
+
["idle" /* IDLE */]: { label: "Idle", color: "default" },
|
|
104
337
|
["instantiated" /* INSTANTIATED */]: { label: "Running", color: "success" },
|
|
105
338
|
["not-instantiated" /* NOT_INSTANTIATED */]: { label: "Not started", color: "warning" },
|
|
106
339
|
["pending-restart" /* PENDING_RESTART */]: { label: "Needs restart", color: "warning" },
|
|
@@ -114,12 +347,42 @@
|
|
|
114
347
|
["webhook" /* WEBHOOK */]: "Webhook",
|
|
115
348
|
["channel" /* CHANNEL */]: "Channel"
|
|
116
349
|
};
|
|
350
|
+
var EmptyState = ({ title, detail }) => {
|
|
351
|
+
const ref = import_react4.default.useRef(null);
|
|
352
|
+
const [top, setTop] = import_react4.default.useState(0);
|
|
353
|
+
import_react4.default.useEffect(() => {
|
|
354
|
+
if (ref.current) setTop(ref.current.getBoundingClientRect().top);
|
|
355
|
+
});
|
|
356
|
+
return /* @__PURE__ */ import_react4.default.createElement(
|
|
357
|
+
import_material3.Stack,
|
|
358
|
+
{
|
|
359
|
+
ref,
|
|
360
|
+
alignItems: "center",
|
|
361
|
+
justifyContent: "center",
|
|
362
|
+
spacing: 1,
|
|
363
|
+
sx: { flex: 1, width: "100%", minHeight: `calc(100vh - ${top}px - 8px)`, px: 4, textAlign: "center" }
|
|
364
|
+
},
|
|
365
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "h6", color: "text.secondary" }, title),
|
|
366
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2", color: "text.secondary" }, detail)
|
|
367
|
+
);
|
|
368
|
+
};
|
|
117
369
|
var StatusTabContent = (props) => {
|
|
118
370
|
const data = props.channelObject.data;
|
|
119
|
-
const [
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
371
|
+
const [, forzarRender] = import_react4.default.useState(0);
|
|
372
|
+
const repintar = () => forzarRender((n) => n + 1);
|
|
373
|
+
const filter = data.filter;
|
|
374
|
+
const setFilter = (v) => {
|
|
375
|
+
data.filter = v;
|
|
376
|
+
repintar();
|
|
377
|
+
};
|
|
378
|
+
const vista = data.view;
|
|
379
|
+
const setVista = (v) => {
|
|
380
|
+
data.view = v;
|
|
381
|
+
repintar();
|
|
382
|
+
};
|
|
383
|
+
const boxRef = import_react4.default.useRef(null);
|
|
384
|
+
const [boxTop, setBoxTop] = import_react4.default.useState(0);
|
|
385
|
+
import_react4.default.useEffect(() => {
|
|
123
386
|
if (boxRef.current) setBoxTop(boxRef.current.getBoundingClientRect().top);
|
|
124
387
|
});
|
|
125
388
|
const refresh = () => {
|
|
@@ -134,36 +397,93 @@
|
|
|
134
397
|
command: "refresh" /* REFRESH */
|
|
135
398
|
}));
|
|
136
399
|
};
|
|
400
|
+
import_react4.default.useEffect(() => {
|
|
401
|
+
if (!data.autoRefresh) return;
|
|
402
|
+
const id = setInterval(() => refresh(), data.autoRefresh * 1e3);
|
|
403
|
+
return () => clearInterval(id);
|
|
404
|
+
}, [data.autoRefresh, props.channelObject.instanceId]);
|
|
137
405
|
const inventory = data.inventory;
|
|
406
|
+
const tasaDe = (id, ahora) => {
|
|
407
|
+
if (ahora === void 0 || !data.previous || !inventory) return void 0;
|
|
408
|
+
const antes = data.previous.components.find((c) => c.id === id)?.events;
|
|
409
|
+
if (antes === void 0 || ahora < antes) return void 0;
|
|
410
|
+
const segundos = (inventory.takenAt - data.previous.takenAt) / 1e3;
|
|
411
|
+
if (segundos <= 0) return void 0;
|
|
412
|
+
return (ahora - antes) / segundos;
|
|
413
|
+
};
|
|
138
414
|
const componentes = (inventory?.components ?? []).filter((c) => {
|
|
139
415
|
if (!filter) return true;
|
|
140
416
|
const f = filter.toLowerCase();
|
|
141
417
|
return c.id.toLowerCase().includes(f) || KIND_LABEL[c.kind].toLowerCase().includes(f);
|
|
142
418
|
});
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
419
|
+
const activos = /* @__PURE__ */ new Set();
|
|
420
|
+
for (const c of inventory?.components ?? []) {
|
|
421
|
+
if (c.events === void 0 || !data.previous) continue;
|
|
422
|
+
const antes = data.previous.components.find((p) => p.id === c.id)?.events;
|
|
423
|
+
if (antes !== void 0 && c.events !== antes) activos.add(c.id);
|
|
424
|
+
}
|
|
425
|
+
const ORDEN = {
|
|
426
|
+
["failed" /* FAILED */]: 0,
|
|
427
|
+
["pending-restart" /* PENDING_RESTART */]: 1,
|
|
428
|
+
["not-instantiated" /* NOT_INSTANTIATED */]: 2,
|
|
429
|
+
["idle" /* IDLE */]: 3,
|
|
430
|
+
["unknown" /* UNKNOWN */]: 4,
|
|
431
|
+
["instantiated" /* INSTANTIATED */]: 5,
|
|
432
|
+
["active" /* ACTIVE */]: 6
|
|
433
|
+
};
|
|
150
434
|
componentes.sort((a, b) => {
|
|
151
|
-
const d = ORDEN
|
|
435
|
+
const d = ORDEN[a.health] - ORDEN[b.health];
|
|
152
436
|
if (d !== 0) return d;
|
|
153
437
|
return a.kind === b.kind ? a.id.localeCompare(b.id) : a.kind.localeCompare(b.kind);
|
|
154
438
|
});
|
|
155
439
|
const fila = (c) => {
|
|
156
440
|
const estado = HEALTH_LABEL[c.health];
|
|
157
|
-
return /* @__PURE__ */
|
|
441
|
+
return /* @__PURE__ */ import_react4.default.createElement(import_material3.TableRow, { key: `${c.kind}-${c.id}` }, /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, { sx: { whiteSpace: "nowrap" } }, KIND_LABEL[c.kind]), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2", sx: { fontWeight: 500 } }, c.displayName)), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(import_material3.Chip, { size: "small", label: estado.label, color: estado.color, variant: c.health === "active" /* ACTIVE */ ? "outlined" : "filled" })), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, { align: "right" }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" }, color: c.subscribers === void 0 ? "text.disabled" : "text.primary" }, c.subscribers === void 0 ? "\u2014" : c.subscribers)), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, { align: "right" }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2", sx: { fontVariantNumeric: "tabular-nums" }, color: c.events === void 0 ? "text.disabled" : "text.primary" }, c.events === void 0 ? "\u2014" : c.events.toLocaleString()), (() => {
|
|
442
|
+
const t = tasaDe(c.id, c.events);
|
|
443
|
+
if (t === void 0) return null;
|
|
444
|
+
return /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", display: "block" }, t < 1 && t > 0 ? t.toFixed(2) : Math.round(t), "/s");
|
|
445
|
+
})()), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "body2", color: "text.secondary" }, c.reason ?? "")));
|
|
158
446
|
};
|
|
447
|
+
if (!data.started) {
|
|
448
|
+
return /* @__PURE__ */ import_react4.default.createElement(
|
|
449
|
+
EmptyState,
|
|
450
|
+
{
|
|
451
|
+
title: "Kwirth Status not started",
|
|
452
|
+
detail: "Start the channel (tab settings \u2699 \u2192 Start) to see what this Kwirth has inside."
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
}
|
|
159
456
|
if (!inventory) {
|
|
160
|
-
return /* @__PURE__ */
|
|
457
|
+
return /* @__PURE__ */ import_react4.default.createElement(
|
|
458
|
+
EmptyState,
|
|
459
|
+
{
|
|
460
|
+
title: "Waiting for the first snapshot",
|
|
461
|
+
detail: "The channel is running; the inventory should appear in a moment."
|
|
462
|
+
}
|
|
463
|
+
);
|
|
161
464
|
}
|
|
162
|
-
return /* @__PURE__ */
|
|
465
|
+
return /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, { sx: { p: 2, display: "flex", flexDirection: "column", minHeight: 0 } }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { mb: 1 } }, /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "subtitle2" }, "What this Kwirth has inside"), /* @__PURE__ */ import_react4.default.createElement(import_material3.Chip, { size: "small", variant: "outlined", label: `${inventory.components.length} components` }), /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, { sx: { flexGrow: 1 } }), /* @__PURE__ */ import_react4.default.createElement(import_material3.TextField, { size: "small", placeholder: "Filter\u2026", value: filter, onChange: (e) => setFilter(e.target.value), sx: { width: 220 } }), /* @__PURE__ */ import_react4.default.createElement(import_material3.Tooltip, { title: "Table view" }, /* @__PURE__ */ import_react4.default.createElement(import_material3.IconButton, { size: "small", color: vista === "table" ? "primary" : "default", "aria-label": "Table view", onClick: () => setVista("table") }, /* @__PURE__ */ import_react4.default.createElement(import_icons.ViewList, { fontSize: "small" }))), /* @__PURE__ */ import_react4.default.createElement(import_material3.Tooltip, { title: "Graph view" }, /* @__PURE__ */ import_react4.default.createElement(import_material3.IconButton, { size: "small", color: vista === "graph" ? "primary" : "default", "aria-label": "Graph view", onClick: () => setVista("graph") }, /* @__PURE__ */ import_react4.default.createElement(import_icons.Hub, { fontSize: "small" }))), /* @__PURE__ */ import_react4.default.createElement(
|
|
466
|
+
import_material3.Select,
|
|
467
|
+
{
|
|
468
|
+
size: "small",
|
|
469
|
+
value: data.autoRefresh,
|
|
470
|
+
"aria-label": "Auto refresh",
|
|
471
|
+
onChange: (e) => {
|
|
472
|
+
data.autoRefresh = Number(e.target.value);
|
|
473
|
+
repintar();
|
|
474
|
+
},
|
|
475
|
+
sx: { minWidth: 104, "& .MuiSelect-select": { py: 0.5, fontSize: "0.8rem" } }
|
|
476
|
+
},
|
|
477
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { value: 0 }, "Manual"),
|
|
478
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { value: 5 }, "Every 5s"),
|
|
479
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { value: 15 }, "Every 15s"),
|
|
480
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { value: 30 }, "Every 30s"),
|
|
481
|
+
/* @__PURE__ */ import_react4.default.createElement(import_material3.MenuItem, { value: 60 }, "Every minute")
|
|
482
|
+
), /* @__PURE__ */ import_react4.default.createElement(import_material3.Tooltip, { title: "Take a new snapshot" }, /* @__PURE__ */ import_react4.default.createElement(import_material3.IconButton, { size: "small", onClick: refresh }, /* @__PURE__ */ import_react4.default.createElement(import_icons.Refresh, { fontSize: "small" })))), /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { variant: "caption", color: "text.secondary", sx: { mb: 1 } }, "Snapshot taken at ", new Date(inventory.takenAt).toLocaleTimeString(), data.autoRefresh ? ` \u2014 refreshing every ${data.autoRefresh}s while this tab is open` : " \u2014 it does not refresh on its own", ".", " ", "Delivered counts since each component started; the rate is measured against your previous snapshot."), /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, { ref: boxRef, sx: { display: "flex", flexDirection: "column", overflowY: vista === "table" ? "auto" : "hidden", overflowX: "hidden", width: "100%", flexGrow: 1, height: `calc(100vh - ${boxTop}px - 35px)` } }, vista === "graph" && /* @__PURE__ */ import_react4.default.createElement(StatusDiagram, { inventory, active: activos }), vista === "table" && /* @__PURE__ */ import_react4.default.createElement(import_material3.Table, { size: "small", stickyHeader: true }, /* @__PURE__ */ import_react4.default.createElement(import_material3.TableHead, null, /* @__PURE__ */ import_react4.default.createElement(import_material3.TableRow, null, /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, "Kind"), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, "Name"), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, "State"), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, { align: "right" }, "Consumers"), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, { align: "right" }, "Delivered"), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableCell, null, "Why"))), /* @__PURE__ */ import_react4.default.createElement(import_material3.TableBody, null, componentes.map(fila)))), data.signals.length > 0 && /* @__PURE__ */ import_react4.default.createElement(import_material3.Box, { sx: { mt: 1 } }, data.signals.map((s, i) => /* @__PURE__ */ import_react4.default.createElement(import_material3.Typography, { key: i, variant: "caption", color: "error", display: "block" }, s))));
|
|
163
483
|
};
|
|
164
484
|
|
|
165
485
|
// src/front/StatusChannel.ts
|
|
166
|
-
var StatusSetup = () =>
|
|
486
|
+
var StatusSetup = () => import_react5.default.createElement("div", null, "Kwirth Status has nothing to configure: open it and it shows what this Kwirth has inside.");
|
|
167
487
|
var StatusChannel = class {
|
|
168
488
|
constructor() {
|
|
169
489
|
this.setupVisible = false;
|
|
@@ -194,7 +514,7 @@
|
|
|
194
514
|
return import_kwirth_common2.EInstanceConfigScope.NONE;
|
|
195
515
|
}
|
|
196
516
|
getChannelIcon() {
|
|
197
|
-
return
|
|
517
|
+
return import_react5.default.createElement(StatusIcon);
|
|
198
518
|
}
|
|
199
519
|
getSetupVisibility() {
|
|
200
520
|
return this.setupVisible;
|
|
@@ -208,6 +528,7 @@
|
|
|
208
528
|
switch (msg.type) {
|
|
209
529
|
case import_kwirth_common2.EInstanceMessageType.DATA:
|
|
210
530
|
if (msg.payloadType === "inventory" /* INVENTORY */ && msg.inventory) {
|
|
531
|
+
data.previous = data.inventory;
|
|
211
532
|
data.inventory = msg.inventory;
|
|
212
533
|
}
|
|
213
534
|
return { action: import_kwirth_common_front.EChannelRefreshAction.REFRESH };
|
|
@@ -232,7 +553,21 @@
|
|
|
232
553
|
startChannel(_channelObject) {
|
|
233
554
|
return true;
|
|
234
555
|
}
|
|
235
|
-
|
|
556
|
+
/*
|
|
557
|
+
Al parar hay que DECIRLO, y ademas tirar la foto.
|
|
558
|
+
|
|
559
|
+
'started' lo ponia a true la respuesta del arranque y no lo bajaba nadie, asi que al parar el
|
|
560
|
+
canal la pestaña se quedaba enseñando el inventario como si nada. Y esa foto ya no vale: es de
|
|
561
|
+
un momento anterior y nada la va a refrescar mientras el canal este parado — dejarla puesta es
|
|
562
|
+
justo el tipo de dato viejo con pinta de actual que este plugin existe para evitar.
|
|
563
|
+
*/
|
|
564
|
+
stopChannel(channelObject) {
|
|
565
|
+
const data = channelObject.data;
|
|
566
|
+
if (data) {
|
|
567
|
+
data.started = false;
|
|
568
|
+
data.inventory = void 0;
|
|
569
|
+
data.previous = void 0;
|
|
570
|
+
}
|
|
236
571
|
return true;
|
|
237
572
|
}
|
|
238
573
|
pauseChannel(_channelObject) {
|
|
@@ -241,7 +576,13 @@
|
|
|
241
576
|
continueChannel(_channelObject) {
|
|
242
577
|
return true;
|
|
243
578
|
}
|
|
244
|
-
|
|
579
|
+
/*
|
|
580
|
+
Si se cae el socket, el canal deja de recibir y la foto se queda congelada sin avisar. Se trata
|
|
581
|
+
igual que una parada: mejor decir que hay que arrancar que enseñar algo que ya no se actualiza.
|
|
582
|
+
*/
|
|
583
|
+
socketDisconnected(channelObject) {
|
|
584
|
+
const data = channelObject.data;
|
|
585
|
+
if (data) data.started = false;
|
|
245
586
|
return true;
|
|
246
587
|
}
|
|
247
588
|
/*
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"id": "status",
|
|
5
5
|
"name": "@kwirthmagnify/kwirth-plugin-status",
|
|
6
6
|
"displayName": "Kwirth Status",
|
|
7
|
-
"version": "0.
|
|
7
|
+
"version": "0.2.0",
|
|
8
8
|
"description": "A look inside Kwirth: what is installed, how it is doing and who consumes what",
|
|
9
9
|
"icon": "<svg viewBox='0 0 24 24'><g fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><rect x='2.6' y='4.6' width='18.8' height='14.8' rx='2.2'/><path d='M6 12h2.6l1.5-3.4 2.6 6.8 1.5-3.4H18'/></g></svg>",
|
|
10
10
|
"requiresRestart": false,
|