eva4j 1.0.13 → 1.0.14
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/AGENTS.md +51 -9
- package/DOMAIN_YAML_GUIDE.md +150 -0
- package/bin/eva4j.js +31 -1
- package/design-system.md +797 -0
- package/docs/commands/EVALUATE_SYSTEM.md +542 -0
- package/docs/commands/GENERATE_ENTITIES.md +196 -0
- package/docs/commands/INDEX.md +10 -1
- package/examples/domain-endpoints-relations.yaml +353 -0
- package/examples/domain-endpoints-versioned.yaml +144 -0
- package/examples/domain-endpoints.yaml +135 -0
- package/examples/system.yaml +289 -0
- package/package.json +1 -1
- package/src/commands/create.js +6 -3
- package/src/commands/evaluate-system.js +384 -0
- package/src/commands/generate-entities.js +677 -14
- package/src/commands/generate-kafka-event.js +59 -5
- package/src/commands/generate-system.js +243 -0
- package/src/generators/base-generator.js +9 -1
- package/src/utils/naming.js +3 -2
- package/src/utils/system-validator.js +314 -0
- package/src/utils/yaml-to-entity.js +31 -2
- package/templates/aggregate/AggregateRepository.java.ejs +5 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +9 -0
- package/templates/aggregate/DomainEventHandler.java.ejs +24 -20
- package/templates/aggregate/JpaRepository.java.ejs +5 -0
- package/templates/base/root/skill-build-domain-yaml-references-generate-entities.md.ejs +1103 -0
- package/templates/base/root/skill-build-domain-yaml.ejs +292 -0
- package/templates/base/root/skill-build-system-yaml.ejs +252 -0
- package/templates/base/root/system.yaml.ejs +97 -0
- package/templates/crud/EndpointsController.java.ejs +178 -0
- package/templates/crud/FindByQuery.java.ejs +17 -0
- package/templates/crud/FindByQueryHandler.java.ejs +57 -0
- package/templates/crud/ScaffoldCommand.java.ejs +12 -0
- package/templates/crud/ScaffoldCommandHandler.java.ejs +43 -0
- package/templates/crud/ScaffoldQuery.java.ejs +12 -0
- package/templates/crud/ScaffoldQueryHandler.java.ejs +40 -0
- package/templates/crud/SubEntityAddCommand.java.ejs +17 -0
- package/templates/crud/SubEntityAddCommandHandler.java.ejs +43 -0
- package/templates/crud/SubEntityRemoveCommand.java.ejs +9 -0
- package/templates/crud/SubEntityRemoveCommandHandler.java.ejs +42 -0
- package/templates/crud/TransitionCommand.java.ejs +9 -0
- package/templates/crud/TransitionCommandHandler.java.ejs +39 -0
- package/templates/evaluate/report.html.ejs +971 -0
- package/templates/kafka-event/Event.java.ejs +7 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="es">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title><%= data.systemName %> — eva4j Architecture Validator</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet" />
|
|
10
|
+
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
|
11
|
+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
|
12
|
+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
13
|
+
<script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
|
|
14
|
+
<style>
|
|
15
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
16
|
+
body { background: #0a0a0f; color: #e8e8f0; font-family: 'Plus Jakarta Sans', system-ui, -apple-system, sans-serif; }
|
|
17
|
+
::-webkit-scrollbar { width: 6px; }
|
|
18
|
+
::-webkit-scrollbar-track { background: #0a0a0f; }
|
|
19
|
+
::-webkit-scrollbar-thumb { background: #2e2e50; border-radius: 3px; }
|
|
20
|
+
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
21
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
22
|
+
@keyframes shimmer { 0% { box-shadow: 0 0 0 0 rgba(230,57,80,0.4); } 70% { box-shadow: 0 0 0 8px rgba(230,57,80,0); } 100% { box-shadow: 0 0 0 0 rgba(230,57,80,0); } }
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<div id="root"></div>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
(function () {
|
|
30
|
+
var _d = '<%- Buffer.from(JSON.stringify(data)).toString("base64") %>';
|
|
31
|
+
var bytes = Uint8Array.from(atob(_d), function(c) { return c.charCodeAt(0); });
|
|
32
|
+
window.__EVA_DATA__ = JSON.parse(new TextDecoder('utf-8').decode(bytes));
|
|
33
|
+
})();
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script type="text/babel">
|
|
37
|
+
const { useState, useRef, useEffect } = React;
|
|
38
|
+
|
|
39
|
+
// ─── Injected data ──────────────────────────────────────────────────────
|
|
40
|
+
const {
|
|
41
|
+
systemName,
|
|
42
|
+
modules: MODULES_LIST,
|
|
43
|
+
events: EVENTS,
|
|
44
|
+
syncIntegrations: SYNC_INTEGRATIONS,
|
|
45
|
+
endpoints: ENDPOINTS,
|
|
46
|
+
flows: FLOWS_LIST,
|
|
47
|
+
validation: VALIDATION,
|
|
48
|
+
generatedAt,
|
|
49
|
+
} = window.__EVA_DATA__;
|
|
50
|
+
|
|
51
|
+
// Convert arrays to maps for fast lookup
|
|
52
|
+
const MODULES = Object.fromEntries(MODULES_LIST.map(m => [m.id, m]));
|
|
53
|
+
const FLOWS = Object.fromEntries(FLOWS_LIST.map(f => [f.id, f]));
|
|
54
|
+
|
|
55
|
+
// ─── Design system ──────────────────────────────────────────────────────
|
|
56
|
+
const C = {
|
|
57
|
+
bg: "#0a0a0f",
|
|
58
|
+
surface: "#12121a",
|
|
59
|
+
surfaceHover: "#1a1a28",
|
|
60
|
+
border: "#1e1e30",
|
|
61
|
+
borderBright: "#2e2e50",
|
|
62
|
+
accent: "#e63950",
|
|
63
|
+
gold: "#f5c842",
|
|
64
|
+
green: "#2dcc8f",
|
|
65
|
+
blue: "#4a9eff",
|
|
66
|
+
purple: "#9b6dff",
|
|
67
|
+
orange: "#ff8c42",
|
|
68
|
+
text: "#e8e8f0",
|
|
69
|
+
textMuted: "#8c8caa",
|
|
70
|
+
textDim: "#b4b4cc",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ─── Primitive components ───────────────────────────────────────────────
|
|
74
|
+
const Tag = ({ color, children }) => (
|
|
75
|
+
<span style={{
|
|
76
|
+
background: color + "22", color, border: `1px solid ${color}44`,
|
|
77
|
+
borderRadius: 4, padding: "1px 8px", fontSize: 11, fontWeight: 600,
|
|
78
|
+
fontFamily: "'JetBrains Mono', 'Courier New', monospace", letterSpacing: 0.3, display: "inline-block",
|
|
79
|
+
}}>
|
|
80
|
+
{children}
|
|
81
|
+
</span>
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const Badge = ({ color, children }) => (
|
|
85
|
+
<span style={{
|
|
86
|
+
background: color + "33", color, borderRadius: 20,
|
|
87
|
+
padding: "2px 10px", fontSize: 11, fontWeight: 700,
|
|
88
|
+
}}>
|
|
89
|
+
{children}
|
|
90
|
+
</span>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// ─── ModuleCard ─────────────────────────────────────────────────────────
|
|
94
|
+
function ModuleCard({ id, selected, onClick }) {
|
|
95
|
+
const mod = MODULES[id];
|
|
96
|
+
if (!mod) return null;
|
|
97
|
+
return (
|
|
98
|
+
<div onClick={onClick} style={{
|
|
99
|
+
background: selected ? mod.color + "22" : C.surface,
|
|
100
|
+
border: `1px solid ${selected ? mod.color : C.border}`,
|
|
101
|
+
borderRadius: 10, padding: "10px 14px", cursor: "pointer",
|
|
102
|
+
transition: "all 0.18s", display: "flex", alignItems: "center", gap: 10,
|
|
103
|
+
boxShadow: selected ? `0 0 14px ${mod.color}33` : "none",
|
|
104
|
+
}}>
|
|
105
|
+
<span style={{ fontSize: 20 }}>{mod.icon}</span>
|
|
106
|
+
<div>
|
|
107
|
+
<div style={{ fontWeight: 700, color: selected ? mod.color : C.text, fontSize: 13 }}>{mod.label}</div>
|
|
108
|
+
<div style={{ color: C.textMuted, fontSize: 10, marginTop: 1 }}>{mod.desc}</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── ValidationTab ──────────────────────────────────────────────────────
|
|
115
|
+
function ValidationTab() {
|
|
116
|
+
const [expanded, setExpanded] = useState({ errors: true, warnings: true, ok: false });
|
|
117
|
+
|
|
118
|
+
const score = VALIDATION.score;
|
|
119
|
+
const scoreColor = score > 80 ? C.green : score > 60 ? C.gold : C.accent;
|
|
120
|
+
|
|
121
|
+
function Section({ title, items, color, icon, sectionKey }) {
|
|
122
|
+
const isOpen = expanded[sectionKey];
|
|
123
|
+
return (
|
|
124
|
+
<div style={{ marginBottom: 16 }}>
|
|
125
|
+
<div
|
|
126
|
+
onClick={() => setExpanded(e => ({ ...e, [sectionKey]: !e[sectionKey] }))}
|
|
127
|
+
style={{
|
|
128
|
+
display: "flex", alignItems: "center", gap: 10, cursor: "pointer",
|
|
129
|
+
padding: "10px 16px", background: C.surface, border: `1px solid ${C.border}`,
|
|
130
|
+
borderRadius: isOpen ? "8px 8px 0 0" : 8, userSelect: "none",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<span style={{ fontSize: 18 }}>{icon}</span>
|
|
134
|
+
<span style={{ fontWeight: 700, color, flex: 1 }}>{title}</span>
|
|
135
|
+
<Badge color={color}>{items.length}</Badge>
|
|
136
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>{isOpen ? "▲" : "▼"}</span>
|
|
137
|
+
</div>
|
|
138
|
+
{isOpen && items.length > 0 && (
|
|
139
|
+
<div style={{ border: `1px solid ${C.border}`, borderTop: "none", borderRadius: "0 0 8px 8px", overflow: "hidden" }}>
|
|
140
|
+
{items.map((item, i) => (
|
|
141
|
+
<div key={i} style={{
|
|
142
|
+
padding: "10px 16px",
|
|
143
|
+
borderBottom: i < items.length - 1 ? `1px solid ${C.border}` : "none",
|
|
144
|
+
display: "flex", alignItems: "flex-start", gap: 10,
|
|
145
|
+
background: i % 2 === 0 ? C.bg : C.surface,
|
|
146
|
+
}}>
|
|
147
|
+
<span style={{ color, marginTop: 1, flexShrink: 0 }}>•</span>
|
|
148
|
+
<span style={{ color: C.textDim, fontSize: 13, lineHeight: 1.6 }}>
|
|
149
|
+
{typeof item === "string" ? item : item.msg}
|
|
150
|
+
</span>
|
|
151
|
+
</div>
|
|
152
|
+
))}
|
|
153
|
+
</div>
|
|
154
|
+
)}
|
|
155
|
+
{isOpen && items.length === 0 && (
|
|
156
|
+
<div style={{ border: `1px solid ${C.border}`, borderTop: "none", borderRadius: "0 0 8px 8px", padding: "12px 16px", background: C.bg }}>
|
|
157
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>— ninguno —</span>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<div>
|
|
166
|
+
{/* Score cards */}
|
|
167
|
+
<div style={{ display: "flex", gap: 16, marginBottom: 24, flexWrap: "wrap" }}>
|
|
168
|
+
<div style={{
|
|
169
|
+
flex: 1, minWidth: 160, background: C.surface, border: `1px solid ${scoreColor}44`,
|
|
170
|
+
borderRadius: 12, padding: 20, textAlign: "center",
|
|
171
|
+
boxShadow: `0 0 24px ${scoreColor}22`,
|
|
172
|
+
}}>
|
|
173
|
+
<div style={{ fontSize: 52, fontWeight: 900, color: scoreColor, fontFamily: "monospace", lineHeight: 1 }}>
|
|
174
|
+
{score}%
|
|
175
|
+
</div>
|
|
176
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginTop: 6, letterSpacing: 1 }}>SCORE DE CALIDAD</div>
|
|
177
|
+
</div>
|
|
178
|
+
{[
|
|
179
|
+
{ label: "Errores", count: VALIDATION.errors.length, color: C.accent, icon: "🔴" },
|
|
180
|
+
{ label: "Advertencias", count: VALIDATION.warnings.length, color: C.gold, icon: "🟡" },
|
|
181
|
+
{ label: "Validados", count: VALIDATION.ok.length, color: C.green, icon: "🟢" },
|
|
182
|
+
].map(s => (
|
|
183
|
+
<div key={s.label} style={{
|
|
184
|
+
flex: 1, minWidth: 130, background: C.surface, border: `1px solid ${C.border}`,
|
|
185
|
+
borderRadius: 12, padding: 20, textAlign: "center",
|
|
186
|
+
}}>
|
|
187
|
+
<div style={{ fontSize: 38, fontWeight: 900, color: s.color, fontFamily: "monospace" }}>{s.count}</div>
|
|
188
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginTop: 6, letterSpacing: 0.5 }}>
|
|
189
|
+
{s.icon} {s.label.toUpperCase()}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<Section title="Errores críticos" items={VALIDATION.errors} color={C.accent} icon="🔴" sectionKey="errors" />
|
|
196
|
+
<Section title="Advertencias" items={VALIDATION.warnings} color={C.gold} icon="🟡" sectionKey="warnings" />
|
|
197
|
+
<Section title="Validaciones pasadas" items={VALIDATION.ok} color={C.green} icon="🟢" sectionKey="ok" />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ─── FlowSimulator ──────────────────────────────────────────────────────
|
|
203
|
+
function FlowSimulator() {
|
|
204
|
+
const defaultFlow = FLOWS_LIST[0]?.id || null;
|
|
205
|
+
const [selectedFlowId, setSelectedFlowId] = useState(defaultFlow);
|
|
206
|
+
const [currentStep, setCurrentStep] = useState(-1);
|
|
207
|
+
const [running, setRunning] = useState(false);
|
|
208
|
+
const [completed, setCompleted] = useState(false);
|
|
209
|
+
const intervalRef = useRef(null);
|
|
210
|
+
|
|
211
|
+
const flow = FLOWS[selectedFlowId] || FLOWS_LIST[0];
|
|
212
|
+
|
|
213
|
+
if (!flow) {
|
|
214
|
+
return (
|
|
215
|
+
<div style={{ color: C.textMuted, padding: 40, textAlign: "center" }}>
|
|
216
|
+
No hay flujos de eventos declarados en integrations.async
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const reset = () => {
|
|
222
|
+
clearInterval(intervalRef.current);
|
|
223
|
+
setCurrentStep(-1);
|
|
224
|
+
setRunning(false);
|
|
225
|
+
setCompleted(false);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const selectFlow = (id) => {
|
|
229
|
+
reset();
|
|
230
|
+
setSelectedFlowId(id);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const run = () => {
|
|
234
|
+
if (running) return;
|
|
235
|
+
reset();
|
|
236
|
+
setRunning(true);
|
|
237
|
+
let step = 0;
|
|
238
|
+
setCurrentStep(0);
|
|
239
|
+
intervalRef.current = setInterval(() => {
|
|
240
|
+
step++;
|
|
241
|
+
if (step >= flow.steps.length) {
|
|
242
|
+
clearInterval(intervalRef.current);
|
|
243
|
+
setRunning(false);
|
|
244
|
+
setCompleted(true);
|
|
245
|
+
setCurrentStep(flow.steps.length);
|
|
246
|
+
} else {
|
|
247
|
+
setCurrentStep(step);
|
|
248
|
+
}
|
|
249
|
+
}, 1100);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
useEffect(() => () => clearInterval(intervalRef.current), []);
|
|
253
|
+
|
|
254
|
+
const getStepIcon = (step) => {
|
|
255
|
+
if (step.type === "event") return "⚡";
|
|
256
|
+
if (step.type === "external") return "🌐";
|
|
257
|
+
if (step.type === "action") return "📤";
|
|
258
|
+
return "→";
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const getStepColor = (step) => {
|
|
262
|
+
if (step.type === "event") return C.gold;
|
|
263
|
+
if (step.type === "external") return C.purple;
|
|
264
|
+
if (step.type === "action") return C.green;
|
|
265
|
+
return C.blue;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const getActorColor = (actor) => {
|
|
269
|
+
if (MODULES[actor]) return MODULES[actor].color;
|
|
270
|
+
if (actor === "client" || actor === "organizer") return C.green;
|
|
271
|
+
if (actor === "admin") return C.purple;
|
|
272
|
+
if (actor === "gateway") return C.textDim;
|
|
273
|
+
return C.textMuted;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<div>
|
|
278
|
+
{/* Flow selector tabs */}
|
|
279
|
+
<div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
|
|
280
|
+
{FLOWS_LIST.map(f => (
|
|
281
|
+
<button key={f.id} onClick={() => selectFlow(f.id)} style={{
|
|
282
|
+
background: selectedFlowId === f.id ? f.color + "22" : C.surface,
|
|
283
|
+
border: `1px solid ${selectedFlowId === f.id ? f.color : C.border}`,
|
|
284
|
+
color: selectedFlowId === f.id ? f.color : C.textMuted,
|
|
285
|
+
borderRadius: 8, padding: "7px 12px", cursor: "pointer",
|
|
286
|
+
fontWeight: selectedFlowId === f.id ? 700 : 400, fontSize: 12,
|
|
287
|
+
transition: "all 0.15s", display: "flex", alignItems: "center", gap: 6,
|
|
288
|
+
boxShadow: selectedFlowId === f.id ? `0 0 12px ${f.color}33` : "none",
|
|
289
|
+
fontFamily: "inherit",
|
|
290
|
+
}}>
|
|
291
|
+
<span>{f.icon}</span>{f.label}
|
|
292
|
+
</button>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Flow header */}
|
|
297
|
+
<div style={{
|
|
298
|
+
background: C.surface, border: `1px solid ${flow.color}44`, borderRadius: 10,
|
|
299
|
+
padding: 16, marginBottom: 20, display: "flex", alignItems: "center", gap: 16,
|
|
300
|
+
}}>
|
|
301
|
+
<span style={{ fontSize: 32 }}>{flow.icon}</span>
|
|
302
|
+
<div style={{ flex: 1 }}>
|
|
303
|
+
<div style={{ fontWeight: 700, color: flow.color, fontSize: 16 }}>{flow.label}</div>
|
|
304
|
+
<div style={{ color: C.textDim, fontSize: 12, marginTop: 3 }}>{flow.description}</div>
|
|
305
|
+
</div>
|
|
306
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
307
|
+
<button onClick={run} disabled={running} style={{
|
|
308
|
+
background: running ? C.border : flow.color, color: "#fff",
|
|
309
|
+
border: "none", borderRadius: 8, padding: "10px 20px",
|
|
310
|
+
cursor: running ? "not-allowed" : "pointer", fontWeight: 700, fontSize: 14,
|
|
311
|
+
transition: "all 0.15s", opacity: running ? 0.6 : 1, fontFamily: "inherit",
|
|
312
|
+
}}>
|
|
313
|
+
{running ? "⏳ Ejecutando..." : completed ? "▶ Re-ejecutar" : "▶ Simular"}
|
|
314
|
+
</button>
|
|
315
|
+
<button onClick={reset} style={{
|
|
316
|
+
background: "transparent", color: C.textMuted,
|
|
317
|
+
border: `1px solid ${C.border}`, borderRadius: 8,
|
|
318
|
+
padding: "10px 14px", cursor: "pointer", fontFamily: "inherit",
|
|
319
|
+
}}>⟳</button>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
{/* Steps timeline */}
|
|
324
|
+
<div style={{ position: "relative" }}>
|
|
325
|
+
{flow.steps.map((step, i) => {
|
|
326
|
+
const isActive = currentStep === i;
|
|
327
|
+
const isDone = currentStep > i;
|
|
328
|
+
const stepColor = getStepColor(step);
|
|
329
|
+
|
|
330
|
+
return (
|
|
331
|
+
<div key={step.id} style={{ display: "flex", gap: 16, marginBottom: 8, alignItems: "flex-start" }}>
|
|
332
|
+
{/* Timeline dot */}
|
|
333
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 36, flexShrink: 0 }}>
|
|
334
|
+
<div style={{
|
|
335
|
+
width: 36, height: 36, borderRadius: "50%",
|
|
336
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
337
|
+
background: isDone ? C.green + "22" : isActive ? stepColor + "33" : C.surface,
|
|
338
|
+
border: `2px solid ${isDone ? C.green : isActive ? stepColor : C.border}`,
|
|
339
|
+
color: isDone ? C.green : isActive ? stepColor : C.textMuted,
|
|
340
|
+
fontWeight: 700, fontSize: 13, transition: "all 0.3s",
|
|
341
|
+
boxShadow: isActive ? `0 0 16px ${stepColor}44` : "none",
|
|
342
|
+
}}>
|
|
343
|
+
{isDone ? "✓" : step.id}
|
|
344
|
+
</div>
|
|
345
|
+
{i < flow.steps.length - 1 && (
|
|
346
|
+
<div style={{
|
|
347
|
+
width: 2, flex: 1, minHeight: 24,
|
|
348
|
+
background: isDone ? C.green + "44" : C.border,
|
|
349
|
+
marginTop: 4,
|
|
350
|
+
}} />
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
{/* Step card */}
|
|
355
|
+
<div style={{
|
|
356
|
+
flex: 1,
|
|
357
|
+
background: isActive ? stepColor + "11" : isDone ? C.green + "08" : C.surface,
|
|
358
|
+
border: `1px solid ${isActive ? stepColor + "66" : isDone ? C.green + "33" : C.border}`,
|
|
359
|
+
borderRadius: 10, padding: "12px 16px", marginBottom: 4,
|
|
360
|
+
transition: "all 0.3s",
|
|
361
|
+
opacity: currentStep === -1 ? 0.55 : isDone || isActive ? 1 : 0.4,
|
|
362
|
+
}}>
|
|
363
|
+
{/* Step header */}
|
|
364
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
365
|
+
<span style={{ fontSize: 16 }}>{getStepIcon(step)}</span>
|
|
366
|
+
{step.from && (
|
|
367
|
+
<>
|
|
368
|
+
<Tag color={getActorColor(step.from)}>{step.from}</Tag>
|
|
369
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>→</span>
|
|
370
|
+
</>
|
|
371
|
+
)}
|
|
372
|
+
{step.to && !Array.isArray(step.to) && (
|
|
373
|
+
<Tag color={getActorColor(step.to)}>{step.to}</Tag>
|
|
374
|
+
)}
|
|
375
|
+
{Array.isArray(step.to) && step.to.map(t => (
|
|
376
|
+
<Tag key={t} color={getActorColor(t)}>{t}</Tag>
|
|
377
|
+
))}
|
|
378
|
+
{step.label && (
|
|
379
|
+
<code style={{
|
|
380
|
+
background: C.bg, color: stepColor,
|
|
381
|
+
padding: "2px 8px", borderRadius: 4, fontSize: 12,
|
|
382
|
+
border: `1px solid ${C.border}`,
|
|
383
|
+
}}>
|
|
384
|
+
{step.label}
|
|
385
|
+
</code>
|
|
386
|
+
)}
|
|
387
|
+
{step.event && <Tag color={C.gold}>⚡ {step.event}</Tag>}
|
|
388
|
+
{step.topic && <Tag color={C.purple}>kafka:{step.topic}</Tag>}
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div style={{ color: C.textDim, fontSize: 12, marginTop: 6, lineHeight: 1.6 }}>
|
|
392
|
+
{step.desc}
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
{/* Sync sub-calls (shown when step is active) */}
|
|
396
|
+
{step.syncCalls && step.syncCalls.length > 0 && isActive && (
|
|
397
|
+
<div style={{ marginTop: 10, paddingTop: 10, borderTop: `1px dashed ${C.border}` }}>
|
|
398
|
+
<div style={{ color: C.textMuted, fontSize: 10, marginBottom: 6, fontWeight: 700, letterSpacing: 1 }}>
|
|
399
|
+
LLAMADAS SÍNCRONAS:
|
|
400
|
+
</div>
|
|
401
|
+
{step.syncCalls.map((sc, j) => (
|
|
402
|
+
<div key={j} style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
|
403
|
+
<span style={{ color: C.blue, fontSize: 11 }}>⟶</span>
|
|
404
|
+
<Tag color={getActorColor(sc.to)}>{sc.to}</Tag>
|
|
405
|
+
<code style={{
|
|
406
|
+
background: C.bg, color: C.blue,
|
|
407
|
+
padding: "1px 6px", borderRadius: 3, fontSize: 11,
|
|
408
|
+
}}>{sc.label}</code>
|
|
409
|
+
<span style={{ color: C.textMuted, fontSize: 10 }}>via {sc.port}</span>
|
|
410
|
+
</div>
|
|
411
|
+
))}
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
);
|
|
417
|
+
})}
|
|
418
|
+
</div>
|
|
419
|
+
|
|
420
|
+
{completed && (
|
|
421
|
+
<div style={{
|
|
422
|
+
background: C.green + "15", border: `1px solid ${C.green}44`,
|
|
423
|
+
borderRadius: 10, padding: 16, textAlign: "center", marginTop: 8,
|
|
424
|
+
animation: "fadeIn 0.3s",
|
|
425
|
+
}}>
|
|
426
|
+
<span style={{ fontSize: 24 }}>✅</span>
|
|
427
|
+
<div style={{ color: C.green, fontWeight: 700, marginTop: 4 }}>Flujo completado exitosamente</div>
|
|
428
|
+
<div style={{ color: C.textMuted, fontSize: 12, marginTop: 2 }}>
|
|
429
|
+
{flow.steps.length} pasos · {flow.label}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
</div>
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ─── ArchitectureTab ────────────────────────────────────────────────────
|
|
438
|
+
function ArchitectureTab() {
|
|
439
|
+
const [selected, setSelected] = useState(null);
|
|
440
|
+
|
|
441
|
+
const moduleInfo = (id) => ({
|
|
442
|
+
produces: EVENTS.filter(e => e.producer === id),
|
|
443
|
+
consumes: EVENTS.filter(e => (e.consumers || []).includes(id)),
|
|
444
|
+
callsSync: SYNC_INTEGRATIONS.filter(s => s.caller === id),
|
|
445
|
+
calledBy: SYNC_INTEGRATIONS.filter(s => s.calls === id),
|
|
446
|
+
endpoints: ENDPOINTS[id] || [],
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
return (
|
|
450
|
+
<div>
|
|
451
|
+
{/* Module grid */}
|
|
452
|
+
<div style={{ marginBottom: 20 }}>
|
|
453
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginBottom: 10, fontWeight: 700, letterSpacing: 1 }}>
|
|
454
|
+
MÓDULOS — clic para explorar dependencias
|
|
455
|
+
</div>
|
|
456
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 10 }}>
|
|
457
|
+
{MODULES_LIST.map(m => (
|
|
458
|
+
<ModuleCard
|
|
459
|
+
key={m.id}
|
|
460
|
+
id={m.id}
|
|
461
|
+
selected={selected === m.id}
|
|
462
|
+
onClick={() => setSelected(selected === m.id ? null : m.id)}
|
|
463
|
+
/>
|
|
464
|
+
))}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
|
|
468
|
+
{/* Selected module detail */}
|
|
469
|
+
{selected && (() => {
|
|
470
|
+
const mod = MODULES[selected];
|
|
471
|
+
const info = moduleInfo(selected);
|
|
472
|
+
if (!mod) return null;
|
|
473
|
+
return (
|
|
474
|
+
<div style={{
|
|
475
|
+
background: C.surface, border: `1px solid ${mod.color}44`,
|
|
476
|
+
borderRadius: 12, padding: 20, marginBottom: 20,
|
|
477
|
+
animation: "fadeIn 0.2s",
|
|
478
|
+
}}>
|
|
479
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
|
|
480
|
+
<span style={{ fontSize: 28 }}>{mod.icon}</span>
|
|
481
|
+
<div>
|
|
482
|
+
<div style={{ fontWeight: 800, color: mod.color, fontSize: 18 }}>{mod.label}</div>
|
|
483
|
+
<div style={{ color: C.textMuted, fontSize: 12 }}>{mod.desc}</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: 16 }}>
|
|
488
|
+
{[
|
|
489
|
+
{ title: "Produce eventos", items: info.produces, color: C.gold, icon: "📤", render: e => e.event },
|
|
490
|
+
{ title: "Consume eventos", items: info.consumes, color: C.blue, icon: "📥", render: e => e.event },
|
|
491
|
+
{ title: "Llama síncronamente", items: info.callsSync, color: C.purple, icon: "⟶", render: s => `→ ${s.calls} (${s.port})` },
|
|
492
|
+
{ title: "Es llamado por", items: info.calledBy, color: C.green, icon: "⟵", render: s => `← ${s.caller} (${s.port})` },
|
|
493
|
+
{ title: "Endpoints expuestos", items: info.endpoints, color: mod.color, icon: "🔌", render: e => e },
|
|
494
|
+
].filter(sec => sec.items.length > 0).map(section => (
|
|
495
|
+
<div key={section.title}>
|
|
496
|
+
<div style={{ color: section.color, fontSize: 10, fontWeight: 700, marginBottom: 8, letterSpacing: 0.5 }}>
|
|
497
|
+
{section.icon} {section.title.toUpperCase()}
|
|
498
|
+
</div>
|
|
499
|
+
{section.items.map((item, i) => (
|
|
500
|
+
<div key={i} style={{
|
|
501
|
+
color: C.textDim, fontSize: 12, padding: "5px 0",
|
|
502
|
+
borderBottom: `1px solid ${C.border}`, fontFamily: "monospace",
|
|
503
|
+
}}>
|
|
504
|
+
{section.render(item)}
|
|
505
|
+
</div>
|
|
506
|
+
))}
|
|
507
|
+
</div>
|
|
508
|
+
))}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
);
|
|
512
|
+
})()}
|
|
513
|
+
|
|
514
|
+
{/* Sync integrations graph */}
|
|
515
|
+
{SYNC_INTEGRATIONS.length > 0 && (
|
|
516
|
+
<div style={{ marginBottom: 24 }}>
|
|
517
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginBottom: 10, fontWeight: 700, letterSpacing: 1 }}>
|
|
518
|
+
DEPENDENCIAS SÍNCRONAS ({SYNC_INTEGRATIONS.length} puertos)
|
|
519
|
+
</div>
|
|
520
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))", gap: 10 }}>
|
|
521
|
+
{SYNC_INTEGRATIONS.map((s, i) => {
|
|
522
|
+
const callerMod = MODULES[s.caller];
|
|
523
|
+
const calleeMod = MODULES[s.calls];
|
|
524
|
+
return (
|
|
525
|
+
<div key={i} style={{
|
|
526
|
+
background: C.surface, border: `1px solid ${C.border}`,
|
|
527
|
+
borderRadius: 10, padding: "12px 16px",
|
|
528
|
+
}}>
|
|
529
|
+
<div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 6, marginBottom: 8 }}>
|
|
530
|
+
<Tag color={callerMod?.color || C.textMuted}>{s.caller}</Tag>
|
|
531
|
+
<span style={{ color: C.textMuted, fontSize: 14, fontWeight: 700 }}>→</span>
|
|
532
|
+
<Tag color={calleeMod?.color || C.textMuted}>{s.calls}</Tag>
|
|
533
|
+
<Tag color={C.purple}>{s.port}</Tag>
|
|
534
|
+
</div>
|
|
535
|
+
<div style={{ marginLeft: 4 }}>
|
|
536
|
+
{(s.endpoints || []).map((ep, j) => (
|
|
537
|
+
<div key={j} style={{ color: C.textDim, fontSize: 11, fontFamily: "monospace", padding: "2px 0" }}>
|
|
538
|
+
· {ep}
|
|
539
|
+
</div>
|
|
540
|
+
))}
|
|
541
|
+
</div>
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
})}
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
)}
|
|
548
|
+
|
|
549
|
+
{/* Kafka topics */}
|
|
550
|
+
<div>
|
|
551
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginBottom: 10, fontWeight: 700, letterSpacing: 1 }}>
|
|
552
|
+
KAFKA TOPICS ({EVENTS.length} eventos)
|
|
553
|
+
</div>
|
|
554
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: 8 }}>
|
|
555
|
+
{EVENTS.map(e => (
|
|
556
|
+
<div key={e.event} style={{
|
|
557
|
+
background: C.surface, border: `1px solid ${C.border}`,
|
|
558
|
+
borderRadius: 8, padding: "10px 14px",
|
|
559
|
+
}}>
|
|
560
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 6 }}>
|
|
561
|
+
<Tag color={C.gold}>{e.topic}</Tag>
|
|
562
|
+
</div>
|
|
563
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" }}>
|
|
564
|
+
<Tag color={MODULES[e.producer]?.color || C.textMuted}>{e.producer}</Tag>
|
|
565
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>→</span>
|
|
566
|
+
{(e.consumers || []).map(c => (
|
|
567
|
+
<Tag key={c} color={MODULES[c]?.color || C.textMuted}>{c}</Tag>
|
|
568
|
+
))}
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
))}
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</div>
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ─── DiagramTab ──────────────────────────────────────────────────────────
|
|
579
|
+
function DiagramTab() {
|
|
580
|
+
const containerRef = useRef(null);
|
|
581
|
+
const networkRef = useRef(null);
|
|
582
|
+
const edgesDataRef = useRef(null);
|
|
583
|
+
const edgeGroupMapRef = useRef({});
|
|
584
|
+
const eventEdgesMapRef = useRef({});
|
|
585
|
+
const edgeLabelMapRef = useRef({});
|
|
586
|
+
const [physicsOn, setPhysicsOn] = useState(true);
|
|
587
|
+
const [hoveredNode, setHoveredNode] = useState(null);
|
|
588
|
+
const [hoveredEvent, setHoveredEvent] = useState(null); // { name, producer, consumers[] }
|
|
589
|
+
|
|
590
|
+
// Build vis datasets from injected data
|
|
591
|
+
function buildDatasets() {
|
|
592
|
+
const visNodes = MODULES_LIST.map(mod => ({
|
|
593
|
+
id: mod.id,
|
|
594
|
+
label: mod.icon + "\n" + mod.label,
|
|
595
|
+
title: mod.desc,
|
|
596
|
+
color: {
|
|
597
|
+
background: mod.color + "22",
|
|
598
|
+
border: mod.color,
|
|
599
|
+
highlight: { background: mod.color + "44", border: mod.color },
|
|
600
|
+
hover: { background: mod.color + "33", border: mod.color },
|
|
601
|
+
},
|
|
602
|
+
font: { color: C.text, size: 13, face: "'Plus Jakarta Sans', sans-serif", multi: false },
|
|
603
|
+
shape: "box",
|
|
604
|
+
borderWidth: 2,
|
|
605
|
+
borderWidthSelected: 3,
|
|
606
|
+
margin: 10,
|
|
607
|
+
}));
|
|
608
|
+
|
|
609
|
+
const visEdges = [];
|
|
610
|
+
|
|
611
|
+
// Sync edges — solid blue
|
|
612
|
+
SYNC_INTEGRATIONS.forEach((s, i) => {
|
|
613
|
+
visEdges.push({
|
|
614
|
+
id: "sync-" + i,
|
|
615
|
+
from: s.caller,
|
|
616
|
+
to: s.calls,
|
|
617
|
+
label: s.port,
|
|
618
|
+
dashes: false,
|
|
619
|
+
color: { color: C.blue, highlight: C.blue, hover: C.blue },
|
|
620
|
+
font: { color: C.blue, size: 10, face: "'JetBrains Mono', monospace",
|
|
621
|
+
background: C.bg, strokeWidth: 0, align: "middle" },
|
|
622
|
+
arrows: { to: { enabled: true, scaleFactor: 0.7 } },
|
|
623
|
+
width: 2,
|
|
624
|
+
smooth: { enabled: true, type: "curvedCW", roundness: 0.15 },
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
// Broker node — shown only if there are async events
|
|
629
|
+
if (EVENTS.length > 0) {
|
|
630
|
+
const brokerLabel = ((window.__EVA_DATA__.brokerName || "Kafka") + "\nBroker");
|
|
631
|
+
visNodes.push({
|
|
632
|
+
id: "__broker__",
|
|
633
|
+
label: "⚡ " + brokerLabel,
|
|
634
|
+
title: "Message broker — retransmite eventos asíncronos entre módulos",
|
|
635
|
+
color: {
|
|
636
|
+
background: C.gold, border: "#c49a00",
|
|
637
|
+
highlight: { background: "#ffd84d", border: "#c49a00" },
|
|
638
|
+
hover: { background: "#ffd84d", border: "#c49a00" },
|
|
639
|
+
},
|
|
640
|
+
font: { color: "#0d1f3c", size: 13, face: "'Plus Jakarta Sans', sans-serif", bold: true },
|
|
641
|
+
shape: "box", borderWidth: 2, borderWidthSelected: 3, margin: 12,
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Async edges — routed through broker + group maps for hover highlighting
|
|
646
|
+
const edgeGroupMap = {}; // edgeId → eventIndex
|
|
647
|
+
const eventEdgesMap = {}; // eventIndex → [edgeIds]
|
|
648
|
+
const edgeLabelMap = {}; // edgeId → original label
|
|
649
|
+
EVENTS.forEach((ev, i) => {
|
|
650
|
+
const shortLabel = ev.event.replace(/Event$/, "");
|
|
651
|
+
eventEdgesMap[i] = [];
|
|
652
|
+
// producer → broker
|
|
653
|
+
const pubId = "async-pub-" + i;
|
|
654
|
+
edgeGroupMap[pubId] = i;
|
|
655
|
+
eventEdgesMap[i].push(pubId);
|
|
656
|
+
edgeLabelMap[pubId] = shortLabel;
|
|
657
|
+
visEdges.push({
|
|
658
|
+
id: pubId,
|
|
659
|
+
from: ev.producer,
|
|
660
|
+
to: "__broker__",
|
|
661
|
+
label: shortLabel,
|
|
662
|
+
dashes: [6, 4],
|
|
663
|
+
color: { color: C.gold + "cc", highlight: C.gold, hover: C.gold },
|
|
664
|
+
font: { color: C.gold, size: 10, face: "'JetBrains Mono', monospace",
|
|
665
|
+
background: C.bg, strokeWidth: 0, align: "middle" },
|
|
666
|
+
arrows: { to: { enabled: true, scaleFactor: 0.6 } },
|
|
667
|
+
width: 1.5,
|
|
668
|
+
smooth: { enabled: true, type: "dynamic" },
|
|
669
|
+
});
|
|
670
|
+
// broker → each consumer
|
|
671
|
+
(ev.consumers || []).forEach((consumer, j) => {
|
|
672
|
+
const subId = "async-sub-" + i + "-" + j;
|
|
673
|
+
edgeGroupMap[subId] = i;
|
|
674
|
+
eventEdgesMap[i].push(subId);
|
|
675
|
+
edgeLabelMap[subId] = "";
|
|
676
|
+
visEdges.push({
|
|
677
|
+
id: subId,
|
|
678
|
+
from: "__broker__",
|
|
679
|
+
to: consumer,
|
|
680
|
+
label: "",
|
|
681
|
+
dashes: [4, 4],
|
|
682
|
+
color: { color: C.gold + "77", highlight: C.gold, hover: C.gold },
|
|
683
|
+
font: { color: "rgba(0,0,0,0)", strokeWidth: 0 },
|
|
684
|
+
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
685
|
+
width: 1.5,
|
|
686
|
+
smooth: { enabled: true, type: "dynamic" },
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
return { visNodes, visEdges, edgeGroupMap, eventEdgesMap, edgeLabelMap };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function initNetwork(phys) {
|
|
695
|
+
if (!containerRef.current) return;
|
|
696
|
+
if (networkRef.current) { networkRef.current.destroy(); }
|
|
697
|
+
|
|
698
|
+
const { visNodes, visEdges, edgeGroupMap, eventEdgesMap, edgeLabelMap } = buildDatasets();
|
|
699
|
+
edgeGroupMapRef.current = edgeGroupMap;
|
|
700
|
+
eventEdgesMapRef.current = eventEdgesMap;
|
|
701
|
+
edgeLabelMapRef.current = edgeLabelMap;
|
|
702
|
+
const edgesDS = new vis.DataSet(visEdges);
|
|
703
|
+
edgesDataRef.current = edgesDS;
|
|
704
|
+
const data = {
|
|
705
|
+
nodes: new vis.DataSet(visNodes),
|
|
706
|
+
edges: edgesDS,
|
|
707
|
+
};
|
|
708
|
+
const options = {
|
|
709
|
+
physics: {
|
|
710
|
+
enabled: phys,
|
|
711
|
+
solver: "forceAtlas2Based",
|
|
712
|
+
forceAtlas2Based: { gravitationalConstant: -60, springLength: 160, springConstant: 0.05, damping: 0.5 },
|
|
713
|
+
stabilization: { iterations: 400, updateInterval: 25 },
|
|
714
|
+
},
|
|
715
|
+
interaction: { dragNodes: true, zoomView: true, hover: true, tooltipDelay: 150 },
|
|
716
|
+
edges: { selectionWidth: 2 },
|
|
717
|
+
nodes: { chosen: true },
|
|
718
|
+
layout: { randomSeed: 42 },
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
const net = new vis.Network(containerRef.current, data, options);
|
|
722
|
+
net.once("stabilizationIterationsDone", () => {
|
|
723
|
+
net.setOptions({ physics: { enabled: false } });
|
|
724
|
+
setPhysicsOn(false);
|
|
725
|
+
});
|
|
726
|
+
net.on("hoverNode", (p) => setHoveredNode(p.node));
|
|
727
|
+
net.on("blurNode", () => setHoveredNode(null));
|
|
728
|
+
net.on("hoverEdge", (p) => {
|
|
729
|
+
const groupIdx = edgeGroupMapRef.current[p.edge];
|
|
730
|
+
if (groupIdx === undefined || !edgesDataRef.current) return;
|
|
731
|
+
const groupIds = eventEdgesMapRef.current[groupIdx] || [];
|
|
732
|
+
const allIds = Object.keys(edgeGroupMapRef.current);
|
|
733
|
+
const RED = "#ff6b6b";
|
|
734
|
+
const GREEN = "#4ade80";
|
|
735
|
+
const FONT_PUB = { color: C.gold, size: 10, face: "'JetBrains Mono', monospace", background: C.bg, strokeWidth: 0, align: "middle" };
|
|
736
|
+
const FONT_HID = { color: "rgba(0,0,0,0)", background: "rgba(0,0,0,0)", strokeWidth: 0 };
|
|
737
|
+
edgesDataRef.current.update(allIds.map(id => {
|
|
738
|
+
const inGroup = groupIds.includes(id);
|
|
739
|
+
const isPub = id.startsWith("async-pub-");
|
|
740
|
+
return inGroup
|
|
741
|
+
? { id,
|
|
742
|
+
color: { color: RED, highlight: RED, hover: RED },
|
|
743
|
+
font: isPub ? { ...FONT_PUB, color: GREEN, background: "rgba(0,0,0,0)", vadjust: -14 } : FONT_HID,
|
|
744
|
+
label: edgeLabelMapRef.current[id],
|
|
745
|
+
width: 1.5 }
|
|
746
|
+
: { id,
|
|
747
|
+
color: { color: C.gold + "18", highlight: C.gold + "22", hover: C.gold + "22" },
|
|
748
|
+
font: FONT_HID,
|
|
749
|
+
label: "",
|
|
750
|
+
width: 1.5 };
|
|
751
|
+
}));
|
|
752
|
+
// update event tooltip
|
|
753
|
+
const ev = EVENTS[groupIdx];
|
|
754
|
+
if (ev) setHoveredEvent({ name: ev.event, producer: ev.producer, consumers: ev.consumers || [] });
|
|
755
|
+
});
|
|
756
|
+
net.on("blurEdge", () => {
|
|
757
|
+
setHoveredEvent(null);
|
|
758
|
+
if (!edgesDataRef.current) return;
|
|
759
|
+
const allIds = Object.keys(edgeGroupMapRef.current);
|
|
760
|
+
const FONT_PUB = { color: C.gold, size: 10, face: "'JetBrains Mono', monospace", background: C.bg, strokeWidth: 0, align: "middle" };
|
|
761
|
+
const FONT_SUB = { color: "rgba(0,0,0,0)", strokeWidth: 0 };
|
|
762
|
+
edgesDataRef.current.update(allIds.map(id => {
|
|
763
|
+
const isPub = id.startsWith("async-pub-");
|
|
764
|
+
return {
|
|
765
|
+
id,
|
|
766
|
+
color: isPub
|
|
767
|
+
? { color: C.gold + "cc", highlight: C.gold, hover: C.gold }
|
|
768
|
+
: { color: C.gold + "77", highlight: C.gold, hover: C.gold },
|
|
769
|
+
font: isPub ? FONT_PUB : FONT_SUB,
|
|
770
|
+
label: edgeLabelMapRef.current[id],
|
|
771
|
+
width: 1.5,
|
|
772
|
+
};
|
|
773
|
+
}));
|
|
774
|
+
});
|
|
775
|
+
networkRef.current = net;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
useEffect(() => {
|
|
779
|
+
initNetwork(true);
|
|
780
|
+
return () => { if (networkRef.current) networkRef.current.destroy(); };
|
|
781
|
+
}, []);
|
|
782
|
+
|
|
783
|
+
const togglePhysics = () => {
|
|
784
|
+
const next = !physicsOn;
|
|
785
|
+
setPhysicsOn(next);
|
|
786
|
+
if (networkRef.current) networkRef.current.setOptions({ physics: { enabled: next } });
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
const fitView = () => networkRef.current && networkRef.current.fit({ animation: { duration: 400, easingFunction: "easeInOutQuad" } });
|
|
790
|
+
const resetNet = () => initNetwork(true);
|
|
791
|
+
|
|
792
|
+
const hoveredMod = (hoveredNode && hoveredNode !== "__broker__") ? MODULES[hoveredNode] : null;
|
|
793
|
+
const hoveredBroker = hoveredNode === "__broker__";
|
|
794
|
+
// overlay priority: hoveredEvent > node tooltips
|
|
795
|
+
const showOverlay = hoveredEvent || hoveredMod || hoveredBroker;
|
|
796
|
+
|
|
797
|
+
return (
|
|
798
|
+
<div>
|
|
799
|
+
{/* Toolbar */}
|
|
800
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14, flexWrap: "wrap" }}>
|
|
801
|
+
<button onClick={togglePhysics} style={{
|
|
802
|
+
background: physicsOn ? C.blue + "22" : C.surface,
|
|
803
|
+
border: `1px solid ${physicsOn ? C.blue : C.border}`,
|
|
804
|
+
color: physicsOn ? C.blue : C.textMuted,
|
|
805
|
+
borderRadius: 8, padding: "7px 14px", cursor: "pointer",
|
|
806
|
+
fontFamily: "inherit", fontSize: 12, fontWeight: 600,
|
|
807
|
+
transition: "all 0.15s",
|
|
808
|
+
}}>
|
|
809
|
+
{physicsOn ? "⏸ Detener física" : "▶ Activar física"}
|
|
810
|
+
</button>
|
|
811
|
+
<button onClick={fitView} style={{
|
|
812
|
+
background: C.surface, border: `1px solid ${C.border}`,
|
|
813
|
+
color: C.textMuted, borderRadius: 8, padding: "7px 14px",
|
|
814
|
+
cursor: "pointer", fontFamily: "inherit", fontSize: 12,
|
|
815
|
+
}}>⊞ Ajustar vista</button>
|
|
816
|
+
<button onClick={resetNet} style={{
|
|
817
|
+
background: C.surface, border: `1px solid ${C.border}`,
|
|
818
|
+
color: C.textMuted, borderRadius: 8, padding: "7px 14px",
|
|
819
|
+
cursor: "pointer", fontFamily: "inherit", fontSize: 12,
|
|
820
|
+
}}>⟳ Reiniciar</button>
|
|
821
|
+
<div style={{ flex: 1 }} />
|
|
822
|
+
</div>
|
|
823
|
+
|
|
824
|
+
{/* Canvas */}
|
|
825
|
+
<div style={{
|
|
826
|
+
position: "relative",
|
|
827
|
+
background: C.surface, border: `1px solid ${C.border}`,
|
|
828
|
+
borderRadius: 12, overflow: "hidden",
|
|
829
|
+
}}>
|
|
830
|
+
<div ref={containerRef} style={{ width: "100%", height: 520 }} />
|
|
831
|
+
{showOverlay && (
|
|
832
|
+
<div style={{
|
|
833
|
+
position: "absolute", bottom: 12, left: 12,
|
|
834
|
+
background: C.bg + "ee",
|
|
835
|
+
border: `1px solid ${
|
|
836
|
+
hoveredEvent ? "#4ade80"
|
|
837
|
+
: hoveredBroker ? C.gold
|
|
838
|
+
: hoveredMod.color}66`,
|
|
839
|
+
borderRadius: 8, padding: "6px 12px", backdropFilter: "blur(4px)",
|
|
840
|
+
fontSize: 12, fontWeight: 600,
|
|
841
|
+
animation: "fadeIn 0.15s", pointerEvents: "none",
|
|
842
|
+
maxWidth: "80%",
|
|
843
|
+
}}>
|
|
844
|
+
{hoveredEvent
|
|
845
|
+
? <span style={{ color: "#4ade80" }}>
|
|
846
|
+
⬡ {hoveredEvent.name}
|
|
847
|
+
<span style={{ color: C.textMuted, fontWeight: 400 }}> — </span>
|
|
848
|
+
<span style={{ color: C.textDim, fontWeight: 400 }}>
|
|
849
|
+
{hoveredEvent.producer} → [{hoveredEvent.consumers.join(", ")}]
|
|
850
|
+
</span>
|
|
851
|
+
</span>
|
|
852
|
+
: hoveredBroker
|
|
853
|
+
? <span style={{ color: C.gold }}>⚡ Kafka Broker — <span style={{ color: C.textDim, fontWeight: 400 }}>Retransmite eventos asíncronos entre módulos</span></span>
|
|
854
|
+
: <span style={{ color: hoveredMod.color }}>{hoveredMod.icon} {hoveredMod.label} — <span style={{ color: C.textDim, fontWeight: 400 }}>{hoveredMod.desc}</span></span>
|
|
855
|
+
}
|
|
856
|
+
</div>
|
|
857
|
+
)}
|
|
858
|
+
</div>
|
|
859
|
+
|
|
860
|
+
{/* Legend */}
|
|
861
|
+
<div style={{
|
|
862
|
+
display: "flex", gap: 24, marginTop: 14, flexWrap: "wrap",
|
|
863
|
+
alignItems: "center", paddingLeft: 4,
|
|
864
|
+
}}>
|
|
865
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
866
|
+
<svg width="36" height="12">
|
|
867
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
868
|
+
stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
|
|
869
|
+
<polygon points="32,3 36,6 32,9" fill={C.blue} />
|
|
870
|
+
</svg>
|
|
871
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>Síncrono (HTTP)</span>
|
|
872
|
+
</div>
|
|
873
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
874
|
+
<svg width="36" height="12">
|
|
875
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
876
|
+
stroke={C.gold} strokeWidth="1.5" strokeDasharray="6 4" strokeLinecap="round" />
|
|
877
|
+
<polygon points="32,3 36,6 32,9" fill={C.gold} />
|
|
878
|
+
</svg>
|
|
879
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>Asíncrono (Kafka)</span>
|
|
880
|
+
</div>
|
|
881
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
|
|
882
|
+
{MODULES_LIST.length} módulos · {SYNC_INTEGRATIONS.length} puertos sync · {EVENTS.length} eventos
|
|
883
|
+
</div>
|
|
884
|
+
</div>
|
|
885
|
+
</div>
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// ─── App root ───────────────────────────────────────────────────────────
|
|
890
|
+
function App() {
|
|
891
|
+
const [tab, setTab] = useState("validation");
|
|
892
|
+
|
|
893
|
+
const tabs = [
|
|
894
|
+
{ id: "validation", label: "Validación", icon: "🔍" },
|
|
895
|
+
{ id: "flows", label: "Simulador de flujos", icon: "▶" },
|
|
896
|
+
{ id: "architecture", label: "Arquitectura", icon: "🗺️" },
|
|
897
|
+
{ id: "diagram", label: "Diagrama", icon: "◈" },
|
|
898
|
+
];
|
|
899
|
+
|
|
900
|
+
const sys = window.__EVA_DATA__;
|
|
901
|
+
const tech = [];
|
|
902
|
+
// Detect tech from systemConfig embedded in data
|
|
903
|
+
if (sys.events && sys.events.length > 0) tech.push({ label: "Kafka", color: C.gold });
|
|
904
|
+
|
|
905
|
+
return (
|
|
906
|
+
<div style={{ background: C.bg, minHeight: "100vh", color: C.text, fontFamily: "'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" }}>
|
|
907
|
+
|
|
908
|
+
{/* Header */}
|
|
909
|
+
<div style={{ borderBottom: `1px solid ${C.border}`, padding: "0 24px" }}>
|
|
910
|
+
<div style={{ maxWidth: 1100, margin: "0 auto", display: "flex", alignItems: "center", gap: 16, height: 64 }}>
|
|
911
|
+
<div>
|
|
912
|
+
<span style={{ fontWeight: 900, fontSize: 18, color: C.accent, letterSpacing: -0.5 }}>eva4j</span>
|
|
913
|
+
<span style={{ fontWeight: 400, fontSize: 14, color: C.textMuted, marginLeft: 8 }}>/ architecture validator</span>
|
|
914
|
+
</div>
|
|
915
|
+
<div style={{ height: 20, width: 1, background: C.border }} />
|
|
916
|
+
<div style={{ fontWeight: 700, fontSize: 16, color: C.text }}>{systemName}</div>
|
|
917
|
+
<div style={{ flex: 1 }} />
|
|
918
|
+
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
919
|
+
{sys.events && sys.events.length > 0 && (
|
|
920
|
+
<Tag color={C.gold}>Kafka · {sys.events.length} events</Tag>
|
|
921
|
+
)}
|
|
922
|
+
{sys.syncIntegrations && sys.syncIntegrations.length > 0 && (
|
|
923
|
+
<Tag color={C.blue}>Sync · {sys.syncIntegrations.length} ports</Tag>
|
|
924
|
+
)}
|
|
925
|
+
<Tag color={C.purple}>{sys.modules.length} modules</Tag>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
</div>
|
|
929
|
+
|
|
930
|
+
{/* Tabs */}
|
|
931
|
+
<div style={{ borderBottom: `1px solid ${C.border}`, padding: "0 24px" }}>
|
|
932
|
+
<div style={{ maxWidth: 1100, margin: "0 auto", display: "flex", gap: 4 }}>
|
|
933
|
+
{tabs.map(t => (
|
|
934
|
+
<button key={t.id} onClick={() => setTab(t.id)} style={{
|
|
935
|
+
background: "transparent", border: "none",
|
|
936
|
+
color: tab === t.id ? C.text : C.textMuted,
|
|
937
|
+
padding: "16px 20px", cursor: "pointer",
|
|
938
|
+
fontWeight: tab === t.id ? 700 : 400,
|
|
939
|
+
borderBottom: `2px solid ${tab === t.id ? C.accent : "transparent"}`,
|
|
940
|
+
fontSize: 13, transition: "all 0.15s", fontFamily: "inherit",
|
|
941
|
+
display: "flex", alignItems: "center", gap: 8,
|
|
942
|
+
}}>
|
|
943
|
+
<span>{t.icon}</span> {t.label}
|
|
944
|
+
</button>
|
|
945
|
+
))}
|
|
946
|
+
<div style={{ flex: 1 }} />
|
|
947
|
+
<div style={{ display: "flex", alignItems: "center" }}>
|
|
948
|
+
<span style={{ color: C.textMuted, fontSize: 10 }}>
|
|
949
|
+
generated {new Date(generatedAt).toLocaleString()}
|
|
950
|
+
</span>
|
|
951
|
+
</div>
|
|
952
|
+
</div>
|
|
953
|
+
</div>
|
|
954
|
+
|
|
955
|
+
{/* Tab content */}
|
|
956
|
+
<div style={{ maxWidth: 1100, margin: "0 auto", padding: "28px 24px" }}>
|
|
957
|
+
{tab === "validation" && <ValidationTab />}
|
|
958
|
+
{tab === "flows" && <FlowSimulator />}
|
|
959
|
+
{tab === "architecture" && <ArchitectureTab />}
|
|
960
|
+
{tab === "diagram" && <DiagramTab />}
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Mount
|
|
967
|
+
const root = ReactDOM.createRoot(document.getElementById("root"));
|
|
968
|
+
root.render(React.createElement(App, null));
|
|
969
|
+
</script>
|
|
970
|
+
</body>
|
|
971
|
+
</html>
|