eva4j 1.0.16 β 1.0.18
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 +220 -5
- package/DOMAIN_YAML_GUIDE.md +188 -3
- package/FUTURE_FEATURES.md +33 -52
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +290 -10
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-events.yaml +26 -0
- package/examples/domain-read-models.yaml +113 -0
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/read-model-spec.md +664 -0
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +34 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +560 -24
- package/src/commands/generate-http-exchange.js +3 -0
- package/src/commands/generate-kafka-event.js +3 -0
- package/src/commands/generate-kafka-listener.js +3 -0
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-record.js +2 -2
- package/src/commands/generate-resource.js +4 -1
- package/src/commands/generate-temporal-activity.js +970 -33
- package/src/commands/generate-temporal-flow.js +98 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/commands/generate-usecase.js +4 -1
- package/src/skills/build-system-yaml/SKILL.md +343 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +253 -26
- package/src/skills/build-system-yaml/references/module-spec.md +90 -9
- package/src/skills/build-system-yaml/references/system-yaml-spec.md +36 -0
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/config-manager.js +4 -2
- package/src/utils/domain-validator.js +495 -17
- package/src/utils/naming.js +20 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/validator.js +3 -1
- package/src/utils/yaml-to-entity.js +281 -9
- package/templates/aggregate/AggregateRepository.java.ejs +4 -0
- package/templates/aggregate/AggregateRepositoryImpl.java.ejs +8 -0
- package/templates/aggregate/AggregateRoot.java.ejs +38 -4
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +4 -4
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/DeleteCommandHandler.java.ejs +19 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/crud/UpdateCommandHandler.java.ejs +53 -2
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelDomain.java.ejs +46 -0
- package/templates/read-model/ReadModelJpa.java.ejs +58 -0
- package/templates/read-model/ReadModelJpaRepository.java.ejs +13 -0
- package/templates/read-model/ReadModelKafkaListener.java.ejs +64 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepository.java.ejs +42 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +85 -0
- package/templates/read-model/ReadModelSyncHandler.java.ejs +54 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
|
@@ -48,6 +48,16 @@
|
|
|
48
48
|
validation: VALIDATION,
|
|
49
49
|
domainValidation: DOMAIN_VALIDATION,
|
|
50
50
|
generatedAt,
|
|
51
|
+
isTemporalMode: IS_TEMPORAL_MODE,
|
|
52
|
+
orchestration: TEMPORAL_ORCHESTRATION,
|
|
53
|
+
workflows: TEMPORAL_WORKFLOWS,
|
|
54
|
+
localWorkflows: LOCAL_WORKFLOWS,
|
|
55
|
+
sagaWorkflows: SAGA_WORKFLOWS,
|
|
56
|
+
activityCatalog: ACTIVITY_CATALOG,
|
|
57
|
+
moduleRoles: MODULE_ROLES,
|
|
58
|
+
queueTopology: QUEUE_TOPOLOGY,
|
|
59
|
+
externalTypeDeps: EXTERNAL_TYPE_DEPS,
|
|
60
|
+
temporalValidation: TEMPORAL_VALIDATION,
|
|
51
61
|
} = window.__EVA_DATA__;
|
|
52
62
|
|
|
53
63
|
// Convert arrays to maps for fast lookup
|
|
@@ -583,13 +593,15 @@
|
|
|
583
593
|
function DiagramTab() {
|
|
584
594
|
const containerRef = useRef(null);
|
|
585
595
|
const networkRef = useRef(null);
|
|
586
|
-
const edgesDataRef
|
|
587
|
-
const edgeGroupMapRef
|
|
588
|
-
const eventEdgesMapRef
|
|
589
|
-
const edgeLabelMapRef
|
|
590
|
-
const [
|
|
591
|
-
const [
|
|
592
|
-
const [
|
|
596
|
+
const edgesDataRef = useRef(null);
|
|
597
|
+
const edgeGroupMapRef = useRef({});
|
|
598
|
+
const eventEdgesMapRef = useRef({});
|
|
599
|
+
const edgeLabelMapRef = useRef({});
|
|
600
|
+
const temporalEdgeActivitiesRef = useRef({}); // edgeId β { activities[], color, wfLabel }
|
|
601
|
+
const [physicsOn, setPhysicsOn] = useState(true);
|
|
602
|
+
const [hoveredNode, setHoveredNode] = useState(null);
|
|
603
|
+
const [hoveredEvent, setHoveredEvent] = useState(null); // { name, producer, consumers[] }
|
|
604
|
+
const [hoveredTemporalEdge, setHoveredTemporalEdge] = useState(null); // { activities[], color, wfLabel }
|
|
593
605
|
|
|
594
606
|
// Build vis datasets from injected data
|
|
595
607
|
function buildDatasets() {
|
|
@@ -629,6 +641,120 @@
|
|
|
629
641
|
});
|
|
630
642
|
});
|
|
631
643
|
|
|
644
|
+
// Temporal workflow edges β central Temporal Server hub (mirrors Kafka broker pattern)
|
|
645
|
+
if (IS_TEMPORAL_MODE && Array.isArray(TEMPORAL_WORKFLOWS)) {
|
|
646
|
+
// Build camelCase β vis node id lookup (handles hyphenated names like "shopping-carts")
|
|
647
|
+
const camelToNodeId = {};
|
|
648
|
+
MODULES_LIST.forEach(m => {
|
|
649
|
+
camelToNodeId[m.id] = m.id;
|
|
650
|
+
const cc = m.id
|
|
651
|
+
.replace(/[-_\s]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''))
|
|
652
|
+
.replace(/^(.)/, c => c.toLowerCase());
|
|
653
|
+
camelToNodeId[cc] = m.id;
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Central Temporal Server node
|
|
657
|
+
const temporalTarget = (TEMPORAL_ORCHESTRATION && TEMPORAL_ORCHESTRATION.target) || "localhost:7233";
|
|
658
|
+
const temporalNs = (TEMPORAL_ORCHESTRATION && TEMPORAL_ORCHESTRATION.namespace) || "default";
|
|
659
|
+
visNodes.push({
|
|
660
|
+
id: "__temporal__",
|
|
661
|
+
label: "β±οΈ Temporal\n" + temporalNs,
|
|
662
|
+
title: `Temporal Server: ${temporalTarget} β orquesta todos los workflows`,
|
|
663
|
+
color: {
|
|
664
|
+
background: "#3b1f6e",
|
|
665
|
+
border: C.purple,
|
|
666
|
+
highlight: { background: "#5a2fa8", border: C.purple },
|
|
667
|
+
hover: { background: "#4a2890", border: C.purple },
|
|
668
|
+
},
|
|
669
|
+
font: { color: "#e8e8f0", size: 13, face: "'Plus Jakarta Sans', sans-serif", bold: true },
|
|
670
|
+
shape: "ellipse", borderWidth: 3, borderWidthSelected: 4, margin: 14,
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const WF_COLORS = [C.blue, C.orange, C.green, C.accent, C.gold, "#c084fc"];
|
|
674
|
+
const temporalEdgeActivities = {}; // populated below, stored in ref after build
|
|
675
|
+
|
|
676
|
+
TEMPORAL_WORKFLOWS.forEach((wf, wfIdx) => {
|
|
677
|
+
const orchId = camelToNodeId[wf.triggerModule];
|
|
678
|
+
if (!orchId) return;
|
|
679
|
+
const color = WF_COLORS[wfIdx % WF_COLORS.length];
|
|
680
|
+
const shortLabel = (wf.name || "").replace(/Workflow$/, "");
|
|
681
|
+
|
|
682
|
+
// Fix 2: one trigger edge PER WORKFLOW (not per unique orchestrator module)
|
|
683
|
+
// Uses workflow color + shortLabel so both Checkout and CancelOrder are distinct
|
|
684
|
+
visEdges.push({
|
|
685
|
+
id: `temporal-trigger-${wfIdx}`,
|
|
686
|
+
from: orchId,
|
|
687
|
+
to: "__temporal__",
|
|
688
|
+
label: shortLabel,
|
|
689
|
+
dashes: [5, 3],
|
|
690
|
+
color: { color: color + "cc", highlight: color, hover: color },
|
|
691
|
+
font: { color, size: 9, face: "'JetBrains Mono', monospace",
|
|
692
|
+
background: C.bg, strokeWidth: 0, align: "middle" },
|
|
693
|
+
arrows: { to: { enabled: true, scaleFactor: 0.6 } },
|
|
694
|
+
width: 1.5,
|
|
695
|
+
smooth: { enabled: true, type: "dynamic" },
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Mejora 4: determine dashes per target (all-async β dashed, else solid)
|
|
699
|
+
// Fix 1: removed `|| tgtId === orchId` β allow self-calls (e.g. TemporalβShoppingCarts)
|
|
700
|
+
// Build per-target activity list for Mejora 3 (tooltip)
|
|
701
|
+
const wfTargetActivities = {}; // tgtId β [{activity, roleClass, type, compensation}]
|
|
702
|
+
(wf.steps || []).forEach((step) => {
|
|
703
|
+
if (step._isWait || !step.target) return;
|
|
704
|
+
const tgtId = camelToNodeId[step.target];
|
|
705
|
+
if (!tgtId) return;
|
|
706
|
+
if (!wfTargetActivities[tgtId]) wfTargetActivities[tgtId] = [];
|
|
707
|
+
wfTargetActivities[tgtId].push({
|
|
708
|
+
activity: step.activity || step.activityPascal || "",
|
|
709
|
+
roleClass: step.roleClass || "",
|
|
710
|
+
type: step.type || "sync",
|
|
711
|
+
compensation: step.compensation || null,
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Emit one dispatch edge per (wf, target) pair
|
|
716
|
+
Object.entries(wfTargetActivities).forEach(([tgtId, activities]) => {
|
|
717
|
+
const allAsync = activities.every(a => a.type === "async");
|
|
718
|
+
const edgeId = `temporal-dispatch-${wfIdx}-${tgtId}`;
|
|
719
|
+
temporalEdgeActivities[edgeId] = { activities, color, wfLabel: shortLabel };
|
|
720
|
+
visEdges.push({
|
|
721
|
+
id: edgeId,
|
|
722
|
+
from: "__temporal__",
|
|
723
|
+
to: tgtId,
|
|
724
|
+
label: shortLabel,
|
|
725
|
+
dashes: allAsync ? [4, 3] : false,
|
|
726
|
+
color: { color, highlight: color, hover: color },
|
|
727
|
+
font: { color, size: 10, face: "'JetBrains Mono', monospace",
|
|
728
|
+
background: C.bg, strokeWidth: 0, align: "middle" },
|
|
729
|
+
arrows: { to: { enabled: true, scaleFactor: 0.7 } },
|
|
730
|
+
width: 2,
|
|
731
|
+
smooth: { enabled: true, type: "curvedCW", roundness: 0.12 + wfIdx * 0.18 },
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Mejora 5: compensation edges (red dashed, Temporal β same target, thin)
|
|
735
|
+
activities.forEach((act, aIdx) => {
|
|
736
|
+
if (!act.compensation) return;
|
|
737
|
+
const compId = `temporal-comp-${wfIdx}-${tgtId}-${aIdx}`;
|
|
738
|
+
visEdges.push({
|
|
739
|
+
id: compId,
|
|
740
|
+
from: "__temporal__",
|
|
741
|
+
to: tgtId,
|
|
742
|
+
label: "β© " + act.compensation,
|
|
743
|
+
dashes: [3, 4],
|
|
744
|
+
color: { color: C.accent + "99", highlight: C.accent, hover: C.accent },
|
|
745
|
+
font: { color: C.accent, size: 8, face: "'JetBrains Mono', monospace",
|
|
746
|
+
background: C.bg, strokeWidth: 0, align: "middle" },
|
|
747
|
+
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
748
|
+
width: 1,
|
|
749
|
+
smooth: { enabled: true, type: "curvedCCW", roundness: 0.35 },
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
temporalEdgeActivitiesRef.current = temporalEdgeActivities;
|
|
756
|
+
}
|
|
757
|
+
|
|
632
758
|
// Broker node β shown only if there are async events
|
|
633
759
|
if (EVENTS.length > 0) {
|
|
634
760
|
const brokerLabel = ((window.__EVA_DATA__.brokerName || "Kafka") + "\nBroker");
|
|
@@ -725,11 +851,18 @@
|
|
|
725
851
|
const net = new vis.Network(containerRef.current, data, options);
|
|
726
852
|
net.once("stabilizationIterationsDone", () => {
|
|
727
853
|
net.setOptions({ physics: { enabled: false } });
|
|
854
|
+
net.fit({ animation: { duration: 300, easingFunction: "easeInOutQuad" } });
|
|
728
855
|
setPhysicsOn(false);
|
|
729
856
|
});
|
|
730
857
|
net.on("hoverNode", (p) => setHoveredNode(p.node));
|
|
731
858
|
net.on("blurNode", () => setHoveredNode(null));
|
|
732
859
|
net.on("hoverEdge", (p) => {
|
|
860
|
+
// Mejora 3: temporal dispatch edges have their own tooltip
|
|
861
|
+
const temporalInfo = temporalEdgeActivitiesRef.current[p.edge];
|
|
862
|
+
if (temporalInfo) {
|
|
863
|
+
setHoveredTemporalEdge(temporalInfo);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
733
866
|
const groupIdx = edgeGroupMapRef.current[p.edge];
|
|
734
867
|
if (groupIdx === undefined || !edgesDataRef.current) return;
|
|
735
868
|
const groupIds = eventEdgesMapRef.current[groupIdx] || [];
|
|
@@ -759,6 +892,7 @@
|
|
|
759
892
|
});
|
|
760
893
|
net.on("blurEdge", () => {
|
|
761
894
|
setHoveredEvent(null);
|
|
895
|
+
setHoveredTemporalEdge(null);
|
|
762
896
|
if (!edgesDataRef.current) return;
|
|
763
897
|
const allIds = Object.keys(edgeGroupMapRef.current);
|
|
764
898
|
const FONT_PUB = { color: C.gold, size: 10, face: "'JetBrains Mono', monospace", background: C.bg, strokeWidth: 0, align: "middle" };
|
|
@@ -793,10 +927,11 @@
|
|
|
793
927
|
const fitView = () => networkRef.current && networkRef.current.fit({ animation: { duration: 400, easingFunction: "easeInOutQuad" } });
|
|
794
928
|
const resetNet = () => initNetwork(true);
|
|
795
929
|
|
|
796
|
-
const hoveredMod
|
|
797
|
-
const hoveredBroker
|
|
798
|
-
|
|
799
|
-
|
|
930
|
+
const hoveredMod = (hoveredNode && hoveredNode !== "__broker__" && hoveredNode !== "__temporal__") ? MODULES[hoveredNode] : null;
|
|
931
|
+
const hoveredBroker = hoveredNode === "__broker__";
|
|
932
|
+
const hoveredTemporal = hoveredNode === "__temporal__";
|
|
933
|
+
// overlay priority: hoveredTemporalEdge > hoveredEvent > node tooltips
|
|
934
|
+
const showOverlay = hoveredTemporalEdge || hoveredEvent || hoveredMod || hoveredBroker || hoveredTemporal;
|
|
800
935
|
|
|
801
936
|
return (
|
|
802
937
|
<div>
|
|
@@ -837,15 +972,48 @@
|
|
|
837
972
|
position: "absolute", bottom: 12, left: 12,
|
|
838
973
|
background: C.bg + "ee",
|
|
839
974
|
border: `1px solid ${
|
|
840
|
-
|
|
975
|
+
hoveredTemporalEdge ? hoveredTemporalEdge.color
|
|
976
|
+
: hoveredEvent ? "#4ade80"
|
|
841
977
|
: hoveredBroker ? C.gold
|
|
842
|
-
:
|
|
843
|
-
|
|
978
|
+
: hoveredTemporal ? C.purple
|
|
979
|
+
: hoveredMod ? hoveredMod.color : C.borderBright}66`,
|
|
980
|
+
borderRadius: 8, padding: "8px 14px", backdropFilter: "blur(4px)",
|
|
844
981
|
fontSize: 12, fontWeight: 600,
|
|
845
982
|
animation: "fadeIn 0.15s", pointerEvents: "none",
|
|
846
983
|
maxWidth: "80%",
|
|
847
984
|
}}>
|
|
848
|
-
{
|
|
985
|
+
{hoveredTemporalEdge
|
|
986
|
+
? <div>
|
|
987
|
+
<div style={{ color: hoveredTemporalEdge.color, marginBottom: 6 }}>
|
|
988
|
+
β±οΈ <span style={{ fontWeight: 700 }}>{hoveredTemporalEdge.wfLabel}</span>
|
|
989
|
+
<span style={{ color: C.textMuted, fontWeight: 400, marginLeft: 6, fontSize: 11 }}>actividades en este mΓ³dulo</span>
|
|
990
|
+
</div>
|
|
991
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 3 }}>
|
|
992
|
+
{hoveredTemporalEdge.activities.map((act, i) => {
|
|
993
|
+
const ROLE_COLORS = { READ: C.blue, WRITE: C.green, COMPENSATE: C.accent, NOTIFY: C.gold };
|
|
994
|
+
const roleColor = ROLE_COLORS[act.roleClass] || C.textMuted;
|
|
995
|
+
return (
|
|
996
|
+
<div key={i} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11 }}>
|
|
997
|
+
<span style={{
|
|
998
|
+
background: roleColor + "22", color: roleColor,
|
|
999
|
+
border: `1px solid ${roleColor}44`,
|
|
1000
|
+
borderRadius: 3, padding: "0 5px", fontSize: 9, fontWeight: 700,
|
|
1001
|
+
fontFamily: "'JetBrains Mono', monospace", letterSpacing: 0.4,
|
|
1002
|
+
minWidth: 70, textAlign: "center",
|
|
1003
|
+
}}>{act.roleClass || "STEP"}</span>
|
|
1004
|
+
<span style={{ color: C.textDim, fontFamily: "'JetBrains Mono', monospace" }}>{act.activity}</span>
|
|
1005
|
+
{act.type === "async" && (
|
|
1006
|
+
<span style={{ color: C.gold, fontSize: 9, fontWeight: 700, letterSpacing: 0.3 }}>async</span>
|
|
1007
|
+
)}
|
|
1008
|
+
{act.compensation && (
|
|
1009
|
+
<span style={{ color: C.accent, fontSize: 9 }}>β© {act.compensation}</span>
|
|
1010
|
+
)}
|
|
1011
|
+
</div>
|
|
1012
|
+
);
|
|
1013
|
+
})}
|
|
1014
|
+
</div>
|
|
1015
|
+
</div>
|
|
1016
|
+
: hoveredEvent
|
|
849
1017
|
? <span style={{ color: "#4ade80" }}>
|
|
850
1018
|
⬑ {hoveredEvent.name}
|
|
851
1019
|
<span style={{ color: C.textMuted, fontWeight: 400 }}> β </span>
|
|
@@ -853,6 +1021,8 @@
|
|
|
853
1021
|
{hoveredEvent.producer} β [{hoveredEvent.consumers.join(", ")}]
|
|
854
1022
|
</span>
|
|
855
1023
|
</span>
|
|
1024
|
+
: hoveredTemporal
|
|
1025
|
+
? <span style={{ color: C.purple }}>β±οΈ Temporal Server β <span style={{ color: C.textDim, fontWeight: 400 }}>Orquesta la ejecuciΓ³n de workflows y despacha actividades a los mΓ³dulos worker</span></span>
|
|
856
1026
|
: hoveredBroker
|
|
857
1027
|
? <span style={{ color: C.gold }}>β‘ Kafka Broker β <span style={{ color: C.textDim, fontWeight: 400 }}>Retransmite eventos asΓncronos entre mΓ³dulos</span></span>
|
|
858
1028
|
: <span style={{ color: hoveredMod.color }}>{hoveredMod.icon} {hoveredMod.label} β <span style={{ color: C.textDim, fontWeight: 400 }}>{hoveredMod.desc}</span></span>
|
|
@@ -866,25 +1036,67 @@
|
|
|
866
1036
|
display: "flex", gap: 24, marginTop: 14, flexWrap: "wrap",
|
|
867
1037
|
alignItems: "center", paddingLeft: 4,
|
|
868
1038
|
}}>
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
<
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
<
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1039
|
+
{IS_TEMPORAL_MODE ? (
|
|
1040
|
+
<>
|
|
1041
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1042
|
+
<svg width="36" height="12">
|
|
1043
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
1044
|
+
stroke={C.blue} strokeWidth="1.5" strokeDasharray="5 3" strokeLinecap="round" />
|
|
1045
|
+
<polygon points="32,3 36,6 32,9" fill={C.blue} />
|
|
1046
|
+
</svg>
|
|
1047
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>Trigger (por workflow)</span>
|
|
1048
|
+
</div>
|
|
1049
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1050
|
+
<svg width="36" height="12">
|
|
1051
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
1052
|
+
stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
|
|
1053
|
+
<polygon points="32,3 36,6 32,9" fill={C.blue} />
|
|
1054
|
+
</svg>
|
|
1055
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>Dispatch sync</span>
|
|
1056
|
+
</div>
|
|
1057
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1058
|
+
<svg width="36" height="12">
|
|
1059
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
1060
|
+
stroke={C.blue} strokeWidth="2" strokeDasharray="4 3" strokeLinecap="round" />
|
|
1061
|
+
<polygon points="32,3 36,6 32,9" fill={C.blue} />
|
|
1062
|
+
</svg>
|
|
1063
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>Dispatch async</span>
|
|
1064
|
+
</div>
|
|
1065
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1066
|
+
<svg width="36" height="12">
|
|
1067
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
1068
|
+
stroke={C.accent} strokeWidth="1" strokeDasharray="3 4" strokeLinecap="round" />
|
|
1069
|
+
<polygon points="32,3 36,6 32,9" fill={C.accent} />
|
|
1070
|
+
</svg>
|
|
1071
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>CompensaciΓ³n</span>
|
|
1072
|
+
</div>
|
|
1073
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
|
|
1074
|
+
{MODULES_LIST.length} mΓ³dulos Β· {(TEMPORAL_WORKFLOWS || []).length} workflows Β· hover edges para ver actividades
|
|
1075
|
+
</div>
|
|
1076
|
+
</>
|
|
1077
|
+
) : (
|
|
1078
|
+
<>
|
|
1079
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1080
|
+
<svg width="36" height="12">
|
|
1081
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
1082
|
+
stroke={C.blue} strokeWidth="2" strokeLinecap="round" />
|
|
1083
|
+
<polygon points="32,3 36,6 32,9" fill={C.blue} />
|
|
1084
|
+
</svg>
|
|
1085
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>SΓncrono (HTTP)</span>
|
|
1086
|
+
</div>
|
|
1087
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
|
1088
|
+
<svg width="36" height="12">
|
|
1089
|
+
<line x1="0" y1="6" x2="36" y2="6"
|
|
1090
|
+
stroke={C.gold} strokeWidth="1.5" strokeDasharray="6 4" strokeLinecap="round" />
|
|
1091
|
+
<polygon points="32,3 36,6 32,9" fill={C.gold} />
|
|
1092
|
+
</svg>
|
|
1093
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>AsΓncrono (Kafka)</span>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginLeft: "auto" }}>
|
|
1096
|
+
{MODULES_LIST.length} mΓ³dulos Β· {SYNC_INTEGRATIONS.length} puertos sync Β· {EVENTS.length} eventos
|
|
1097
|
+
</div>
|
|
1098
|
+
</>
|
|
1099
|
+
)}
|
|
888
1100
|
</div>
|
|
889
1101
|
</div>
|
|
890
1102
|
);
|
|
@@ -911,17 +1123,76 @@
|
|
|
911
1123
|
}
|
|
912
1124
|
|
|
913
1125
|
// βββ DiagramPanel ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
914
|
-
|
|
1126
|
+
// Props:
|
|
1127
|
+
// moduleKey β unique key for re-render detection
|
|
1128
|
+
// diagramText β Mermaid syntax string
|
|
1129
|
+
// onNodeClick β optional callback(nodeLabel) when a click-enabled node is clicked
|
|
1130
|
+
function DiagramPanel({ moduleKey, diagramText, onNodeClick }) {
|
|
915
1131
|
const containerRef = useRef(null);
|
|
916
1132
|
const wrapperRef = useRef(null);
|
|
917
1133
|
const [renderState, setRenderState] = useState('idle');
|
|
918
1134
|
const [errorMsg, setErrorMsg] = useState('');
|
|
919
|
-
const [
|
|
1135
|
+
const [zoom, setZoom] = useState(1);
|
|
1136
|
+
const [isPanning, setIsPanning] = useState(false);
|
|
1137
|
+
const [panOrigin, setPanOrigin] = useState({ x: 0, y: 0 });
|
|
1138
|
+
const [scrollOrigin, setScrollOrigin] = useState({ x: 0, y: 0 });
|
|
1139
|
+
|
|
1140
|
+
function zoomIn() { setZoom(function(z) { return Math.min(z + 0.2, 3); }); }
|
|
1141
|
+
function zoomOut() { setZoom(function(z) { return Math.max(z - 0.2, 0.3); }); }
|
|
1142
|
+
function zoomFit() {
|
|
1143
|
+
if (!wrapperRef.current || !containerRef.current) return;
|
|
1144
|
+
var svgEl = containerRef.current.querySelector('svg');
|
|
1145
|
+
if (!svgEl) return;
|
|
1146
|
+
var vb = svgEl.getAttribute('viewBox');
|
|
1147
|
+
if (!vb) { setZoom(1); return; }
|
|
1148
|
+
var intrinsicW = parseFloat(vb.split(/\s+/)[2]);
|
|
1149
|
+
var containerW = wrapperRef.current.clientWidth - 40;
|
|
1150
|
+
if (intrinsicW > 0 && containerW > 0) {
|
|
1151
|
+
setZoom(Math.min(containerW / intrinsicW, 2));
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
function zoomReset() { setZoom(1); }
|
|
1155
|
+
|
|
1156
|
+
// Wheel zoom β use native listener via useEffect so we can set { passive: false }
|
|
1157
|
+
// (React onWheel is passive by default, so preventDefault is ignored)
|
|
1158
|
+
useEffect(function() {
|
|
1159
|
+
var el = wrapperRef.current;
|
|
1160
|
+
if (!el) return;
|
|
1161
|
+
function onWheel(e) {
|
|
1162
|
+
if (!e.ctrlKey && !e.metaKey) return;
|
|
1163
|
+
e.preventDefault();
|
|
1164
|
+
setZoom(function(z) {
|
|
1165
|
+
var delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
1166
|
+
return Math.max(0.3, Math.min(z + delta, 3));
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
el.addEventListener('wheel', onWheel, { passive: false });
|
|
1170
|
+
return function() { el.removeEventListener('wheel', onWheel); };
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
// Pan via mouse drag
|
|
1174
|
+
function handleMouseDown(e) {
|
|
1175
|
+
if (e.button !== 0) return;
|
|
1176
|
+
setIsPanning(true);
|
|
1177
|
+
setPanOrigin({ x: e.clientX, y: e.clientY });
|
|
1178
|
+
setScrollOrigin({
|
|
1179
|
+
x: wrapperRef.current ? wrapperRef.current.scrollLeft : 0,
|
|
1180
|
+
y: wrapperRef.current ? wrapperRef.current.scrollTop : 0,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
function handleMouseMove(e) {
|
|
1184
|
+
if (!isPanning || !wrapperRef.current) return;
|
|
1185
|
+
wrapperRef.current.scrollLeft = scrollOrigin.x - (e.clientX - panOrigin.x);
|
|
1186
|
+
wrapperRef.current.scrollTop = scrollOrigin.y - (e.clientY - panOrigin.y);
|
|
1187
|
+
}
|
|
1188
|
+
function handleMouseUp() { setIsPanning(false); }
|
|
920
1189
|
|
|
921
1190
|
useEffect(() => {
|
|
922
1191
|
if (!diagramText) return;
|
|
923
1192
|
let cancelled = false;
|
|
924
1193
|
setRenderState('loading');
|
|
1194
|
+
setZoom(1);
|
|
1195
|
+
|
|
925
1196
|
(async () => {
|
|
926
1197
|
try {
|
|
927
1198
|
// Wait up to 4s for mermaid to load (deferred script)
|
|
@@ -931,31 +1202,57 @@
|
|
|
931
1202
|
waited += 100;
|
|
932
1203
|
}
|
|
933
1204
|
if (!ensureMermaid()) throw new Error('Mermaid no estΓ‘ disponible');
|
|
1205
|
+
|
|
1206
|
+
// Enable click callbacks via securityLevel loose
|
|
1207
|
+
window.mermaid.initialize({
|
|
1208
|
+
startOnLoad: false,
|
|
1209
|
+
theme: 'dark',
|
|
1210
|
+
securityLevel: 'loose',
|
|
1211
|
+
themeVariables: {
|
|
1212
|
+
background: '#0a0a0f',
|
|
1213
|
+
primaryColor: '#1e1e2e',
|
|
1214
|
+
primaryBorderColor: '#4a4a8a',
|
|
1215
|
+
lineColor: '#8c8caa',
|
|
1216
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
1217
|
+
fontSize: '13px',
|
|
1218
|
+
},
|
|
1219
|
+
});
|
|
1220
|
+
_mermaidReady = true;
|
|
1221
|
+
|
|
934
1222
|
if (cancelled) return;
|
|
935
1223
|
const id = 'mmd-' + moduleKey.replace(/[^a-zA-Z0-9]/g, '-') + '-' + Date.now();
|
|
936
1224
|
const { svg } = await window.mermaid.render(id, diagramText);
|
|
937
1225
|
if (cancelled || !containerRef.current) return;
|
|
938
1226
|
containerRef.current.innerHTML = svg;
|
|
1227
|
+
|
|
1228
|
+
// Manually bind click handlers to CMD_ and Q_ nodes in the SVG.
|
|
1229
|
+
// Mermaid v11 click directives are unreliable across versions,
|
|
1230
|
+
// so we bypass them and attach listeners directly to the DOM.
|
|
1231
|
+
if (onNodeClick) {
|
|
1232
|
+
containerRef.current.querySelectorAll('[id*="flowchart-CMD_"], [id*="flowchart-Q_"]').forEach(function(node) {
|
|
1233
|
+
var match = (node.id || '').match(/flowchart-(CMD|Q)_([^-]+)/);
|
|
1234
|
+
if (match) {
|
|
1235
|
+
var ucName = match[2];
|
|
1236
|
+
node.style.cursor = 'pointer';
|
|
1237
|
+
node.addEventListener('click', function(e) {
|
|
1238
|
+
e.stopPropagation();
|
|
1239
|
+
onNodeClick(ucName);
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
939
1245
|
const svgEl = containerRef.current.querySelector('svg');
|
|
940
1246
|
if (svgEl) {
|
|
941
1247
|
svgEl.removeAttribute('height');
|
|
942
1248
|
svgEl.removeAttribute('width');
|
|
943
1249
|
|
|
944
|
-
//
|
|
1250
|
+
// Render at natural intrinsic size; zoom handles scaling
|
|
945
1251
|
const vb = svgEl.getAttribute('viewBox');
|
|
946
1252
|
const intrinsicW = vb ? parseFloat(vb.split(/\s+/)[2]) : 0;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
if (intrinsicW > 0 && intrinsicW < containerW * 0.65) {
|
|
950
|
-
// Small diagram (1-2 classes): render at natural size and center
|
|
1253
|
+
if (intrinsicW > 0) {
|
|
951
1254
|
svgEl.style.width = intrinsicW + 'px';
|
|
952
|
-
svgEl.style.maxWidth = '100%';
|
|
953
|
-
setWrapperJustify('center');
|
|
954
|
-
} else {
|
|
955
|
-
// Large diagram: expand to fill; overflow: auto handles horizontal scroll
|
|
956
|
-
svgEl.style.width = Math.max(intrinsicW, containerW) + 'px';
|
|
957
1255
|
svgEl.style.maxWidth = 'none';
|
|
958
|
-
setWrapperJustify('flex-start');
|
|
959
1256
|
}
|
|
960
1257
|
}
|
|
961
1258
|
setRenderState('done');
|
|
@@ -976,26 +1273,293 @@
|
|
|
976
1273
|
</div>
|
|
977
1274
|
);
|
|
978
1275
|
}
|
|
1276
|
+
|
|
1277
|
+
var zoomPct = Math.round(zoom * 100) + '%';
|
|
1278
|
+
|
|
979
1279
|
return (
|
|
980
|
-
<div
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1280
|
+
<div style={{ position: 'relative' }}>
|
|
1281
|
+
{/* Zoom toolbar */}
|
|
1282
|
+
{renderState === 'done' && (
|
|
1283
|
+
<div style={{
|
|
1284
|
+
display: 'flex', alignItems: 'center', gap: 4, marginBottom: 8,
|
|
1285
|
+
background: C.surface, border: '1px solid ' + C.border,
|
|
1286
|
+
borderRadius: 6, padding: '4px 8px', width: 'fit-content',
|
|
1287
|
+
}}>
|
|
1288
|
+
<button onClick={zoomOut} title="Zoom out" style={{
|
|
1289
|
+
background: 'transparent', color: C.textMuted,
|
|
1290
|
+
border: '1px solid ' + C.border, borderRadius: 4,
|
|
1291
|
+
padding: '3px 10px', fontSize: 13, cursor: 'pointer',
|
|
1292
|
+
fontFamily: 'inherit', fontWeight: 600,
|
|
1293
|
+
}}>β</button>
|
|
1294
|
+
<button onClick={zoomReset} title="Reset zoom" style={{
|
|
1295
|
+
background: 'transparent', color: C.textMuted,
|
|
1296
|
+
border: '1px solid ' + C.border, borderRadius: 4,
|
|
1297
|
+
padding: '3px 10px', fontSize: 13, cursor: 'pointer',
|
|
1298
|
+
fontFamily: "'JetBrains Mono', monospace", fontWeight: 600,
|
|
1299
|
+
minWidth: 48, textAlign: 'center',
|
|
1300
|
+
}}>{zoomPct}</button>
|
|
1301
|
+
<button onClick={zoomIn} title="Zoom in" style={{
|
|
1302
|
+
background: 'transparent', color: C.textMuted,
|
|
1303
|
+
border: '1px solid ' + C.border, borderRadius: 4,
|
|
1304
|
+
padding: '3px 10px', fontSize: 13, cursor: 'pointer',
|
|
1305
|
+
fontFamily: 'inherit', fontWeight: 600,
|
|
1306
|
+
}}>+</button>
|
|
1307
|
+
<button onClick={zoomFit} title="Fit to width" style={{
|
|
1308
|
+
background: 'transparent', color: C.textMuted,
|
|
1309
|
+
border: '1px solid ' + C.border, borderRadius: 4,
|
|
1310
|
+
padding: '3px 10px', fontSize: 13, cursor: 'pointer',
|
|
1311
|
+
fontFamily: 'inherit', fontWeight: 600,
|
|
1312
|
+
}}>β€’</button>
|
|
1313
|
+
<span style={{ fontSize: 11, color: C.textMuted, marginLeft: 8 }}>Ctrl+scroll Β· drag to pan</span>
|
|
991
1314
|
</div>
|
|
992
1315
|
)}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1316
|
+
|
|
1317
|
+
{/* Diagram viewport */}
|
|
1318
|
+
<div
|
|
1319
|
+
ref={wrapperRef}
|
|
1320
|
+
onMouseDown={handleMouseDown}
|
|
1321
|
+
onMouseMove={handleMouseMove}
|
|
1322
|
+
onMouseUp={handleMouseUp}
|
|
1323
|
+
onMouseLeave={handleMouseUp}
|
|
1324
|
+
style={{
|
|
1325
|
+
background: '#0d0d15', borderRadius: 10, padding: 20,
|
|
1326
|
+
overflow: 'auto', border: '1px solid ' + C.border, minHeight: 200,
|
|
1327
|
+
cursor: isPanning ? 'grabbing' : 'grab',
|
|
1328
|
+
}}
|
|
1329
|
+
>
|
|
1330
|
+
{renderState === 'loading' && (
|
|
1331
|
+
<div style={{ color: C.textMuted, fontSize: 13, textAlign: 'center', padding: 30, width: '100%' }}>
|
|
1332
|
+
β³ Renderizando diagrama...
|
|
1333
|
+
</div>
|
|
1334
|
+
)}
|
|
1335
|
+
{renderState === 'error' && (
|
|
1336
|
+
<div style={{ color: C.accent, fontSize: 12, padding: 10, fontFamily: "'JetBrains Mono', monospace" }}>
|
|
1337
|
+
β {errorMsg}
|
|
1338
|
+
</div>
|
|
1339
|
+
)}
|
|
1340
|
+
<div
|
|
1341
|
+
ref={containerRef}
|
|
1342
|
+
style={{
|
|
1343
|
+
display: renderState === 'done' ? 'block' : 'none',
|
|
1344
|
+
transform: 'scale(' + zoom + ')',
|
|
1345
|
+
transformOrigin: 'top left',
|
|
1346
|
+
transition: isPanning ? 'none' : 'transform 0.15s ease',
|
|
1347
|
+
}}
|
|
1348
|
+
/>
|
|
1349
|
+
</div>
|
|
1350
|
+
</div>
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
// βββ UseCaseModal βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1355
|
+
function UseCaseModal({ uc, onClose }) {
|
|
1356
|
+
if (!uc) return null;
|
|
1357
|
+
|
|
1358
|
+
var typeColor = uc.type === 'command' ? '#E67E22' : '#4A90D9';
|
|
1359
|
+
var typeLabel = uc.type === 'command' ? 'βοΈ Command' : 'π Query';
|
|
1360
|
+
var badgeBg = uc.isStandard ? '#27AE6033' : '#E67E2233';
|
|
1361
|
+
var badgeBorder = uc.isStandard ? '#27AE6066' : '#E67E2266';
|
|
1362
|
+
var badgeColor = uc.isStandard ? '#27AE60' : '#E67E22';
|
|
1363
|
+
var badgeText = uc.isStandard ? 'EstΓ‘ndar' : 'Custom';
|
|
1364
|
+
|
|
1365
|
+
return (
|
|
1366
|
+
<div onClick={onClose} style={{
|
|
1367
|
+
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
|
1368
|
+
background: 'rgba(0,0,0,0.65)', zIndex: 9999,
|
|
1369
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
1370
|
+
animation: 'fadeIn 0.15s ease',
|
|
1371
|
+
}}>
|
|
1372
|
+
<div onClick={function(e) { e.stopPropagation(); }} style={{
|
|
1373
|
+
background: '#12121f', border: '1px solid #2a2a4a',
|
|
1374
|
+
borderRadius: 14, padding: 0, maxWidth: 560, width: '90%',
|
|
1375
|
+
maxHeight: '80vh', overflow: 'auto', boxShadow: '0 20px 60px rgba(0,0,0,0.5)',
|
|
1376
|
+
}}>
|
|
1377
|
+
{/* Header */}
|
|
1378
|
+
<div style={{
|
|
1379
|
+
padding: '20px 24px 16px', borderBottom: '1px solid #1e1e3a',
|
|
1380
|
+
display: 'flex', alignItems: 'flex-start', gap: 12,
|
|
1381
|
+
}}>
|
|
1382
|
+
<div style={{ flex: 1 }}>
|
|
1383
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
|
|
1384
|
+
<span style={{ color: typeColor, fontSize: 12, fontWeight: 700,
|
|
1385
|
+
background: typeColor + '22', border: '1px solid ' + typeColor + '44',
|
|
1386
|
+
borderRadius: 4, padding: '2px 10px',
|
|
1387
|
+
}}>{typeLabel}</span>
|
|
1388
|
+
<span style={{ color: badgeColor, fontSize: 11, fontWeight: 600,
|
|
1389
|
+
background: badgeBg, border: '1px solid ' + badgeBorder,
|
|
1390
|
+
borderRadius: 4, padding: '2px 8px',
|
|
1391
|
+
}}>{badgeText}</span>
|
|
1392
|
+
</div>
|
|
1393
|
+
<div style={{ fontSize: 20, fontWeight: 800, color: '#e2e2f0',
|
|
1394
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
1395
|
+
}}>{uc.name}</div>
|
|
1396
|
+
<div style={{ fontSize: 12, color: '#8c8caa', marginTop: 4,
|
|
1397
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
1398
|
+
}}>{uc.name + (uc.type === 'command' ? 'CommandHandler' : 'QueryHandler')}</div>
|
|
1399
|
+
</div>
|
|
1400
|
+
<button onClick={onClose} style={{
|
|
1401
|
+
background: 'transparent', border: '1px solid #2a2a4a', borderRadius: 6,
|
|
1402
|
+
color: '#8c8caa', fontSize: 16, cursor: 'pointer', padding: '4px 10px',
|
|
1403
|
+
fontFamily: 'inherit',
|
|
1404
|
+
}}>β</button>
|
|
996
1405
|
</div>
|
|
997
|
-
|
|
998
|
-
|
|
1406
|
+
|
|
1407
|
+
{/* Body */}
|
|
1408
|
+
<div style={{ padding: '16px 24px 20px' }}>
|
|
1409
|
+
{/* Description */}
|
|
1410
|
+
<div style={{
|
|
1411
|
+
background: '#0d0d18', borderRadius: 8, padding: '12px 16px', marginBottom: 16,
|
|
1412
|
+
border: '1px solid #1e1e3a', fontSize: 13, color: '#b0b0cc', lineHeight: 1.6,
|
|
1413
|
+
}}>{uc.description}</div>
|
|
1414
|
+
|
|
1415
|
+
{/* Endpoint */}
|
|
1416
|
+
{uc.endpoint && (
|
|
1417
|
+
<div style={{ marginBottom: 14 }}>
|
|
1418
|
+
<div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Endpoint</div>
|
|
1419
|
+
<div style={{
|
|
1420
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
1421
|
+
background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
|
|
1422
|
+
}}>
|
|
1423
|
+
<span style={{
|
|
1424
|
+
fontWeight: 800, fontSize: 12, color: uc.endpoint.method === 'GET' ? '#4A90D9' : uc.endpoint.method === 'POST' ? '#27AE60' : uc.endpoint.method === 'DELETE' ? '#E74C3C' : '#E67E22',
|
|
1425
|
+
fontFamily: "'JetBrains Mono', monospace",
|
|
1426
|
+
}}>{uc.endpoint.method}</span>
|
|
1427
|
+
<span style={{ fontSize: 13, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace" }}>{uc.endpoint.path}</span>
|
|
1428
|
+
{uc.endpoint.version && (
|
|
1429
|
+
<span style={{ fontSize: 11, color: '#8c8caa', marginLeft: 'auto',
|
|
1430
|
+
background: '#1e1e3a', borderRadius: 4, padding: '1px 6px',
|
|
1431
|
+
}}>{uc.endpoint.version}</span>
|
|
1432
|
+
)}
|
|
1433
|
+
</div>
|
|
1434
|
+
</div>
|
|
1435
|
+
)}
|
|
1436
|
+
|
|
1437
|
+
{/* Triggered by event */}
|
|
1438
|
+
{uc.triggeredBy && (
|
|
1439
|
+
<div style={{ marginBottom: 14 }}>
|
|
1440
|
+
<div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Disparado por evento</div>
|
|
1441
|
+
<div style={{
|
|
1442
|
+
background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
|
|
1443
|
+
display: 'flex', alignItems: 'center', gap: 8,
|
|
1444
|
+
}}>
|
|
1445
|
+
<span style={{ color: '#27AE60', fontSize: 13 }}>π₯</span>
|
|
1446
|
+
<span style={{ fontSize: 13, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace" }}>{uc.triggeredBy.event}</span>
|
|
1447
|
+
{uc.triggeredBy.producer && (
|
|
1448
|
+
<span style={{ fontSize: 11, color: '#8c8caa', marginLeft: 'auto' }}>β {uc.triggeredBy.producer}</span>
|
|
1449
|
+
)}
|
|
1450
|
+
</div>
|
|
1451
|
+
{uc.triggeredBy.topic && (
|
|
1452
|
+
<div style={{ fontSize: 11, color: '#666', marginTop: 4, fontFamily: "'JetBrains Mono', monospace" }}>topic: {uc.triggeredBy.topic}</div>
|
|
1453
|
+
)}
|
|
1454
|
+
</div>
|
|
1455
|
+
)}
|
|
1456
|
+
|
|
1457
|
+
{/* State transitions */}
|
|
1458
|
+
{uc.stateTransitions && uc.stateTransitions.length > 0 && (
|
|
1459
|
+
<div style={{ marginBottom: 14 }}>
|
|
1460
|
+
<div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Transiciones de estado</div>
|
|
1461
|
+
{uc.stateTransitions.map(function(t, i) {
|
|
1462
|
+
return (
|
|
1463
|
+
<div key={i} style={{
|
|
1464
|
+
background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
|
|
1465
|
+
marginBottom: 4, display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, flexWrap: 'wrap',
|
|
1466
|
+
}}>
|
|
1467
|
+
<span style={{ color: '#B07CC6', fontFamily: "'JetBrains Mono', monospace", fontWeight: 600 }}>{t.method}()</span>
|
|
1468
|
+
<span style={{ color: '#8c8caa' }}>:</span>
|
|
1469
|
+
<span style={{ color: '#E67E22', fontFamily: "'JetBrains Mono', monospace" }}>{t.from}</span>
|
|
1470
|
+
<span style={{ color: '#8c8caa' }}>β</span>
|
|
1471
|
+
<span style={{ color: '#27AE60', fontFamily: "'JetBrains Mono', monospace" }}>{t.to}</span>
|
|
1472
|
+
{t.guard && (
|
|
1473
|
+
<span style={{ fontSize: 11, color: '#E74C3C', marginLeft: 'auto' }}>guard: {t.guard}</span>
|
|
1474
|
+
)}
|
|
1475
|
+
</div>
|
|
1476
|
+
);
|
|
1477
|
+
})}
|
|
1478
|
+
</div>
|
|
1479
|
+
)}
|
|
1480
|
+
|
|
1481
|
+
{/* Events emitted */}
|
|
1482
|
+
{uc.eventsEmitted && uc.eventsEmitted.length > 0 && (
|
|
1483
|
+
<div style={{ marginBottom: 14 }}>
|
|
1484
|
+
<div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Eventos emitidos</div>
|
|
1485
|
+
{uc.eventsEmitted.map(function(ev, i) {
|
|
1486
|
+
return (
|
|
1487
|
+
<div key={i} style={{
|
|
1488
|
+
background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
|
|
1489
|
+
marginBottom: 4,
|
|
1490
|
+
}}>
|
|
1491
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
1492
|
+
<span style={{ color: '#E67E22', fontSize: 13 }}>π€</span>
|
|
1493
|
+
<span style={{ fontSize: 13, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace", fontWeight: 600 }}>{ev.name}</span>
|
|
1494
|
+
</div>
|
|
1495
|
+
{ev.fields.length > 0 && (
|
|
1496
|
+
<div style={{ marginTop: 4, fontSize: 11, color: '#8c8caa', fontFamily: "'JetBrains Mono', monospace" }}>
|
|
1497
|
+
{ev.fields.join(' Β· ')}
|
|
1498
|
+
</div>
|
|
1499
|
+
)}
|
|
1500
|
+
</div>
|
|
1501
|
+
);
|
|
1502
|
+
})}
|
|
1503
|
+
</div>
|
|
1504
|
+
)}
|
|
1505
|
+
|
|
1506
|
+
{/* Request fields */}
|
|
1507
|
+
{uc.requestFields && uc.requestFields.length > 0 && (
|
|
1508
|
+
<div style={{ marginBottom: 14 }}>
|
|
1509
|
+
<div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Campos del request</div>
|
|
1510
|
+
<div style={{
|
|
1511
|
+
background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
|
|
1512
|
+
}}>
|
|
1513
|
+
{uc.requestFields.map(function(f, i) {
|
|
1514
|
+
return (
|
|
1515
|
+
<div key={i} style={{
|
|
1516
|
+
display: 'flex', alignItems: 'center', gap: 8, padding: '3px 0',
|
|
1517
|
+
borderBottom: i < uc.requestFields.length - 1 ? '1px solid #1a1a2e' : 'none',
|
|
1518
|
+
}}>
|
|
1519
|
+
<span style={{ fontSize: 12, color: '#e2e2f0', fontFamily: "'JetBrains Mono', monospace", flex: 1 }}>{f.name}</span>
|
|
1520
|
+
<span style={{ fontSize: 11, color: '#8c8caa', fontFamily: "'JetBrains Mono', monospace" }}>{f.type}</span>
|
|
1521
|
+
{f.required && (
|
|
1522
|
+
<span style={{ fontSize: 10, color: '#E74C3C', fontWeight: 700 }}>REQ</span>
|
|
1523
|
+
)}
|
|
1524
|
+
</div>
|
|
1525
|
+
);
|
|
1526
|
+
})}
|
|
1527
|
+
</div>
|
|
1528
|
+
</div>
|
|
1529
|
+
)}
|
|
1530
|
+
|
|
1531
|
+
{/* Available ports */}
|
|
1532
|
+
{uc.availablePorts && uc.availablePorts.length > 0 && (
|
|
1533
|
+
<div style={{ marginBottom: 14 }}>
|
|
1534
|
+
<div style={{ fontSize: 11, color: '#8c8caa', fontWeight: 700, marginBottom: 6, textTransform: 'uppercase', letterSpacing: 1 }}>Puertos sΓncronos disponibles</div>
|
|
1535
|
+
{uc.availablePorts.map(function(p, i) {
|
|
1536
|
+
return (
|
|
1537
|
+
<div key={i} style={{
|
|
1538
|
+
background: '#0d0d18', borderRadius: 6, padding: '8px 12px', border: '1px solid #1e1e3a',
|
|
1539
|
+
marginBottom: 4,
|
|
1540
|
+
}}>
|
|
1541
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
1542
|
+
<span style={{ color: '#16A085', fontSize: 13 }}>π</span>
|
|
1543
|
+
<span style={{ fontSize: 13, color: '#e2e2f0', fontWeight: 600 }}>{p.service}</span>
|
|
1544
|
+
{p.target && (
|
|
1545
|
+
<span style={{ fontSize: 11, color: '#8c8caa', marginLeft: 'auto' }}>β {p.target}</span>
|
|
1546
|
+
)}
|
|
1547
|
+
</div>
|
|
1548
|
+
<div style={{ marginTop: 4, fontSize: 11, color: '#8c8caa', fontFamily: "'JetBrains Mono', monospace" }}>
|
|
1549
|
+
{p.methods.join(' Β· ')}
|
|
1550
|
+
</div>
|
|
1551
|
+
</div>
|
|
1552
|
+
);
|
|
1553
|
+
})}
|
|
1554
|
+
</div>
|
|
1555
|
+
)}
|
|
1556
|
+
|
|
1557
|
+
{/* Aggregate */}
|
|
1558
|
+
<div style={{ fontSize: 11, color: '#666', borderTop: '1px solid #1e1e3a', paddingTop: 12, marginTop: 8 }}>
|
|
1559
|
+
Agregado: <strong style={{ color: '#B07CC6' }}>{uc.aggregate}</strong>
|
|
1560
|
+
</div>
|
|
1561
|
+
</div>
|
|
1562
|
+
</div>
|
|
999
1563
|
</div>
|
|
1000
1564
|
);
|
|
1001
1565
|
}
|
|
@@ -1005,16 +1569,19 @@
|
|
|
1005
1569
|
const [expandedChecks, setExpandedChecks] = useState({});
|
|
1006
1570
|
const [selectedModule, setSelectedModule] = useState('all');
|
|
1007
1571
|
const [view, setView] = useState('findings');
|
|
1572
|
+
const [selectedUC, setSelectedUC] = useState(null); // use case detail object for modal
|
|
1008
1573
|
if (!DOMAIN_VALIDATION) return null;
|
|
1009
1574
|
|
|
1010
|
-
const { summary, categories, diagrams } = DOMAIN_VALIDATION;
|
|
1575
|
+
const { summary, categories, diagrams, blueprints, useCaseDetails } = DOMAIN_VALIDATION;
|
|
1011
1576
|
|
|
1012
1577
|
// Collect all unique module names that have at least one finding
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
)
|
|
1578
|
+
// Also include modules that have diagrams/blueprints even without findings
|
|
1579
|
+
const findingsModules = categories.flatMap(cat =>
|
|
1580
|
+
cat.checks.flatMap(check => check.findings.map(f => f.module))
|
|
1581
|
+
);
|
|
1582
|
+
const diagramModules = diagrams ? Object.keys(diagrams).filter(k => diagrams[k]) : [];
|
|
1583
|
+
const blueprintModules = blueprints ? Object.keys(blueprints).filter(k => blueprints[k]) : [];
|
|
1584
|
+
const allModules = [...new Set([...findingsModules, ...diagramModules, ...blueprintModules])].sort();
|
|
1018
1585
|
|
|
1019
1586
|
const SEV_COLOR = {
|
|
1020
1587
|
error: C.accent,
|
|
@@ -1110,13 +1677,13 @@
|
|
|
1110
1677
|
)}
|
|
1111
1678
|
</div>
|
|
1112
1679
|
|
|
1113
|
-
{/* View toggle pill (only rendered when diagrams are available) */}
|
|
1114
|
-
{diagrams && Object.keys(diagrams).length > 0 && (
|
|
1680
|
+
{/* View toggle pill (only rendered when diagrams or blueprints are available) */}
|
|
1681
|
+
{((diagrams && Object.keys(diagrams).length > 0) || (blueprints && Object.keys(blueprints).length > 0)) && (
|
|
1115
1682
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 20 }}>
|
|
1116
|
-
{[{ id: 'findings', label: 'π Hallazgos' }, { id: 'diagram', label: 'πΊ Diagrama' }].map(v => (
|
|
1683
|
+
{[{ id: 'findings', label: 'π Hallazgos' }, { id: 'diagram', label: 'πΊ Diagrama' }, { id: 'blueprint', label: 'π Blueprint' }].map(v => (
|
|
1117
1684
|
<button
|
|
1118
1685
|
key={v.id}
|
|
1119
|
-
onClick={() => setView(v.id)}
|
|
1686
|
+
onClick={() => { setView(v.id); }}
|
|
1120
1687
|
style={{
|
|
1121
1688
|
background: view === v.id ? C.purple + '33' : 'transparent',
|
|
1122
1689
|
color: view === v.id ? C.purple : C.textMuted,
|
|
@@ -1143,6 +1710,31 @@
|
|
|
1143
1710
|
</div>
|
|
1144
1711
|
)}
|
|
1145
1712
|
|
|
1713
|
+
{/* Blueprint view */}
|
|
1714
|
+
{view === 'blueprint' && (
|
|
1715
|
+
<div>
|
|
1716
|
+
{selectedModule === 'all' ? (
|
|
1717
|
+
<div style={{ padding: 32, textAlign: 'center', background: C.surface, borderRadius: 10, border: `1px solid ${C.border}`, color: C.textMuted, fontSize: 13 }}>
|
|
1718
|
+
Selecciona un mΓ³dulo en el filtro de arriba para ver su blueprint de bounded context
|
|
1719
|
+
</div>
|
|
1720
|
+
) : (
|
|
1721
|
+
<DiagramPanel
|
|
1722
|
+
moduleKey={selectedModule + '-bp'}
|
|
1723
|
+
diagramText={blueprints && blueprints[selectedModule]}
|
|
1724
|
+
onNodeClick={function(label) {
|
|
1725
|
+
var moduleUCs = useCaseDetails && useCaseDetails[selectedModule];
|
|
1726
|
+
if (moduleUCs && moduleUCs[label]) {
|
|
1727
|
+
setSelectedUC(moduleUCs[label]);
|
|
1728
|
+
}
|
|
1729
|
+
}}
|
|
1730
|
+
/>
|
|
1731
|
+
)}
|
|
1732
|
+
</div>
|
|
1733
|
+
)}
|
|
1734
|
+
|
|
1735
|
+
{/* Use case detail modal */}
|
|
1736
|
+
{selectedUC && <UseCaseModal uc={selectedUC} onClose={function() { setSelectedUC(null); }} />}
|
|
1737
|
+
|
|
1146
1738
|
{/* Category cards */}
|
|
1147
1739
|
{view === 'findings' && categories.map(cat => {
|
|
1148
1740
|
// Apply module filter to each check's findings
|
|
@@ -1276,22 +1868,765 @@
|
|
|
1276
1868
|
</div>
|
|
1277
1869
|
);
|
|
1278
1870
|
}
|
|
1871
|
+
// βββ Temporal: role badge colors ββββββββββββββββββββββββββββββββββββββββ
|
|
1872
|
+
const ROLE_STYLE = {
|
|
1873
|
+
READ: { color: C.blue, label: "READ", bg: C.blue + "22" },
|
|
1874
|
+
WRITE: { color: C.green, label: "WRITE", bg: C.green + "22" },
|
|
1875
|
+
COMPENSATE: { color: C.accent, label: "COMPENSATE", bg: C.accent + "22" },
|
|
1876
|
+
NOTIFY: { color: C.gold, label: "NOTIFY", bg: C.gold + "22" },
|
|
1877
|
+
};
|
|
1878
|
+
|
|
1879
|
+
const PRIMARY_ROLE_STYLE = {
|
|
1880
|
+
Orchestrator: { color: C.purple, icon: "π―" },
|
|
1881
|
+
Executor: { color: C.green, icon: "βοΈ" },
|
|
1882
|
+
DataProvider: { color: C.blue, icon: "π¦" },
|
|
1883
|
+
Reactor: { color: C.gold, icon: "β‘" },
|
|
1884
|
+
Standalone: { color: C.textMuted, icon: "β" },
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
function RoleBadge({ role, small }) {
|
|
1888
|
+
const rs = ROLE_STYLE[role] || { color: C.textMuted, label: role, bg: C.surface };
|
|
1889
|
+
return (
|
|
1890
|
+
<span style={{
|
|
1891
|
+
background: rs.bg, color: rs.color, border: `1px solid ${rs.color}55`,
|
|
1892
|
+
borderRadius: 4, padding: small ? "0px 5px" : "2px 8px",
|
|
1893
|
+
fontSize: small ? 9 : 11, fontWeight: 700, letterSpacing: 0.5,
|
|
1894
|
+
fontFamily: "'JetBrains Mono', monospace", display: "inline-block",
|
|
1895
|
+
}}>{rs.label}</span>
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
// βββ WorkflowsTab βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1900
|
+
function WorkflowsTab() {
|
|
1901
|
+
const allWfs = [...(TEMPORAL_WORKFLOWS || []), ...(LOCAL_WORKFLOWS || [])];
|
|
1902
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
1903
|
+
const [expandedStep, setExpandedStep] = useState(null);
|
|
1904
|
+
const wf = allWfs[selectedIdx] || null;
|
|
1905
|
+
|
|
1906
|
+
if (!wf) return (
|
|
1907
|
+
<div style={{ color: C.textMuted, padding: 60, textAlign: "center" }}>
|
|
1908
|
+
<div style={{ fontSize: 40, marginBottom: 16 }}>β±οΈ</div>
|
|
1909
|
+
No hay workflows Temporal declarados en system.yaml
|
|
1910
|
+
</div>
|
|
1911
|
+
);
|
|
1912
|
+
|
|
1913
|
+
const typeIcon = (type, actType) => {
|
|
1914
|
+
if (type === "async") return "β‘";
|
|
1915
|
+
if (actType === "heavy") return "ποΈ";
|
|
1916
|
+
return "βΆ";
|
|
1917
|
+
};
|
|
1918
|
+
|
|
1919
|
+
return (
|
|
1920
|
+
<div>
|
|
1921
|
+
{/* Workflow selector */}
|
|
1922
|
+
<div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
|
|
1923
|
+
{allWfs.map((w, i) => (
|
|
1924
|
+
<button key={i} onClick={() => { setSelectedIdx(i); setExpandedStep(null); }} style={{
|
|
1925
|
+
background: i === selectedIdx ? C.purple + "22" : C.surface,
|
|
1926
|
+
border: `1px solid ${i === selectedIdx ? C.purple : C.border}`,
|
|
1927
|
+
color: i === selectedIdx ? C.purple : C.textMuted,
|
|
1928
|
+
borderRadius: 8, padding: "7px 14px", cursor: "pointer",
|
|
1929
|
+
fontWeight: i === selectedIdx ? 700 : 400, fontSize: 12,
|
|
1930
|
+
transition: "all 0.15s", fontFamily: "inherit",
|
|
1931
|
+
display: "flex", alignItems: "center", gap: 6,
|
|
1932
|
+
}}>
|
|
1933
|
+
{w.saga && <span title="Saga">π</span>}
|
|
1934
|
+
{w._isLocal && <span title="Local">π</span>}
|
|
1935
|
+
{w.name}
|
|
1936
|
+
</button>
|
|
1937
|
+
))}
|
|
1938
|
+
</div>
|
|
1939
|
+
|
|
1940
|
+
{/* Workflow header */}
|
|
1941
|
+
<div style={{
|
|
1942
|
+
background: C.surface, border: `1px solid ${C.purple}44`, borderRadius: 12,
|
|
1943
|
+
padding: 20, marginBottom: 20,
|
|
1944
|
+
}}>
|
|
1945
|
+
<div style={{ display: "flex", alignItems: "flex-start", gap: 16 }}>
|
|
1946
|
+
<div style={{ flex: 1 }}>
|
|
1947
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" }}>
|
|
1948
|
+
<span style={{ fontWeight: 800, fontSize: 18, color: C.text }}>{wf.name}</span>
|
|
1949
|
+
{wf.saga && <Tag color={C.accent}>π SAGA</Tag>}
|
|
1950
|
+
{wf._isLocal && <Tag color={C.gold}>π LOCAL</Tag>}
|
|
1951
|
+
</div>
|
|
1952
|
+
{wf.trigger && (
|
|
1953
|
+
<div style={{ marginTop: 10, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
1954
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>TRIGGER</span>
|
|
1955
|
+
<Tag color={wf.triggerModuleColor || C.blue}>
|
|
1956
|
+
{wf.triggerModuleIcon} {wf.triggerModuleLabel}
|
|
1957
|
+
</Tag>
|
|
1958
|
+
{wf.trigger.on && (
|
|
1959
|
+
<>
|
|
1960
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>evento</span>
|
|
1961
|
+
<code style={{ background: C.bg, color: C.gold, padding: "2px 8px", borderRadius: 4, fontSize: 11, border: `1px solid ${C.border}` }}>{wf.trigger.on}</code>
|
|
1962
|
+
</>
|
|
1963
|
+
)}
|
|
1964
|
+
</div>
|
|
1965
|
+
)}
|
|
1966
|
+
<div style={{ marginTop: 10, display: "flex", gap: 16, flexWrap: "wrap" }}>
|
|
1967
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>
|
|
1968
|
+
<span style={{ color: C.textDim, fontWeight: 700 }}>{wf.stepCount}</span> pasos
|
|
1969
|
+
</span>
|
|
1970
|
+
{wf.compensableStepCount > 0 && (
|
|
1971
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>
|
|
1972
|
+
<span style={{ color: C.accent, fontWeight: 700 }}>{wf.compensableStepCount}</span> compensables
|
|
1973
|
+
</span>
|
|
1974
|
+
)}
|
|
1975
|
+
{wf.asyncStepCount > 0 && (
|
|
1976
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>
|
|
1977
|
+
<span style={{ color: C.gold, fontWeight: 700 }}>{wf.asyncStepCount}</span> async (fire-and-forget)
|
|
1978
|
+
</span>
|
|
1979
|
+
)}
|
|
1980
|
+
{wf.taskQueue && (
|
|
1981
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>
|
|
1982
|
+
queue: <code style={{ color: C.purple, fontSize: 10 }}>{wf.taskQueue}</code>
|
|
1983
|
+
</span>
|
|
1984
|
+
)}
|
|
1985
|
+
</div>
|
|
1986
|
+
</div>
|
|
1987
|
+
</div>
|
|
1988
|
+
</div>
|
|
1989
|
+
|
|
1990
|
+
{/* Steps timeline */}
|
|
1991
|
+
<div style={{ marginBottom: 24 }}>
|
|
1992
|
+
{(wf.steps || []).filter(s => !s._isWait).map((step, i) => {
|
|
1993
|
+
const expanded = expandedStep === i;
|
|
1994
|
+
const roleSt = ROLE_STYLE[step.roleClass] || {};
|
|
1995
|
+
const actColor = roleSt.color || C.blue;
|
|
1996
|
+
return (
|
|
1997
|
+
<div key={i} style={{ display: "flex", gap: 14, marginBottom: 6 }}>
|
|
1998
|
+
{/* Timeline */}
|
|
1999
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 40, flexShrink: 0 }}>
|
|
2000
|
+
<div style={{
|
|
2001
|
+
width: 38, height: 38, borderRadius: "50%",
|
|
2002
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
2003
|
+
background: actColor + "22", border: `2px solid ${actColor}55`,
|
|
2004
|
+
color: actColor, fontWeight: 800, fontSize: 13,
|
|
2005
|
+
}}>{step._stepNum}</div>
|
|
2006
|
+
{i < (wf.steps || []).filter(s => !s._isWait).length - 1 && (
|
|
2007
|
+
<div style={{ width: 2, flex: 1, minHeight: 16, background: C.border, marginTop: 3 }} />
|
|
2008
|
+
)}
|
|
2009
|
+
</div>
|
|
2010
|
+
{/* Card */}
|
|
2011
|
+
<div style={{ flex: 1 }}>
|
|
2012
|
+
<div
|
|
2013
|
+
onClick={() => setExpandedStep(expanded ? null : i)}
|
|
2014
|
+
style={{
|
|
2015
|
+
background: expanded ? actColor + "11" : C.surface,
|
|
2016
|
+
border: `1px solid ${expanded ? actColor + "66" : C.border}`,
|
|
2017
|
+
borderRadius: expanded ? "10px 10px 0 0" : 10,
|
|
2018
|
+
padding: "11px 16px", cursor: "pointer", transition: "all 0.15s",
|
|
2019
|
+
}}
|
|
2020
|
+
>
|
|
2021
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
2022
|
+
<span style={{ fontSize: 15 }}>{typeIcon(step.type, step.activityType)}</span>
|
|
2023
|
+
<span style={{ fontWeight: 700, color: C.text, fontSize: 14 }}>{step.activity}</span>
|
|
2024
|
+
<RoleBadge role={step.roleClass} />
|
|
2025
|
+
{step.type === "async" && <Tag color={C.gold}>async</Tag>}
|
|
2026
|
+
{step.activityType === "heavy" && <Tag color={C.orange}>heavy</Tag>}
|
|
2027
|
+
<div style={{ flex: 1 }} />
|
|
2028
|
+
<Tag color={step.targetColor || C.blue}>{step.targetIcon} {step.targetLabel || step.target}</Tag>
|
|
2029
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>{expanded ? "β²" : "βΌ"}</span>
|
|
2030
|
+
</div>
|
|
2031
|
+
{step.roleDesc && (
|
|
2032
|
+
<div style={{ color: C.textMuted, fontSize: 11, marginTop: 5, lineHeight: 1.5 }}>{step.roleDesc}</div>
|
|
2033
|
+
)}
|
|
2034
|
+
</div>
|
|
2035
|
+
{expanded && (
|
|
2036
|
+
<div style={{ border: `1px solid ${actColor}66`, borderTop: "none", borderRadius: "0 0 10px 10px", background: C.bg, padding: 16 }}>
|
|
2037
|
+
<div style={{ display: "flex", gap: 20, flexWrap: "wrap" }}>
|
|
2038
|
+
{/* Inputs */}
|
|
2039
|
+
{step.inputs && step.inputs.length > 0 && (
|
|
2040
|
+
<div style={{ flex: 1, minWidth: 200 }}>
|
|
2041
|
+
<div style={{ color: C.textMuted, fontSize: 10, fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>ENTRADAS</div>
|
|
2042
|
+
{step.inputs.map((inp, j) => (
|
|
2043
|
+
<div key={j} style={{ marginBottom: 4 }}>
|
|
2044
|
+
<code style={{ color: C.blue, fontSize: 11, background: C.surface, padding: "1px 6px", borderRadius: 3 }}>{inp.name}</code>
|
|
2045
|
+
<span style={{ color: C.textMuted, fontSize: 10, marginLeft: 6 }}>β {inp.source}</span>
|
|
2046
|
+
</div>
|
|
2047
|
+
))}
|
|
2048
|
+
</div>
|
|
2049
|
+
)}
|
|
2050
|
+
{/* Outputs */}
|
|
2051
|
+
{step.outputs && step.outputs.length > 0 && (
|
|
2052
|
+
<div style={{ flex: 1, minWidth: 200 }}>
|
|
2053
|
+
<div style={{ color: C.textMuted, fontSize: 10, fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>SALIDAS</div>
|
|
2054
|
+
{step.outputs.map((out, j) => (
|
|
2055
|
+
<div key={j} style={{ marginBottom: 4 }}>
|
|
2056
|
+
<code style={{ color: C.green, fontSize: 11, background: C.surface, padding: "1px 6px", borderRadius: 3 }}>{out.name}</code>
|
|
2057
|
+
{out.consumers && out.consumers.length > 0 && (
|
|
2058
|
+
<span style={{ color: C.textMuted, fontSize: 10, marginLeft: 6 }}>β {out.consumers.join(", ")}</span>
|
|
2059
|
+
)}
|
|
2060
|
+
</div>
|
|
2061
|
+
))}
|
|
2062
|
+
</div>
|
|
2063
|
+
)}
|
|
2064
|
+
{/* Compensation */}
|
|
2065
|
+
{step.compensation && (
|
|
2066
|
+
<div style={{ flex: 1, minWidth: 200 }}>
|
|
2067
|
+
<div style={{ color: C.textMuted, fontSize: 10, fontWeight: 700, letterSpacing: 1, marginBottom: 6 }}>COMPENSACIΓN</div>
|
|
2068
|
+
<code style={{ color: C.accent, fontSize: 11, background: C.surface, padding: "1px 6px", borderRadius: 3 }}>{step.compensation}</code>
|
|
2069
|
+
</div>
|
|
2070
|
+
)}
|
|
2071
|
+
</div>
|
|
2072
|
+
{/* Retry / timeout */}
|
|
2073
|
+
<div style={{ display: "flex", gap: 12, marginTop: 10, flexWrap: "wrap" }}>
|
|
2074
|
+
{step.timeout && (
|
|
2075
|
+
<span style={{ color: C.textMuted, fontSize: 10 }}>
|
|
2076
|
+
β± timeout: <code style={{ color: C.textDim }}>{step.timeout}</code>
|
|
2077
|
+
</span>
|
|
2078
|
+
)}
|
|
2079
|
+
{step.retryPolicy && (
|
|
2080
|
+
<span style={{ color: C.textMuted, fontSize: 10 }}>
|
|
2081
|
+
π retry: max {step.retryPolicy.maximumAttempts || "β"} Β· backoff {step.retryPolicy.initialInterval || "1s"}
|
|
2082
|
+
</span>
|
|
2083
|
+
)}
|
|
2084
|
+
{!step.retryPolicy && step.activityType === "heavy" && (
|
|
2085
|
+
<span style={{ color: C.accent, fontSize: 10 }}>β heavy activity sin retryPolicy declarado</span>
|
|
2086
|
+
)}
|
|
2087
|
+
</div>
|
|
2088
|
+
</div>
|
|
2089
|
+
)}
|
|
2090
|
+
</div>
|
|
2091
|
+
</div>
|
|
2092
|
+
);
|
|
2093
|
+
})}
|
|
2094
|
+
</div>
|
|
2095
|
+
|
|
2096
|
+
{/* Data flow table */}
|
|
2097
|
+
{wf.dataFlowTable && wf.dataFlowTable.length > 0 && (
|
|
2098
|
+
<div style={{ marginTop: 4 }}>
|
|
2099
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>TABLA DE FLUJO DE DATOS</div>
|
|
2100
|
+
<div style={{ overflowX: "auto" }}>
|
|
2101
|
+
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
|
|
2102
|
+
<thead>
|
|
2103
|
+
<tr style={{ background: C.surface }}>
|
|
2104
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, fontWeight: 700, textAlign: "left" }}>Campo</th>
|
|
2105
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.blue, fontWeight: 700, textAlign: "left" }}>Origen</th>
|
|
2106
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.green, fontWeight: 700, textAlign: "left" }}>Consumido por</th>
|
|
2107
|
+
</tr>
|
|
2108
|
+
</thead>
|
|
2109
|
+
<tbody>
|
|
2110
|
+
{wf.dataFlowTable.map((row, i) => (
|
|
2111
|
+
<tr key={i} style={{ background: i % 2 === 0 ? C.bg : C.surface }}>
|
|
2112
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "7px 12px" }}>
|
|
2113
|
+
<code style={{ color: C.text, fontSize: 11 }}>{row.field}</code>
|
|
2114
|
+
</td>
|
|
2115
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "7px 12px", color: C.textDim, fontSize: 11 }}>{row.source}</td>
|
|
2116
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "7px 12px" }}>
|
|
2117
|
+
{row.consumed && row.consumed.length > 0
|
|
2118
|
+
? row.consumed.map((c, j) => (
|
|
2119
|
+
<span key={j} style={{ color: C.textDim, fontSize: 11, marginRight: 6 }}>{c.activity}</span>
|
|
2120
|
+
))
|
|
2121
|
+
: <span style={{ color: C.textMuted, fontSize: 11 }}>β no consumido β</span>
|
|
2122
|
+
}
|
|
2123
|
+
</td>
|
|
2124
|
+
</tr>
|
|
2125
|
+
))}
|
|
2126
|
+
</tbody>
|
|
2127
|
+
</table>
|
|
2128
|
+
</div>
|
|
2129
|
+
</div>
|
|
2130
|
+
)}
|
|
2131
|
+
</div>
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// βββ SagaAnalysisTab ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
2136
|
+
function SagaAnalysisTab() {
|
|
2137
|
+
const sagas = SAGA_WORKFLOWS || [];
|
|
2138
|
+
const [selectedIdx, setSelectedIdx] = useState(0);
|
|
2139
|
+
const [failAtStep, setFailAtStep] = useState(null);
|
|
2140
|
+
const saga = sagas[selectedIdx] || null;
|
|
2141
|
+
|
|
2142
|
+
if (!sagas.length) return (
|
|
2143
|
+
<div style={{ color: C.textMuted, padding: 60, textAlign: "center" }}>
|
|
2144
|
+
<div style={{ fontSize: 40, marginBottom: 16 }}>π</div>
|
|
2145
|
+
No hay workflows con <code style={{ color: C.accent }}>saga: true</code> declarados
|
|
2146
|
+
</div>
|
|
2147
|
+
);
|
|
2148
|
+
|
|
2149
|
+
const chain = saga ? (saga.sagaChain || []) : [];
|
|
2150
|
+
const lifo = [...chain].reverse();
|
|
2151
|
+
|
|
2152
|
+
// Simulate failure: steps after failAtStep get rolled back
|
|
2153
|
+
const isRolledBack = (stepNum) => failAtStep !== null && stepNum > failAtStep;
|
|
2154
|
+
const isFailPoint = (stepNum) => stepNum === failAtStep;
|
|
2155
|
+
const wasExecuted = (stepNum) => failAtStep === null || stepNum <= failAtStep;
|
|
2156
|
+
|
|
2157
|
+
return (
|
|
2158
|
+
<div>
|
|
2159
|
+
{/* Saga selector */}
|
|
2160
|
+
{sagas.length > 1 && (
|
|
2161
|
+
<div style={{ display: "flex", gap: 8, marginBottom: 20, flexWrap: "wrap" }}>
|
|
2162
|
+
{sagas.map((s, i) => (
|
|
2163
|
+
<button key={i} onClick={() => { setSelectedIdx(i); setFailAtStep(null); }} style={{
|
|
2164
|
+
background: i === selectedIdx ? C.accent + "22" : C.surface,
|
|
2165
|
+
border: `1px solid ${i === selectedIdx ? C.accent : C.border}`,
|
|
2166
|
+
color: i === selectedIdx ? C.accent : C.textMuted,
|
|
2167
|
+
borderRadius: 8, padding: "7px 14px", cursor: "pointer",
|
|
2168
|
+
fontWeight: i === selectedIdx ? 700 : 400, fontSize: 12,
|
|
2169
|
+
transition: "all 0.15s", fontFamily: "inherit",
|
|
2170
|
+
}}>π {s.name}</button>
|
|
2171
|
+
))}
|
|
2172
|
+
</div>
|
|
2173
|
+
)}
|
|
2174
|
+
|
|
2175
|
+
{/* Saga header */}
|
|
2176
|
+
<div style={{ background: C.surface, border: `1px solid ${C.accent}44`, borderRadius: 12, padding: 20, marginBottom: 24 }}>
|
|
2177
|
+
<div style={{ fontWeight: 800, fontSize: 18, color: C.text, marginBottom: 8 }}>π {saga.name}</div>
|
|
2178
|
+
<div style={{ display: "flex", gap: 20, flexWrap: "wrap" }}>
|
|
2179
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>
|
|
2180
|
+
<span style={{ color: C.text, fontWeight: 700 }}>{chain.length}</span> pasos en la cadena
|
|
2181
|
+
</span>
|
|
2182
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>
|
|
2183
|
+
<span style={{ color: C.accent, fontWeight: 700 }}>{chain.filter(s => s.isCompensable).length}</span> compensables
|
|
2184
|
+
</span>
|
|
2185
|
+
<span style={{ color: C.textMuted, fontSize: 12 }}>
|
|
2186
|
+
<span style={{ color: C.gold, fontWeight: 700 }}>{chain.filter(s => !s.isCompensable && s.type === "async").length}</span> async (no compensables)
|
|
2187
|
+
</span>
|
|
2188
|
+
</div>
|
|
2189
|
+
</div>
|
|
2190
|
+
|
|
2191
|
+
{/* Fail simulator */}
|
|
2192
|
+
<div style={{ background: C.surface, border: `1px solid ${C.border}`, borderRadius: 10, padding: 16, marginBottom: 24 }}>
|
|
2193
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>
|
|
2194
|
+
π΄ SIMULAR FALLO EN PASO:
|
|
2195
|
+
</div>
|
|
2196
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }}>
|
|
2197
|
+
<button onClick={() => setFailAtStep(null)} style={{
|
|
2198
|
+
background: failAtStep === null ? C.green + "22" : C.bg,
|
|
2199
|
+
border: `1px solid ${failAtStep === null ? C.green : C.border}`,
|
|
2200
|
+
color: failAtStep === null ? C.green : C.textMuted,
|
|
2201
|
+
borderRadius: 6, padding: "5px 12px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
|
|
2202
|
+
}}>β
Sin fallo</button>
|
|
2203
|
+
{chain.filter(s => s.canFail).map((s) => (
|
|
2204
|
+
<button key={s.stepNum} onClick={() => setFailAtStep(s.stepNum)} style={{
|
|
2205
|
+
background: failAtStep === s.stepNum ? C.accent + "22" : C.bg,
|
|
2206
|
+
border: `1px solid ${failAtStep === s.stepNum ? C.accent : C.border}`,
|
|
2207
|
+
color: failAtStep === s.stepNum ? C.accent : C.textMuted,
|
|
2208
|
+
borderRadius: 6, padding: "5px 12px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
|
|
2209
|
+
}}>Paso {s.stepNum}</button>
|
|
2210
|
+
))}
|
|
2211
|
+
</div>
|
|
2212
|
+
{failAtStep !== null && (
|
|
2213
|
+
<div style={{ marginTop: 12, padding: "10px 14px", background: C.accent + "11", border: `1px solid ${C.accent}44`, borderRadius: 8 }}>
|
|
2214
|
+
<div style={{ color: C.accent, fontWeight: 700, fontSize: 12 }}>
|
|
2215
|
+
π₯ Fallo en paso {failAtStep} β Se ejecutan compensaciones LIFO:
|
|
2216
|
+
</div>
|
|
2217
|
+
<div style={{ color: C.textDim, fontSize: 11, marginTop: 4, lineHeight: 1.7 }}>
|
|
2218
|
+
{lifo.filter(s => s.stepNum < failAtStep && s.isCompensable).map((s, i) => (
|
|
2219
|
+
<span key={i}>
|
|
2220
|
+
<code style={{ color: C.accent, background: C.surface, padding: "1px 5px", borderRadius: 3 }}>{s.compensationName}</code>
|
|
2221
|
+
{" en "}<span style={{ color: C.textDim }}>{s.targetModule}</span>
|
|
2222
|
+
{i < lifo.filter(ss => ss.stepNum < failAtStep && ss.isCompensable).length - 1 ? " β " : ""}
|
|
2223
|
+
</span>
|
|
2224
|
+
))}
|
|
2225
|
+
{lifo.filter(s => s.stepNum < failAtStep && s.isCompensable).length === 0 && (
|
|
2226
|
+
<span style={{ color: C.gold }}>β NingΓΊn paso previo tiene compensaciΓ³n declarada</span>
|
|
2227
|
+
)}
|
|
2228
|
+
</div>
|
|
2229
|
+
</div>
|
|
2230
|
+
)}
|
|
2231
|
+
</div>
|
|
2232
|
+
|
|
2233
|
+
{/* Execution chain (forward) */}
|
|
2234
|
+
<div style={{ marginBottom: 20 }}>
|
|
2235
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>
|
|
2236
|
+
βΆ CADENA DE EJECUCIΓN (orden cronolΓ³gico)
|
|
2237
|
+
</div>
|
|
2238
|
+
{chain.map((step, i) => {
|
|
2239
|
+
const rolledBack = isRolledBack(step.stepNum);
|
|
2240
|
+
const failPoint = isFailPoint(step.stepNum);
|
|
2241
|
+
const executed = wasExecuted(step.stepNum);
|
|
2242
|
+
const color = failPoint ? C.accent : rolledBack ? C.textMuted : step.isCompensable ? C.green : C.gold;
|
|
2243
|
+
|
|
2244
|
+
return (
|
|
2245
|
+
<div key={i} style={{ display: "flex", gap: 12, marginBottom: 6, opacity: rolledBack ? 0.35 : 1 }}>
|
|
2246
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 36, flexShrink: 0 }}>
|
|
2247
|
+
<div style={{
|
|
2248
|
+
width: 34, height: 34, borderRadius: "50%",
|
|
2249
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
2250
|
+
background: color + "22", border: `2px solid ${color}55`,
|
|
2251
|
+
color, fontWeight: 800, fontSize: 12,
|
|
2252
|
+
}}>
|
|
2253
|
+
{failPoint ? "π₯" : rolledBack ? "β" : step.stepNum}
|
|
2254
|
+
</div>
|
|
2255
|
+
{i < chain.length - 1 && (
|
|
2256
|
+
<div style={{ width: 2, flex: 1, minHeight: 12, background: C.border, marginTop: 3 }} />
|
|
2257
|
+
)}
|
|
2258
|
+
</div>
|
|
2259
|
+
<div style={{
|
|
2260
|
+
flex: 1, background: failPoint ? C.accent + "11" : C.surface,
|
|
2261
|
+
border: `1px solid ${failPoint ? C.accent : rolledBack ? C.border : color + "44"}`,
|
|
2262
|
+
borderRadius: 8, padding: "10px 14px",
|
|
2263
|
+
}}>
|
|
2264
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
2265
|
+
<span style={{ fontWeight: 700, color: failPoint ? C.accent : C.text, fontSize: 13 }}>
|
|
2266
|
+
{step.activityName}
|
|
2267
|
+
</span>
|
|
2268
|
+
{step.isCompensable && <Tag color={C.green}>compensable</Tag>}
|
|
2269
|
+
{step.type === "async" && !step.isCompensable && <Tag color={C.gold}>async Β· no compensable</Tag>}
|
|
2270
|
+
{failPoint && <Tag color={C.accent}>FALLO AQUΓ</Tag>}
|
|
2271
|
+
<div style={{ flex: 1 }} />
|
|
2272
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>{step.targetModule}</span>
|
|
2273
|
+
{step.timeout && <code style={{ color: C.textMuted, fontSize: 10 }}>β± {step.timeout}</code>}
|
|
2274
|
+
</div>
|
|
2275
|
+
{step.isCompensable && step.compensationName && (
|
|
2276
|
+
<div style={{ marginTop: 5, color: C.textMuted, fontSize: 11 }}>
|
|
2277
|
+
β© compensaciΓ³n: <code style={{ color: C.accent, background: C.bg, padding: "1px 5px", borderRadius: 3 }}>{step.compensationName}</code>
|
|
2278
|
+
</div>
|
|
2279
|
+
)}
|
|
2280
|
+
</div>
|
|
2281
|
+
</div>
|
|
2282
|
+
);
|
|
2283
|
+
})}
|
|
2284
|
+
</div>
|
|
2285
|
+
|
|
2286
|
+
{/* LIFO compensation chain */}
|
|
2287
|
+
<div>
|
|
2288
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>
|
|
2289
|
+
β© CADENA DE COMPENSACIΓN LIFO
|
|
2290
|
+
</div>
|
|
2291
|
+
{lifo.filter(s => s.isCompensable).length === 0 && (
|
|
2292
|
+
<div style={{ color: C.gold, fontSize: 12, padding: 16, background: C.gold + "11", border: `1px solid ${C.gold}44`, borderRadius: 8 }}>
|
|
2293
|
+
β NingΓΊn paso de este workflow tiene compensaciΓ³n declarada. Considera agregar <code>compensation:</code> en cada paso crΓtico de la saga.
|
|
2294
|
+
</div>
|
|
2295
|
+
)}
|
|
2296
|
+
{lifo.filter(s => s.isCompensable).map((step, i, arr) => (
|
|
2297
|
+
<div key={i} style={{ display: "flex", gap: 12, marginBottom: 6 }}>
|
|
2298
|
+
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", width: 36, flexShrink: 0 }}>
|
|
2299
|
+
<div style={{
|
|
2300
|
+
width: 34, height: 34, borderRadius: "50%",
|
|
2301
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
2302
|
+
background: C.accent + "22", border: `2px solid ${C.accent}55`,
|
|
2303
|
+
color: C.accent, fontWeight: 800, fontSize: 12,
|
|
2304
|
+
}}>{i + 1}</div>
|
|
2305
|
+
{i < arr.length - 1 && (
|
|
2306
|
+
<div style={{ width: 2, flex: 1, minHeight: 12, background: C.accent + "33", marginTop: 3 }} />
|
|
2307
|
+
)}
|
|
2308
|
+
</div>
|
|
2309
|
+
<div style={{ flex: 1, background: C.accent + "08", border: `1px solid ${C.accent}33`, borderRadius: 8, padding: "10px 14px" }}>
|
|
2310
|
+
<div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
|
|
2311
|
+
<span style={{ color: C.accent, fontWeight: 700, fontSize: 12 }}>β© {step.compensationName}</span>
|
|
2312
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>en <span style={{ color: C.textDim }}>{step.targetModule}</span></span>
|
|
2313
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>
|
|
2314
|
+
(revierte paso {step.stepNum}: {step.activityName})
|
|
2315
|
+
</span>
|
|
2316
|
+
</div>
|
|
2317
|
+
</div>
|
|
2318
|
+
</div>
|
|
2319
|
+
))}
|
|
2320
|
+
</div>
|
|
2321
|
+
</div>
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// βββ ActivityCatalogTab βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
2326
|
+
function ActivityCatalogTab() {
|
|
2327
|
+
const catalog = ACTIVITY_CATALOG || [];
|
|
2328
|
+
const [roleFilter, setRoleFilter] = useState("ALL");
|
|
2329
|
+
const [moduleFilter, setModuleFilter] = useState("ALL");
|
|
2330
|
+
const [showOrphans, setShowOrphans] = useState(false);
|
|
2331
|
+
|
|
2332
|
+
const modules = [...new Set(catalog.map(a => a.module))].sort();
|
|
2333
|
+
const roles = ["ALL", "READ", "WRITE", "COMPENSATE", "NOTIFY"];
|
|
2334
|
+
|
|
2335
|
+
const filtered = catalog.filter(a => {
|
|
2336
|
+
if (roleFilter !== "ALL" && a.role !== roleFilter) return false;
|
|
2337
|
+
if (moduleFilter !== "ALL" && a.module !== moduleFilter) return false;
|
|
2338
|
+
if (showOrphans && !a.isOrphan) return false;
|
|
2339
|
+
return true;
|
|
2340
|
+
});
|
|
2341
|
+
|
|
2342
|
+
const orphanCount = catalog.filter(a => a.isOrphan).length;
|
|
2343
|
+
|
|
2344
|
+
return (
|
|
2345
|
+
<div>
|
|
2346
|
+
{/* Stats bar */}
|
|
2347
|
+
<div style={{ display: "flex", gap: 12, marginBottom: 20, flexWrap: "wrap" }}>
|
|
2348
|
+
{[
|
|
2349
|
+
{ label: "Total", count: catalog.length, color: C.text },
|
|
2350
|
+
{ label: "READ", count: catalog.filter(a => a.role === "READ").length, color: C.blue },
|
|
2351
|
+
{ label: "WRITE", count: catalog.filter(a => a.role === "WRITE").length, color: C.green },
|
|
2352
|
+
{ label: "COMPENSATE", count: catalog.filter(a => a.role === "COMPENSATE").length, color: C.accent },
|
|
2353
|
+
{ label: "NOTIFY", count: catalog.filter(a => a.role === "NOTIFY").length, color: C.gold },
|
|
2354
|
+
{ label: "Orphans", count: orphanCount, color: C.orange },
|
|
2355
|
+
].map(stat => (
|
|
2356
|
+
<div key={stat.label} style={{
|
|
2357
|
+
background: C.surface, border: `1px solid ${C.border}`,
|
|
2358
|
+
borderRadius: 10, padding: "12px 18px", textAlign: "center", minWidth: 90,
|
|
2359
|
+
}}>
|
|
2360
|
+
<div style={{ fontSize: 24, fontWeight: 900, color: stat.color, fontFamily: "monospace" }}>{stat.count}</div>
|
|
2361
|
+
<div style={{ color: C.textMuted, fontSize: 10, marginTop: 2, fontWeight: 700, letterSpacing: 0.5 }}>{stat.label}</div>
|
|
2362
|
+
</div>
|
|
2363
|
+
))}
|
|
2364
|
+
</div>
|
|
2365
|
+
|
|
2366
|
+
{/* Filters */}
|
|
2367
|
+
<div style={{ display: "flex", gap: 10, marginBottom: 16, flexWrap: "wrap", alignItems: "center" }}>
|
|
2368
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>Rol:</span>
|
|
2369
|
+
{roles.map(r => (
|
|
2370
|
+
<button key={r} onClick={() => setRoleFilter(r)} style={{
|
|
2371
|
+
background: roleFilter === r ? (ROLE_STYLE[r] || { color: C.text }).color + "22" : C.surface,
|
|
2372
|
+
border: `1px solid ${roleFilter === r ? (ROLE_STYLE[r] || { color: C.border }).color : C.border}`,
|
|
2373
|
+
color: roleFilter === r ? (ROLE_STYLE[r] || { color: C.text }).color : C.textMuted,
|
|
2374
|
+
borderRadius: 6, padding: "4px 10px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
|
|
2375
|
+
}}>{r}</button>
|
|
2376
|
+
))}
|
|
2377
|
+
<span style={{ color: C.border }}>|</span>
|
|
2378
|
+
<span style={{ color: C.textMuted, fontSize: 11 }}>MΓ³dulo:</span>
|
|
2379
|
+
<select value={moduleFilter} onChange={e => setModuleFilter(e.target.value)} style={{
|
|
2380
|
+
background: C.surface, border: `1px solid ${C.border}`, color: C.textDim,
|
|
2381
|
+
borderRadius: 6, padding: "4px 10px", fontSize: 11, fontFamily: "inherit",
|
|
2382
|
+
}}>
|
|
2383
|
+
<option value="ALL">Todos</option>
|
|
2384
|
+
{modules.map(m => <option key={m} value={m}>{m}</option>)}
|
|
2385
|
+
</select>
|
|
2386
|
+
<span style={{ color: C.border }}>|</span>
|
|
2387
|
+
<button onClick={() => setShowOrphans(!showOrphans)} style={{
|
|
2388
|
+
background: showOrphans ? C.orange + "22" : C.surface,
|
|
2389
|
+
border: `1px solid ${showOrphans ? C.orange : C.border}`,
|
|
2390
|
+
color: showOrphans ? C.orange : C.textMuted,
|
|
2391
|
+
borderRadius: 6, padding: "4px 10px", cursor: "pointer", fontSize: 11, fontFamily: "inherit",
|
|
2392
|
+
}}>β Solo orphans ({orphanCount})</button>
|
|
2393
|
+
</div>
|
|
2394
|
+
|
|
2395
|
+
{/* Table */}
|
|
2396
|
+
<div style={{ overflowX: "auto" }}>
|
|
2397
|
+
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 12 }}>
|
|
2398
|
+
<thead>
|
|
2399
|
+
<tr style={{ background: C.surface }}>
|
|
2400
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Actividad</th>
|
|
2401
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>MΓ³dulo</th>
|
|
2402
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Rol</th>
|
|
2403
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Tipo</th>
|
|
2404
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>DescripciΓ³n</th>
|
|
2405
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>Usado en</th>
|
|
2406
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "center" }}>Retry</th>
|
|
2407
|
+
</tr>
|
|
2408
|
+
</thead>
|
|
2409
|
+
<tbody>
|
|
2410
|
+
{filtered.map((act, i) => {
|
|
2411
|
+
const roleSt = ROLE_STYLE[act.role] || { color: C.textMuted };
|
|
2412
|
+
return (
|
|
2413
|
+
<tr key={i} style={{ background: i % 2 === 0 ? C.bg : C.surface }}>
|
|
2414
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2415
|
+
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
2416
|
+
<code style={{ color: C.text, fontSize: 12 }}>{act.name}</code>
|
|
2417
|
+
{act.isOrphan && <span title="No usado en ningΓΊn workflow" style={{ color: C.orange, fontSize: 10 }}>β </span>}
|
|
2418
|
+
</div>
|
|
2419
|
+
</td>
|
|
2420
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textDim, fontSize: 11 }}>{act.moduleLabel || act.module}</td>
|
|
2421
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2422
|
+
<RoleBadge role={act.role} small />
|
|
2423
|
+
</td>
|
|
2424
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2425
|
+
<Tag color={act.type === "heavy" ? C.orange : C.textMuted}>{act.type}</Tag>
|
|
2426
|
+
</td>
|
|
2427
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, fontSize: 11, maxWidth: 280, lineHeight: 1.5 }}>
|
|
2428
|
+
{act.description}
|
|
2429
|
+
</td>
|
|
2430
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2431
|
+
{act.usedInWorkflows && act.usedInWorkflows.length > 0
|
|
2432
|
+
? act.usedInWorkflows.map((w, j) => (
|
|
2433
|
+
<div key={j} style={{ color: C.purple, fontSize: 10, marginBottom: 2 }}>{w}</div>
|
|
2434
|
+
))
|
|
2435
|
+
: <span style={{ color: C.orange, fontSize: 11 }}>β orphan</span>
|
|
2436
|
+
}
|
|
2437
|
+
</td>
|
|
2438
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", textAlign: "center" }}>
|
|
2439
|
+
{act.hasRetryPolicy
|
|
2440
|
+
? <span style={{ color: C.green, fontSize: 14 }} title={`max: ${act.retryPolicy?.maximumAttempts}`}>β</span>
|
|
2441
|
+
: act.type === "heavy"
|
|
2442
|
+
? <span style={{ color: C.accent, fontSize: 14 }} title="Heavy activity sin retryPolicy">β </span>
|
|
2443
|
+
: <span style={{ color: C.border, fontSize: 12 }}>β</span>
|
|
2444
|
+
}
|
|
2445
|
+
</td>
|
|
2446
|
+
</tr>
|
|
2447
|
+
);
|
|
2448
|
+
})}
|
|
2449
|
+
</tbody>
|
|
2450
|
+
</table>
|
|
2451
|
+
{filtered.length === 0 && (
|
|
2452
|
+
<div style={{ color: C.textMuted, padding: 20, textAlign: "center", fontSize: 12 }}>Sin resultados para los filtros seleccionados</div>
|
|
2453
|
+
)}
|
|
2454
|
+
</div>
|
|
2455
|
+
|
|
2456
|
+
{/* External type deps */}
|
|
2457
|
+
{EXTERNAL_TYPE_DEPS && EXTERNAL_TYPE_DEPS.length > 0 && (
|
|
2458
|
+
<div style={{ marginTop: 28 }}>
|
|
2459
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>
|
|
2460
|
+
DEPENDENCIAS DE TIPOS EXTERNOS
|
|
2461
|
+
</div>
|
|
2462
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
2463
|
+
{EXTERNAL_TYPE_DEPS.map((dep, i) => (
|
|
2464
|
+
<div key={i} style={{
|
|
2465
|
+
background: C.surface, border: `1px solid ${C.purple}44`, borderRadius: 8,
|
|
2466
|
+
padding: "8px 14px", fontSize: 11,
|
|
2467
|
+
}}>
|
|
2468
|
+
<span style={{ color: C.text, fontWeight: 700 }}>{dep.consumerModule}</span>
|
|
2469
|
+
<span style={{ color: C.textMuted }}>.</span>
|
|
2470
|
+
<span style={{ color: C.blue }}>{dep.activityName}</span>
|
|
2471
|
+
<span style={{ color: C.textMuted }}> usa </span>
|
|
2472
|
+
<code style={{ color: C.purple }}>{dep.typeName}</code>
|
|
2473
|
+
<span style={{ color: C.textMuted }}> de </span>
|
|
2474
|
+
<span style={{ color: C.green }}>{dep.sourceModule}</span>
|
|
2475
|
+
</div>
|
|
2476
|
+
))}
|
|
2477
|
+
</div>
|
|
2478
|
+
</div>
|
|
2479
|
+
)}
|
|
2480
|
+
</div>
|
|
2481
|
+
);
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
// βββ TemporalArchitectureTab βββββββββββββββββββββββββββββββββββββββββββββ
|
|
2485
|
+
function TemporalArchitectureTab() {
|
|
2486
|
+
const roles = Object.values(MODULE_ROLES || {});
|
|
2487
|
+
const queues = Object.values(QUEUE_TOPOLOGY || {});
|
|
2488
|
+
|
|
2489
|
+
return (
|
|
2490
|
+
<div>
|
|
2491
|
+
{/* Orchestration info */}
|
|
2492
|
+
{TEMPORAL_ORCHESTRATION && (
|
|
2493
|
+
<div style={{ background: C.surface, border: `1px solid ${C.purple}44`, borderRadius: 12, padding: 20, marginBottom: 24 }}>
|
|
2494
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 10 }}>TEMPORAL SERVER</div>
|
|
2495
|
+
<div style={{ display: "flex", gap: 24, flexWrap: "wrap" }}>
|
|
2496
|
+
<span style={{ fontSize: 12 }}>
|
|
2497
|
+
<span style={{ color: C.textMuted }}>target: </span>
|
|
2498
|
+
<code style={{ color: C.purple }}>{TEMPORAL_ORCHESTRATION.target}</code>
|
|
2499
|
+
</span>
|
|
2500
|
+
<span style={{ fontSize: 12 }}>
|
|
2501
|
+
<span style={{ color: C.textMuted }}>namespace: </span>
|
|
2502
|
+
<code style={{ color: C.green }}>{TEMPORAL_ORCHESTRATION.namespace}</code>
|
|
2503
|
+
</span>
|
|
2504
|
+
</div>
|
|
2505
|
+
</div>
|
|
2506
|
+
)}
|
|
2507
|
+
|
|
2508
|
+
{/* Module roles grid */}
|
|
2509
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>ROL POR MΓDULO</div>
|
|
2510
|
+
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 12, marginBottom: 28 }}>
|
|
2511
|
+
{roles.map((mod, i) => {
|
|
2512
|
+
const rs = PRIMARY_ROLE_STYLE[mod.primaryRole] || PRIMARY_ROLE_STYLE["Standalone"];
|
|
2513
|
+
return (
|
|
2514
|
+
<div key={i} style={{
|
|
2515
|
+
background: C.surface, border: `1px solid ${rs.color}44`,
|
|
2516
|
+
borderRadius: 12, padding: 16,
|
|
2517
|
+
boxShadow: `0 0 16px ${rs.color}12`,
|
|
2518
|
+
}}>
|
|
2519
|
+
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 8 }}>
|
|
2520
|
+
<span style={{ fontSize: 22 }}>{rs.icon}</span>
|
|
2521
|
+
<div>
|
|
2522
|
+
<div style={{ fontWeight: 700, color: C.text, fontSize: 14 }}>{mod.label || mod.name}</div>
|
|
2523
|
+
<div style={{ color: rs.color, fontSize: 10, fontWeight: 700, letterSpacing: 0.5 }}>{mod.roleLabel}</div>
|
|
2524
|
+
</div>
|
|
2525
|
+
</div>
|
|
2526
|
+
<div style={{ display: "flex", gap: 6, flexWrap: "wrap" }}>
|
|
2527
|
+
{(mod.roles || []).map((r, j) => {
|
|
2528
|
+
const rrs = PRIMARY_ROLE_STYLE[r] || {};
|
|
2529
|
+
return (
|
|
2530
|
+
<span key={j} style={{
|
|
2531
|
+
background: (rrs.color || C.textMuted) + "22", color: rrs.color || C.textMuted,
|
|
2532
|
+
border: `1px solid ${(rrs.color || C.textMuted)}44`,
|
|
2533
|
+
borderRadius: 4, padding: "1px 7px", fontSize: 10, fontWeight: 700,
|
|
2534
|
+
}}>{r}</span>
|
|
2535
|
+
);
|
|
2536
|
+
})}
|
|
2537
|
+
</div>
|
|
2538
|
+
</div>
|
|
2539
|
+
);
|
|
2540
|
+
})}
|
|
2541
|
+
</div>
|
|
2542
|
+
|
|
2543
|
+
{/* Queue topology */}
|
|
2544
|
+
{queues.length > 0 && (
|
|
2545
|
+
<div>
|
|
2546
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>TASK QUEUES POR MΓDULO</div>
|
|
2547
|
+
<div style={{ overflowX: "auto" }}>
|
|
2548
|
+
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: 11 }}>
|
|
2549
|
+
<thead>
|
|
2550
|
+
<tr style={{ background: C.surface }}>
|
|
2551
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.textMuted, textAlign: "left" }}>MΓ³dulo</th>
|
|
2552
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.purple, textAlign: "left" }}>Workflow Queue</th>
|
|
2553
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.orange, textAlign: "left" }}>Heavy Task Queue</th>
|
|
2554
|
+
<th style={{ border: `1px solid ${C.border}`, padding: "8px 12px", color: C.green, textAlign: "left" }}>Light Task Queue</th>
|
|
2555
|
+
</tr>
|
|
2556
|
+
</thead>
|
|
2557
|
+
<tbody>
|
|
2558
|
+
{queues.map((q, i) => (
|
|
2559
|
+
<tr key={i} style={{ background: i % 2 === 0 ? C.bg : C.surface }}>
|
|
2560
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px", fontWeight: 700, color: C.text }}>{q.label || q.name}</td>
|
|
2561
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2562
|
+
<code style={{ color: C.purple, fontSize: 10 }}>{q.flowQueue}</code>
|
|
2563
|
+
</td>
|
|
2564
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2565
|
+
<code style={{ color: C.orange, fontSize: 10 }}>{q.heavyQueue}</code>
|
|
2566
|
+
</td>
|
|
2567
|
+
<td style={{ border: `1px solid ${C.border}`, padding: "8px 12px" }}>
|
|
2568
|
+
<code style={{ color: C.green, fontSize: 10 }}>{q.lightQueue}</code>
|
|
2569
|
+
</td>
|
|
2570
|
+
</tr>
|
|
2571
|
+
))}
|
|
2572
|
+
</tbody>
|
|
2573
|
+
</table>
|
|
2574
|
+
</div>
|
|
2575
|
+
</div>
|
|
2576
|
+
)}
|
|
2577
|
+
|
|
2578
|
+
{/* Cross-workflow module interaction matrix */}
|
|
2579
|
+
<div style={{ marginTop: 28 }}>
|
|
2580
|
+
<div style={{ color: C.textMuted, fontSize: 11, fontWeight: 700, letterSpacing: 1, marginBottom: 12 }}>PARTICIPACIΓN EN WORKFLOWS</div>
|
|
2581
|
+
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
|
2582
|
+
{(TEMPORAL_WORKFLOWS || []).map((wf, i) => (
|
|
2583
|
+
<div key={i} style={{
|
|
2584
|
+
background: C.surface, border: `1px solid ${C.purple}33`, borderRadius: 10,
|
|
2585
|
+
padding: "12px 16px", minWidth: 200,
|
|
2586
|
+
}}>
|
|
2587
|
+
<div style={{ fontWeight: 700, color: C.purple, fontSize: 13, marginBottom: 8 }}>
|
|
2588
|
+
{wf.saga ? "π " : "β±οΈ "}{wf.name}
|
|
2589
|
+
</div>
|
|
2590
|
+
{[...new Set((wf.steps || []).filter(s => !s._isWait).map(s => s.target || wf.triggerModule))].map((mod, j) => {
|
|
2591
|
+
const rs = PRIMARY_ROLE_STYLE[(MODULE_ROLES[mod] || {}).primaryRole] || { color: C.textMuted, icon: "β" };
|
|
2592
|
+
return (
|
|
2593
|
+
<div key={j} style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 3 }}>
|
|
2594
|
+
<span style={{ fontSize: 12 }}>{rs.icon}</span>
|
|
2595
|
+
<span style={{ color: C.textDim, fontSize: 11 }}>{(MODULE_ROLES[mod] || {}).label || mod}</span>
|
|
2596
|
+
</div>
|
|
2597
|
+
);
|
|
2598
|
+
})}
|
|
2599
|
+
</div>
|
|
2600
|
+
))}
|
|
2601
|
+
</div>
|
|
2602
|
+
</div>
|
|
2603
|
+
</div>
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
1279
2607
|
// βββ App root βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1280
2608
|
function App() {
|
|
1281
2609
|
const [tab, setTab] = useState("validation");
|
|
1282
2610
|
|
|
1283
|
-
const tabs =
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
2611
|
+
const tabs = IS_TEMPORAL_MODE
|
|
2612
|
+
? [
|
|
2613
|
+
{ id: "validation", label: "ValidaciΓ³n", icon: "π" },
|
|
2614
|
+
{ id: "workflows", label: "Workflows", icon: "β±οΈ" },
|
|
2615
|
+
...(SAGA_WORKFLOWS && SAGA_WORKFLOWS.length > 0 ? [{ id: "saga", label: "AnΓ‘lisis Saga", icon: "π" }] : []),
|
|
2616
|
+
{ id: "activities", label: "Actividades", icon: "ποΈ" },
|
|
2617
|
+
{ id: "architecture", label: "Arquitectura", icon: "πΊοΈ" },
|
|
2618
|
+
{ id: "diagram", label: "Diagrama", icon: "β" },
|
|
2619
|
+
...(DOMAIN_VALIDATION ? [{ id: "domain", label: "Dominio", icon: "ποΈ" }] : []),
|
|
2620
|
+
]
|
|
2621
|
+
: [
|
|
2622
|
+
{ id: "validation", label: "ValidaciΓ³n", icon: "π" },
|
|
2623
|
+
{ id: "flows", label: "Simulador de flujos", icon: "βΆ" },
|
|
2624
|
+
{ id: "architecture", label: "Arquitectura", icon: "πΊοΈ" },
|
|
2625
|
+
{ id: "diagram", label: "Diagrama", icon: "β" },
|
|
2626
|
+
...(DOMAIN_VALIDATION ? [{ id: "domain", label: "Dominio", icon: "ποΈ" }] : []),
|
|
2627
|
+
];
|
|
1290
2628
|
|
|
1291
2629
|
const sys = window.__EVA_DATA__;
|
|
1292
|
-
const tech = [];
|
|
1293
|
-
// Detect tech from systemConfig embedded in data
|
|
1294
|
-
if (sys.events && sys.events.length > 0) tech.push({ label: "Kafka", color: C.gold });
|
|
1295
2630
|
|
|
1296
2631
|
return (
|
|
1297
2632
|
<div style={{ background: C.bg, minHeight: "100vh", color: C.text, fontFamily: "'Plus Jakarta Sans', system-ui, -apple-system, sans-serif" }}>
|
|
@@ -1307,11 +2642,26 @@
|
|
|
1307
2642
|
<div style={{ fontWeight: 700, fontSize: 16, color: C.text }}>{systemName}</div>
|
|
1308
2643
|
<div style={{ flex: 1 }} />
|
|
1309
2644
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
1310
|
-
{
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
2645
|
+
{IS_TEMPORAL_MODE ? (
|
|
2646
|
+
<>
|
|
2647
|
+
<Tag color={C.purple}>β±οΈ Temporal</Tag>
|
|
2648
|
+
{TEMPORAL_ORCHESTRATION && (
|
|
2649
|
+
<Tag color={C.textMuted}>{TEMPORAL_ORCHESTRATION.namespace}</Tag>
|
|
2650
|
+
)}
|
|
2651
|
+
{TEMPORAL_WORKFLOWS && <Tag color={C.blue}>{TEMPORAL_WORKFLOWS.length} workflows</Tag>}
|
|
2652
|
+
{SAGA_WORKFLOWS && SAGA_WORKFLOWS.length > 0 && (
|
|
2653
|
+
<Tag color={C.accent}>π {SAGA_WORKFLOWS.length} sagas</Tag>
|
|
2654
|
+
)}
|
|
2655
|
+
</>
|
|
2656
|
+
) : (
|
|
2657
|
+
<>
|
|
2658
|
+
{sys.events && sys.events.length > 0 && (
|
|
2659
|
+
<Tag color={C.gold}>Kafka Β· {sys.events.length} events</Tag>
|
|
2660
|
+
)}
|
|
2661
|
+
{sys.syncIntegrations && sys.syncIntegrations.length > 0 && (
|
|
2662
|
+
<Tag color={C.blue}>Sync Β· {sys.syncIntegrations.length} ports</Tag>
|
|
2663
|
+
)}
|
|
2664
|
+
</>
|
|
1315
2665
|
)}
|
|
1316
2666
|
<Tag color={C.purple}>{sys.modules.length} modules</Tag>
|
|
1317
2667
|
</div>
|
|
@@ -1346,8 +2696,15 @@
|
|
|
1346
2696
|
{/* Tab content */}
|
|
1347
2697
|
<div style={{ maxWidth: 1100, margin: "0 auto", padding: "28px 24px" }}>
|
|
1348
2698
|
{tab === "validation" && <ValidationTab />}
|
|
1349
|
-
{
|
|
1350
|
-
{tab === "
|
|
2699
|
+
{/* Temporal-specific tabs */}
|
|
2700
|
+
{IS_TEMPORAL_MODE && tab === "workflows" && <WorkflowsTab />}
|
|
2701
|
+
{IS_TEMPORAL_MODE && tab === "saga" && <SagaAnalysisTab />}
|
|
2702
|
+
{IS_TEMPORAL_MODE && tab === "activities" && <ActivityCatalogTab />}
|
|
2703
|
+
{IS_TEMPORAL_MODE && tab === "architecture" && <TemporalArchitectureTab />}
|
|
2704
|
+
{/* Broker-mode tabs */}
|
|
2705
|
+
{!IS_TEMPORAL_MODE && tab === "flows" && <FlowSimulator />}
|
|
2706
|
+
{!IS_TEMPORAL_MODE && tab === "architecture" && <ArchitectureTab />}
|
|
2707
|
+
{/* Shared tabs */}
|
|
1351
2708
|
{tab === "diagram" && <DiagramTab />}
|
|
1352
2709
|
{tab === "domain" && DOMAIN_VALIDATION && <DomainTab />}
|
|
1353
2710
|
</div>
|