shokupan 0.7.0 → 0.9.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/README.md +53 -0
- package/dist/context.d.ts +50 -15
- package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
- package/dist/http-server-BEMPIs33.cjs.map +1 -0
- package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
- package/dist/http-server-CCeagTyU.js.map +1 -0
- package/dist/index.cjs +998 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +996 -135
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
- package/dist/plugins/application/dashboard/static/charts.js +328 -0
- package/dist/plugins/application/dashboard/static/failures.js +85 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
- package/dist/plugins/application/dashboard/static/poll.js +146 -0
- package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
- package/dist/plugins/application/dashboard/static/registry.css +131 -0
- package/dist/plugins/application/dashboard/static/registry.js +269 -0
- package/dist/plugins/application/dashboard/static/requests.js +118 -0
- package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
- package/dist/plugins/application/dashboard/static/styles.css +175 -0
- package/dist/plugins/application/dashboard/static/tables.js +92 -0
- package/dist/plugins/application/dashboard/static/tabs.js +113 -0
- package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
- package/dist/plugins/application/dashboard/template.eta +246 -0
- package/dist/plugins/application/socket-io.d.ts +14 -0
- package/dist/router.d.ts +12 -0
- package/dist/shokupan.d.ts +21 -1
- package/dist/util/datastore.d.ts +4 -3
- package/dist/util/decorators.d.ts +5 -0
- package/dist/util/http-error.d.ts +38 -0
- package/dist/util/http-status.d.ts +30 -0
- package/dist/util/request.d.ts +1 -1
- package/dist/util/symbol.d.ts +19 -0
- package/dist/util/types.d.ts +30 -1
- package/package.json +6 -3
- package/dist/http-server-0xH174zz.js.map +0 -1
- package/dist/http-server-DFhwlK8e.cjs.map +0 -1
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import { Background, Controls, Handle, MarkerType, ReactFlow, useEdgesState, useNodesState } from 'https://esm.sh/@xyflow/react@12.3.6?deps=react@18.3.1,react-dom@18.3.1';
|
|
2
|
+
import ELK from 'https://esm.sh/elkjs@0.9.3/lib/elk.bundled.js';
|
|
3
|
+
import { createRoot } from 'https://esm.sh/react-dom@18.3.1/client?deps=react@18.3.1';
|
|
4
|
+
import React, { useEffect, useState } from 'https://esm.sh/react@18.3.1';
|
|
5
|
+
|
|
6
|
+
const elk = new ELK();
|
|
7
|
+
|
|
8
|
+
const NODE_STYLES = {
|
|
9
|
+
router: { background: '#22c58a10', color: '#22c58a', border: '1px solid #22c58a', borderRadius: '8px' },
|
|
10
|
+
controller: { background: 'rgba(124, 58, 237, 0.1)', color: '#a78bfa', border: '1px solid #0a090aff', borderRadius: '6px' },
|
|
11
|
+
middleware: { background: '#7e22ce', color: '#fff', border: '1px solid #6b21a8', borderRadius: '9px', padding: '6px 12px', fontSize: '10px' },
|
|
12
|
+
entrypoint: { background: '#3b82f6', color: '#fff', border: '1px solid #306cce', borderRadius: '12px', padding: '' }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
function renderPath(path) {
|
|
17
|
+
const parts = path.split('/').slice(1);
|
|
18
|
+
|
|
19
|
+
let out = '';
|
|
20
|
+
parts.forEach((part, index) => {
|
|
21
|
+
if (part.startsWith(":")) {
|
|
22
|
+
out += `/<span class="path-segment path-param">${part}</span>`;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (index === parts.length - 1) {
|
|
26
|
+
out += `/<span class="path-segment path-end">${part}</span>`;
|
|
27
|
+
return;
|
|
28
|
+
};
|
|
29
|
+
out += `/<span class="path-segment">${part}</span>`;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const GroupNode = ({ data }) => {
|
|
36
|
+
return React.createElement('div', { style: { padding: '10px', height: '100%' } },
|
|
37
|
+
React.createElement('div', { style: { fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '5px', marginBottom: '5px' } },
|
|
38
|
+
data.type === "controller" ? data.label : "Router: " + data.label
|
|
39
|
+
),
|
|
40
|
+
data.data?.children?.routes?.map((r, i) =>
|
|
41
|
+
React.createElement('div', { key: i, style: { display: 'flex', alignItems: 'center', gap: '8px', fontSize: '12px', margin: '2px 0' } },
|
|
42
|
+
React.createElement('span', {
|
|
43
|
+
style: {
|
|
44
|
+
padding: '2px 6px',
|
|
45
|
+
borderRadius: '4px',
|
|
46
|
+
background: '#0f172a',
|
|
47
|
+
border: `1px solid ${r.method === 'GET' ? '#3b82f6' :
|
|
48
|
+
r.method === 'POST' ? '#22c55e' :
|
|
49
|
+
r.method === 'PUT' ? '#eab308' :
|
|
50
|
+
r.method === 'DELETE' ? '#ef4444' : '#64748b'
|
|
51
|
+
}`,
|
|
52
|
+
color: r.method === 'GET' ? '#3b82f6' :
|
|
53
|
+
r.method === 'POST' ? '#22c55e' :
|
|
54
|
+
r.method === 'PUT' ? '#eab308' :
|
|
55
|
+
r.method === 'DELETE' ? '#ef4444' : '#f8fafc',
|
|
56
|
+
fontWeight: 'bold',
|
|
57
|
+
fontSize: '10px',
|
|
58
|
+
minWidth: '40px',
|
|
59
|
+
textAlign: 'center'
|
|
60
|
+
}
|
|
61
|
+
}, r.method),
|
|
62
|
+
React.createElement('span', {
|
|
63
|
+
style: { fontFamily: 'monospace', color: r.isFailed ? '#ef4444' : '#cbd5e1', fontWeight: r.isFailed ? 'bold' : 'normal' },
|
|
64
|
+
dangerouslySetInnerHTML: { __html: renderPath(r.path) }
|
|
65
|
+
})
|
|
66
|
+
)
|
|
67
|
+
),
|
|
68
|
+
React.createElement(Handle, { type: 'target', position: 'top' }),
|
|
69
|
+
React.createElement(Handle, { type: 'source', position: 'bottom' })
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const EntrypointNode = ({ data }) => {
|
|
74
|
+
return React.createElement('div', { style: { padding: '10px', height: '100%' } },
|
|
75
|
+
React.createElement(
|
|
76
|
+
"svg",
|
|
77
|
+
{
|
|
78
|
+
height: "100%",
|
|
79
|
+
width: "100%",
|
|
80
|
+
version: "1.1",
|
|
81
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
82
|
+
xmlnsXlink: "http://www.w3.org/1999/xlink",
|
|
83
|
+
viewBox: "0 0 512 512",
|
|
84
|
+
xmlSpace: "preserve",
|
|
85
|
+
},
|
|
86
|
+
React.createElement(
|
|
87
|
+
"style",
|
|
88
|
+
{ type: "text/css" },
|
|
89
|
+
".st0{fill:currentColor;}"
|
|
90
|
+
),
|
|
91
|
+
React.createElement(
|
|
92
|
+
"g",
|
|
93
|
+
null,
|
|
94
|
+
React.createElement("path", {
|
|
95
|
+
className: "st0",
|
|
96
|
+
d: "M255.994,0.006C114.607,0.013,0.012,114.612,0,256c0.012,141.387,114.607,255.986,255.994,255.994 C397.393,511.986,511.992,397.387,512,256C511.992,114.612,397.393,0.013,255.994,0.006z M97.607,97.612 c23.34-23.328,51.761-41.475,83.455-52.725c-15.183,18.375-27.84,41.906-37.757,69.116H82.772 C87.452,108.308,92.396,102.824,97.607,97.612z M65.612,138.003h69.986c-9.008,31.929-14.41,67.834-15.363,105.997H32.327 C34.374,205.196,46.3,169.088,65.612,138.003z M65.612,373.997C46.3,342.912,34.374,306.804,32.327,268h87.991 c0.961,38.124,6.21,74.092,15.206,105.998H65.612z M97.607,414.386c-5.211-5.211-10.156-10.695-14.836-16.39h60.573 c4.28,11.774,9.019,22.944,14.312,33.21c6.954,13.438,14.758,25.468,23.348,35.89C149.332,455.846,120.931,437.699,97.607,414.386z M243.998,479.667c-3.746-0.196-7.469-0.477-11.164-0.86c-5.89-2.64-11.722-6.25-17.5-10.961 c-17.632-14.359-33.976-38.671-46.398-69.85h75.061V479.667z M243.998,373.997h-83.436c-9.477-31.171-15.316-67.311-16.328-105.998 h99.763V373.997z M243.998,244H144.31c1.008-38.71,6.875-74.819,16.359-105.997h83.33V244z M243.998,114.003h-74.951 c3.109-7.79,6.367-15.312,9.934-22.195c10.64-20.625,23.17-36.89,36.354-47.656c5.777-4.71,11.609-8.32,17.5-10.96 c3.695-0.382,7.417-0.664,11.164-0.859V114.003z M446.392,138.003c19.312,31.085,31.234,67.194,33.281,105.997h-87.991 c-0.961-38.124-6.21-74.092-15.21-105.997H446.392z M414.393,97.612c5.211,5.211,10.156,10.696,14.836,16.391h-60.577 c-4.281-11.773-9.023-22.945-14.312-33.21c-6.953-13.437-14.758-25.468-23.347-35.89C362.668,56.16,391.065,74.301,414.393,97.612z M267.998,32.333c3.746,0.195,7.469,0.484,11.16,0.859c5.89,2.649,11.723,6.25,17.504,10.96 c17.636,14.359,33.976,38.671,46.397,69.85h-75.061V32.333z M267.998,138.003h83.436c9.476,31.171,15.32,67.31,16.328,105.997 h-99.764V138.003z M267.998,268h99.685c-1.007,38.71-6.874,74.818-16.359,105.998h-83.326V268z M296.661,467.846 c-5.781,4.711-11.614,8.313-17.504,10.961c-3.691,0.375-7.414,0.664-11.16,0.86v-81.67h74.951 c-3.109,7.789-6.367,15.312-9.933,22.195C322.376,440.816,309.845,457.081,296.661,467.846z M414.393,414.386 c-23.336,23.328-51.764,41.476-83.459,52.725c15.187-18.375,27.835-41.905,37.757-69.115h60.538 C424.548,403.692,419.604,409.176,414.393,414.386z M446.392,373.997h-69.998c9.008-31.929,14.414-67.842,15.367-105.998h87.912 C477.626,306.804,465.704,342.912,446.392,373.997z",
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
),
|
|
100
|
+
React.createElement(Handle, { type: 'source', position: 'bottom' })
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const MiddlewareNode = ({ data }) => {
|
|
105
|
+
return React.createElement('div', { style: { padding: '10px', height: '100%' } },
|
|
106
|
+
React.createElement('div', { style: { fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)', paddingBottom: '5px', marginBottom: '5px' } },
|
|
107
|
+
data.label
|
|
108
|
+
),
|
|
109
|
+
React.createElement(Handle, { type: 'target', position: 'top' }),
|
|
110
|
+
React.createElement(Handle, { type: 'source', position: 'bottom' })
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const nodeTypes = {
|
|
115
|
+
controller: GroupNode,
|
|
116
|
+
router: GroupNode,
|
|
117
|
+
middleware: MiddlewareNode,
|
|
118
|
+
entrypoint: EntrypointNode
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const GraphComponent = () => {
|
|
122
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
123
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
124
|
+
const [loading, setLoading] = useState(true);
|
|
125
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
126
|
+
const [zoom, setZoom] = useState(1);
|
|
127
|
+
const [isFullscreen, setIsFullscreen] = useState(false);
|
|
128
|
+
|
|
129
|
+
// Search Filter
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!searchTerm) {
|
|
132
|
+
setNodes(nds => nds.map(node => ({ ...node, style: { ...node.style, opacity: 1 } })));
|
|
133
|
+
setEdges(eds => eds.map(edge => ({ ...edge, style: { ...edge.style, opacity: 1 } })));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const query = searchTerm.toLowerCase();
|
|
138
|
+
const matchedIds = new Set();
|
|
139
|
+
|
|
140
|
+
// Find matches in labels OR in internal routes
|
|
141
|
+
nodes.forEach(node => {
|
|
142
|
+
const labelMatch = node.data.label && node.data.label.toLowerCase().includes(query);
|
|
143
|
+
const routeMatch = node.data.routes && node.data.routes.some(r => r.path.toLowerCase().includes(query));
|
|
144
|
+
|
|
145
|
+
if (labelMatch || routeMatch) matchedIds.add(node.id);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
setNodes(nds => nds.map(node => ({
|
|
149
|
+
...node,
|
|
150
|
+
style: { ...node.style, opacity: matchedIds.has(node.id) ? 1 : 0.1 }
|
|
151
|
+
})));
|
|
152
|
+
|
|
153
|
+
setEdges(eds => eds.map(edge => ({
|
|
154
|
+
...edge,
|
|
155
|
+
style: { ...edge.style, opacity: 0.1 }
|
|
156
|
+
})));
|
|
157
|
+
|
|
158
|
+
}, [searchTerm]);
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
const buildGraph = async () => {
|
|
163
|
+
const registryData = window.registryData;
|
|
164
|
+
if (!registryData) return;
|
|
165
|
+
|
|
166
|
+
const makeId = (type, parent, idx, name) => `${type}_${parent || 'root'}_${idx}_${(name || '').replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
167
|
+
|
|
168
|
+
function getNodeStyle(id) {
|
|
169
|
+
const m = window.metrics?.nodeMetrics && window.metrics.nodeMetrics[id];
|
|
170
|
+
if (!m) return {};
|
|
171
|
+
if (m.failures > 0) {
|
|
172
|
+
// Red intensity based on failures
|
|
173
|
+
const intensity = Math.min(1, m.failures / 5);
|
|
174
|
+
return { backgroundColor: `rgba(239, 68, 68, ${0.2 + intensity * 0.8})`, color: 'white' };
|
|
175
|
+
}
|
|
176
|
+
return {};
|
|
177
|
+
}
|
|
178
|
+
function getEdgeStyle(item) {
|
|
179
|
+
return {
|
|
180
|
+
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function calculateNodeBounds(container) {
|
|
185
|
+
const routes = container.children?.routes || [];
|
|
186
|
+
|
|
187
|
+
// Create a temporary container that matches GroupNode styling
|
|
188
|
+
const wrapper = document.createElement("div");
|
|
189
|
+
wrapper.style.visibility = "hidden";
|
|
190
|
+
wrapper.style.position = "absolute";
|
|
191
|
+
wrapper.style.width = "fit-content";
|
|
192
|
+
wrapper.style.maxWidth = "500px"; // Arbitrary max width
|
|
193
|
+
document.body.appendChild(wrapper);
|
|
194
|
+
|
|
195
|
+
// Mimic GroupNode container
|
|
196
|
+
const nodeEl = document.createElement("div");
|
|
197
|
+
nodeEl.style.padding = "10px";
|
|
198
|
+
nodeEl.style.fontFamily = "Inter, system-ui, sans-serif"; // App font
|
|
199
|
+
nodeEl.style.fontSize = "12px";
|
|
200
|
+
wrapper.appendChild(nodeEl);
|
|
201
|
+
|
|
202
|
+
// Mimic Header
|
|
203
|
+
const header = document.createElement("div");
|
|
204
|
+
header.style.fontWeight = "bold";
|
|
205
|
+
header.style.borderBottom = "1px solid rgba(255,255,255,0.1)";
|
|
206
|
+
header.style.paddingBottom = "5px";
|
|
207
|
+
header.style.marginBottom = "5px";
|
|
208
|
+
// Label logic matches GroupNode
|
|
209
|
+
header.textContent = container.type === "controller"
|
|
210
|
+
? (container.name + (container.metadata?.pluginName ? `\n[${container.metadata.pluginName}]` : ''))
|
|
211
|
+
: "Router: " + container.path;
|
|
212
|
+
nodeEl.appendChild(header);
|
|
213
|
+
|
|
214
|
+
// Mimic Routes
|
|
215
|
+
for (const route of routes) {
|
|
216
|
+
const row = document.createElement("div");
|
|
217
|
+
row.style.display = "flex";
|
|
218
|
+
row.style.alignItems = "center";
|
|
219
|
+
row.style.gap = "8px";
|
|
220
|
+
row.style.margin = "2px 0";
|
|
221
|
+
|
|
222
|
+
const badge = document.createElement("span");
|
|
223
|
+
badge.textContent = route.method;
|
|
224
|
+
badge.style.padding = "2px 6px";
|
|
225
|
+
badge.style.fontSize = "10px";
|
|
226
|
+
badge.style.fontWeight = "bold";
|
|
227
|
+
row.appendChild(badge);
|
|
228
|
+
|
|
229
|
+
const path = document.createElement("span");
|
|
230
|
+
path.textContent = route.path;
|
|
231
|
+
path.style.fontFamily = "monospace";
|
|
232
|
+
// path.style.color... doesn't affect size
|
|
233
|
+
row.appendChild(path);
|
|
234
|
+
|
|
235
|
+
nodeEl.appendChild(row);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const rect = nodeEl.getBoundingClientRect();
|
|
239
|
+
const width = Math.ceil(rect.width) + 20; // Safety buffer
|
|
240
|
+
const height = Math.ceil(rect.height);
|
|
241
|
+
|
|
242
|
+
document.body.removeChild(wrapper);
|
|
243
|
+
|
|
244
|
+
return { width, height };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function addRecursedLevel({ middleware, routes, routers, controllers }, parentId) {
|
|
248
|
+
const restChildrenNodes = [];
|
|
249
|
+
const restChildrenEdges = [];
|
|
250
|
+
|
|
251
|
+
const elkEdges = [];
|
|
252
|
+
const elkNodes = [
|
|
253
|
+
...(middleware || []).map((mw, idx) => {
|
|
254
|
+
const id = makeId("middleware", parentId, idx, mw.name);
|
|
255
|
+
return {
|
|
256
|
+
id,
|
|
257
|
+
label: mw.metadata?.pluginName || mw.name || "Unknown Middleware",
|
|
258
|
+
...calculateNodeBounds(mw),
|
|
259
|
+
// width: 140,
|
|
260
|
+
// height: 40,
|
|
261
|
+
type: "middleware",
|
|
262
|
+
style: getNodeStyle(id),
|
|
263
|
+
data: mw
|
|
264
|
+
};
|
|
265
|
+
}),
|
|
266
|
+
...(routers || []).map((r, idx) => {
|
|
267
|
+
const id = makeId("router", parentId, idx, r.path);
|
|
268
|
+
const { nodes, edges } = addRecursedLevel(r.children, id);
|
|
269
|
+
restChildrenNodes.push(...nodes);
|
|
270
|
+
restChildrenEdges.push(...edges);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
id,
|
|
274
|
+
label: r.path,
|
|
275
|
+
...calculateNodeBounds(r),
|
|
276
|
+
type: "router",
|
|
277
|
+
style: {
|
|
278
|
+
...getNodeStyle(id),
|
|
279
|
+
backgroundColor: 'red'
|
|
280
|
+
},
|
|
281
|
+
data: r
|
|
282
|
+
};
|
|
283
|
+
}),
|
|
284
|
+
...(controllers || []).map((ctrl, idx) => {
|
|
285
|
+
const id = makeId("controller", parentId, idx, ctrl.path);
|
|
286
|
+
|
|
287
|
+
const { nodes, edges } = addRecursedLevel(ctrl.children, id);
|
|
288
|
+
restChildrenNodes.push(...nodes);
|
|
289
|
+
restChildrenEdges.push(...edges);
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
label: ctrl.name + (ctrl.metadata?.pluginName ? `\n[${ctrl.metadata.pluginName}]` : ''),
|
|
294
|
+
...calculateNodeBounds(ctrl),
|
|
295
|
+
type: "controller",
|
|
296
|
+
style: getNodeStyle(id),
|
|
297
|
+
data: ctrl
|
|
298
|
+
};
|
|
299
|
+
})
|
|
300
|
+
].map(n => {
|
|
301
|
+
return {
|
|
302
|
+
style: {},
|
|
303
|
+
...n,
|
|
304
|
+
draggable: false
|
|
305
|
+
};
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
let lastMiddlewareId = "";
|
|
309
|
+
// Create middleware edges
|
|
310
|
+
middleware?.forEach((mw, idx) => {
|
|
311
|
+
const id = makeId("middleware", parentId, idx, mw.name);
|
|
312
|
+
|
|
313
|
+
let sourceId = idx === 0 ?
|
|
314
|
+
parentId ? parentId : "entrypoint-http"
|
|
315
|
+
: makeId("middleware", parentId, idx - 1, middleware[idx - 1].name);
|
|
316
|
+
|
|
317
|
+
elkEdges.push({
|
|
318
|
+
id,
|
|
319
|
+
sources: [sourceId],
|
|
320
|
+
targets: [id],
|
|
321
|
+
type: "straight",
|
|
322
|
+
style: {
|
|
323
|
+
...getEdgeStyle(mw),
|
|
324
|
+
backgroundColor: 'blue'
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
lastMiddlewareId = id;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
routers?.forEach((r, idx) => {
|
|
331
|
+
const id = makeId("router", parentId, idx, r.path);
|
|
332
|
+
elkEdges.push({
|
|
333
|
+
id,
|
|
334
|
+
sources: [lastMiddlewareId || parentId],
|
|
335
|
+
targets: [id],
|
|
336
|
+
style: {
|
|
337
|
+
...getEdgeStyle(r),
|
|
338
|
+
backgroundColor: 'blue'
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
controllers?.forEach((ctrl, idx) => {
|
|
343
|
+
const id = makeId("controller", parentId, idx, ctrl.path);
|
|
344
|
+
console.log({ id, lastMiddlewareId });
|
|
345
|
+
elkEdges.push({
|
|
346
|
+
id,
|
|
347
|
+
sources: [lastMiddlewareId || parentId],
|
|
348
|
+
targets: [id],
|
|
349
|
+
style: {
|
|
350
|
+
...getEdgeStyle(ctrl),
|
|
351
|
+
backgroundColor: 'blue'
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const nodes = elkNodes.concat(restChildrenNodes);
|
|
357
|
+
const edges = elkEdges.concat(restChildrenEdges);
|
|
358
|
+
|
|
359
|
+
return { nodes, edges };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const { nodes: elkNodes, edges: elkEdges } = addRecursedLevel(registryData);
|
|
363
|
+
elkNodes.push({
|
|
364
|
+
id: "entrypoint-http", width: 64, height: 64, type: "entrypoint"
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const nodeNodeGap = '20';
|
|
368
|
+
const nodeEdgeGap = '20';
|
|
369
|
+
const graph = {
|
|
370
|
+
id: 'root',
|
|
371
|
+
layoutOptions: {
|
|
372
|
+
'elk.algorithm': 'layered',
|
|
373
|
+
'elk.direction': 'DOWN',
|
|
374
|
+
'elk.spacing.nodeNode': nodeNodeGap,
|
|
375
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': nodeNodeGap,
|
|
376
|
+
'elk.spacing.edgeNode': nodeEdgeGap,
|
|
377
|
+
'elk.layered.spacing.edgeEdgeBetweenLayers': nodeEdgeGap,
|
|
378
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': nodeEdgeGap,
|
|
379
|
+
'elk.layered.wrapping.additionalEdgeSpacing': nodeEdgeGap,
|
|
380
|
+
'elk.layered.nodePlacement.strategy': 'NETWORK_SIMPLEX',
|
|
381
|
+
},
|
|
382
|
+
children: elkNodes,
|
|
383
|
+
edges: elkEdges
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const layoutedGraph = await elk.layout(graph);
|
|
387
|
+
|
|
388
|
+
const flowNodes = [];
|
|
389
|
+
|
|
390
|
+
const processLayoutedNode = (node, parentId = undefined) => {
|
|
391
|
+
if (node.id === 'root') {
|
|
392
|
+
node.children?.forEach(c => processLayoutedNode(c));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const style = NODE_STYLES[node.type] || {};
|
|
397
|
+
const isGroup = node.children && node.children.length > 0;
|
|
398
|
+
|
|
399
|
+
flowNodes.push({
|
|
400
|
+
id: node.id,
|
|
401
|
+
position: { x: node.x, y: node.y },
|
|
402
|
+
data: node,
|
|
403
|
+
style: {
|
|
404
|
+
...style,
|
|
405
|
+
width: node.width,
|
|
406
|
+
height: node.height,
|
|
407
|
+
zIndex: isGroup ? -1 : 1
|
|
408
|
+
},
|
|
409
|
+
type: node.type,
|
|
410
|
+
parentNode: parentId,
|
|
411
|
+
draggable: false,
|
|
412
|
+
extent: 'parent'
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (node.children) {
|
|
416
|
+
node.children.forEach(c => processLayoutedNode(c, node.id));
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
processLayoutedNode(layoutedGraph);
|
|
421
|
+
|
|
422
|
+
const flowEdges = elkEdges.map(e => ({
|
|
423
|
+
id: e.id,
|
|
424
|
+
source: e.sources[0],
|
|
425
|
+
target: e.targets[0],
|
|
426
|
+
type: e.type || 'smoothstep',
|
|
427
|
+
animated: true,
|
|
428
|
+
style: { stroke: '#475569', strokeWidth: 2 },
|
|
429
|
+
markerEnd: {
|
|
430
|
+
type: MarkerType.ArrowClosed,
|
|
431
|
+
},
|
|
432
|
+
}));
|
|
433
|
+
|
|
434
|
+
setNodes(flowNodes);
|
|
435
|
+
setEdges(flowEdges);
|
|
436
|
+
|
|
437
|
+
setLoading(false);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
buildGraph();
|
|
441
|
+
}, []);
|
|
442
|
+
|
|
443
|
+
if (loading) return React.createElement('div', { style: { color: '#fff', padding: '20px' } }, 'Laying out graph...');
|
|
444
|
+
|
|
445
|
+
return React.createElement('div', {
|
|
446
|
+
style: isFullscreen ? {
|
|
447
|
+
position: 'fixed',
|
|
448
|
+
top: 0,
|
|
449
|
+
left: 0,
|
|
450
|
+
width: '100vw',
|
|
451
|
+
height: '100vh',
|
|
452
|
+
zIndex: 9999,
|
|
453
|
+
backgroundColor: '#1e293b' // Match theme
|
|
454
|
+
} : {
|
|
455
|
+
width: '100%',
|
|
456
|
+
height: '600px', // Ensure explicit height if container doesn't provide it, though normally #cy does.
|
|
457
|
+
position: 'relative'
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
React.createElement(ReactFlow, {
|
|
461
|
+
nodes,
|
|
462
|
+
edges,
|
|
463
|
+
onNodesChange,
|
|
464
|
+
onEdgesChange,
|
|
465
|
+
nodeTypes, // Register custom types
|
|
466
|
+
fitView: true,
|
|
467
|
+
minZoom: 0.1,
|
|
468
|
+
onMove: (_, viewport) => setZoom(viewport.zoom),
|
|
469
|
+
onInit: (instance) => setZoom(instance.getZoom())
|
|
470
|
+
},
|
|
471
|
+
React.createElement(Background, { color: '#334155', gap: 16 }),
|
|
472
|
+
React.createElement(Controls),
|
|
473
|
+
React.createElement('div', {
|
|
474
|
+
style: {
|
|
475
|
+
position: 'absolute',
|
|
476
|
+
top: '10px',
|
|
477
|
+
right: '10px',
|
|
478
|
+
zIndex: 5,
|
|
479
|
+
display: 'flex',
|
|
480
|
+
gap: '8px',
|
|
481
|
+
alignItems: 'center'
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
React.createElement('div', {
|
|
485
|
+
style: {
|
|
486
|
+
background: 'rgba(30, 41, 59, 0.8)',
|
|
487
|
+
color: 'white',
|
|
488
|
+
padding: '6px 12px',
|
|
489
|
+
borderRadius: '6px',
|
|
490
|
+
fontSize: '12px',
|
|
491
|
+
fontFamily: 'monospace',
|
|
492
|
+
border: '1px solid #475569'
|
|
493
|
+
}
|
|
494
|
+
}, `${Math.round(zoom * 100)}%`),
|
|
495
|
+
React.createElement('button', {
|
|
496
|
+
onClick: () => setIsFullscreen(!isFullscreen),
|
|
497
|
+
style: {
|
|
498
|
+
background: '#3b82f6',
|
|
499
|
+
color: 'white',
|
|
500
|
+
border: 'none',
|
|
501
|
+
padding: '6px 12px',
|
|
502
|
+
borderRadius: '6px',
|
|
503
|
+
cursor: 'pointer',
|
|
504
|
+
fontSize: '12px',
|
|
505
|
+
fontWeight: 'bold',
|
|
506
|
+
transition: 'background 0.2s'
|
|
507
|
+
}
|
|
508
|
+
}, isFullscreen ? "Exit Fullscreen" : "Fullscreen")
|
|
509
|
+
)
|
|
510
|
+
)
|
|
511
|
+
);
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
function mountReact() {
|
|
515
|
+
const container = document.getElementById('cy');
|
|
516
|
+
if (container && !container._reactRoot) {
|
|
517
|
+
const root = createRoot(container);
|
|
518
|
+
container._reactRoot = root;
|
|
519
|
+
root.render(React.createElement(GraphComponent));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
window.initGraph = mountReact;
|
|
523
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
|
|
2
|
+
function printDuration(deltaMs/** number in ms */) {
|
|
3
|
+
|
|
4
|
+
// More than 1 week
|
|
5
|
+
if (deltaMs > (7 * 24 * 60 * 60 * 1000)) {
|
|
6
|
+
const weeks = (deltaMs / (7 * 24 * 60 * 60 * 1000));
|
|
7
|
+
const weeksR = (deltaMs % (7 * 24 * 60 * 60 * 1000));
|
|
8
|
+
|
|
9
|
+
if (weeks >= 10) {
|
|
10
|
+
return weeks.toFixed(0) + " weeks";
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
const days = (weeksR / (24 * 60 * 60 * 1000));
|
|
14
|
+
|
|
15
|
+
if (days >= 1) {
|
|
16
|
+
return weeks.toFixed(0) + " weeks " + days.toFixed(0) + " days";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return weeks.toFixed(0) + " weeks";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// More than 1 day
|
|
23
|
+
else if (deltaMs > (24 * 60 * 60 * 1000)) {
|
|
24
|
+
const days = (deltaMs / (24 * 60 * 60 * 1000));
|
|
25
|
+
const daysR = (deltaMs % (24 * 60 * 60 * 1000));
|
|
26
|
+
|
|
27
|
+
if (days >= 10) {
|
|
28
|
+
return days.toFixed(0) + " days";
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const hours = (daysR / (60 * 60 * 1000));
|
|
32
|
+
|
|
33
|
+
if (hours >= 1) {
|
|
34
|
+
return days.toFixed(0) + " days " + hours.toFixed(0) + " hours";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return days.toFixed(0) + " days";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// More than 1 hour
|
|
41
|
+
else if (deltaMs > (60 * 60 * 1000)) {
|
|
42
|
+
const hours = (deltaMs / (60 * 60 * 1000));
|
|
43
|
+
const hoursR = (deltaMs % (60 * 60 * 1000));
|
|
44
|
+
|
|
45
|
+
if (hours >= 10) {
|
|
46
|
+
return hours.toFixed(0) + " hours";
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const minutes = (hoursR / (60 * 1000));
|
|
50
|
+
|
|
51
|
+
if (minutes >= 1) {
|
|
52
|
+
return hours.toFixed(0) + " hours " + minutes.toFixed(0) + " minutes";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return hours.toFixed(0) + " hours";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// less than an hour, print minutes
|
|
59
|
+
else if (deltaMs > 60 * 1000) {
|
|
60
|
+
const minutes = (deltaMs / (60 * 1000));
|
|
61
|
+
const minutesR = (deltaMs % (60 * 1000));
|
|
62
|
+
|
|
63
|
+
if (minutes >= 10) {
|
|
64
|
+
return minutes.toFixed(0) + " minutes";
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
const seconds = (minutesR / (1000));
|
|
68
|
+
|
|
69
|
+
if (seconds >= 1) {
|
|
70
|
+
return minutes.toFixed(0) + " minutes " + seconds.toFixed(0) + " seconds";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return minutes.toFixed(0) + " minutes";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Seconds (whoa)
|
|
77
|
+
else if (deltaMs > 1000) {
|
|
78
|
+
const seconds = (deltaMs / (60 * 1000));
|
|
79
|
+
|
|
80
|
+
if (seconds >= 10) {
|
|
81
|
+
return seconds.toFixed(0) + " seconds";
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
return seconds.toFixed(2) + " seconds";
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Milliseconds
|
|
88
|
+
else if (deltaMs > 1) {
|
|
89
|
+
const ms = deltaMs;
|
|
90
|
+
return ms.toFixed(0) + " ms";
|
|
91
|
+
}
|
|
92
|
+
// Microseconds
|
|
93
|
+
else if (deltaMs > 0.0001) {
|
|
94
|
+
const us = deltaMs;
|
|
95
|
+
return us.toFixed(0) + " us";
|
|
96
|
+
}
|
|
97
|
+
else return "N/A";
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function updateDashboard() {
|
|
101
|
+
try {
|
|
102
|
+
const headers = getRequestHeaders ? getRequestHeaders() : {};
|
|
103
|
+
const interval = document.getElementById('time-range-selector')?.value || '1m';
|
|
104
|
+
// Handle relative path issue when accessing /admin without trailing slash
|
|
105
|
+
const metricsUrl = (window.location.pathname.endsWith('/') ? 'metrics' : window.location.pathname + '/metrics') + `?interval=${interval}`;
|
|
106
|
+
const res = await fetch(metricsUrl, { headers });
|
|
107
|
+
if (!res.ok) return;
|
|
108
|
+
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
const metrics = data.metrics;
|
|
111
|
+
window.metrics = metrics; // Update global metrics for tooltips
|
|
112
|
+
|
|
113
|
+
// Refresh Registry if active
|
|
114
|
+
if (document.getElementById('tab-registry').classList.contains('active')) {
|
|
115
|
+
const registryContainer = document.getElementById('registry-tree');
|
|
116
|
+
// Simple clear and re-render (optimization: could just update tooltips)
|
|
117
|
+
registryContainer.innerHTML = '';
|
|
118
|
+
window.renderRegistry(window.registryData, registryContainer);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
document.getElementById('uptime').innerText = data.uptime;
|
|
122
|
+
document.getElementById('total-requests').innerText = metrics.totalRequests;
|
|
123
|
+
document.getElementById('active-requests').innerText = metrics.activeRequests;
|
|
124
|
+
document.getElementById('successful-requests').innerText = metrics.successfulRequests;
|
|
125
|
+
document.getElementById('failed-requests').innerText = metrics.failedRequests;
|
|
126
|
+
document.getElementById('avg-latency').innerText = metrics.averageTotalTime_ms.toFixed(2);
|
|
127
|
+
|
|
128
|
+
// Recalc rates
|
|
129
|
+
const finishedRequests = metrics.totalRequests - metrics.activeRequests;
|
|
130
|
+
const successRate = finishedRequests ? Math.round((metrics.successfulRequests / finishedRequests) * 100) : 100;
|
|
131
|
+
const failRate = finishedRequests ? Math.round((metrics.failedRequests / finishedRequests) * 100) : 0;
|
|
132
|
+
|
|
133
|
+
document.getElementById('success-rate').innerText = successRate + '%';
|
|
134
|
+
document.getElementById('fail-rate').innerText = failRate + '%';
|
|
135
|
+
|
|
136
|
+
} catch (err) {
|
|
137
|
+
console.error("Failed to update dashboard", err);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Auto-refresh every 2 seconds
|
|
142
|
+
setInterval(updateDashboard, 2000);
|
|
143
|
+
// Initial load
|
|
144
|
+
updateDashboard();
|
|
145
|
+
|
|
146
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.react-flow__controls-button {
|
|
2
|
+
color: #000 !important;
|
|
3
|
+
fill: #000 !important;
|
|
4
|
+
border-bottom: 1px solid #eee;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.react-flow__controls-button svg {
|
|
8
|
+
fill: currentColor;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.react-flow__node.react-flow__node-router {
|
|
12
|
+
height: fit-content !important;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.react-flow__handle {
|
|
16
|
+
opacity: 0;
|
|
17
|
+
pointer-events: none;
|
|
18
|
+
}
|