feat(all-deployment)
This commit is contained in:
1 parent
e261394133
commit
4f0020bc10
39 files changed
+13182
-513
No files matched your search
@@ -0,0 +1,226 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
type: 'main' | 'side';
|
||||
desc: string;
|
||||
}
|
||||
|
||||
interface Edge {
|
||||
from: string;
|
||||
to: string;
|
||||
type: 'main' | 'side';
|
||||
}
|
||||
|
||||
const NODES: Node[] = [
|
||||
{ id: 'frontend', label: 'FRONTEND', x: 80, y: 16, w: 160, h: 38, type: 'main', desc: 'Portales, apps y paneles:interfaces donde el usuario opera' },
|
||||
{ id: 'api', label: 'API GATEWAY', x: 80, y: 108, w: 160, h: 38, type: 'main', desc: 'Seguridad, ruteo, rate limiting y versionado unificados' },
|
||||
{ id: 'logic', label: 'LÓGICA DE NEGOCIO', x: 80, y: 200, w: 160, h: 38, type: 'main', desc: 'Reglas de negocio, validaciones, procesamiento y orquestación' },
|
||||
{ id: 'db', label: 'BASE DE DATOS', x: 80, y: 292, w: 160, h: 38, type: 'main', desc: 'Persistencia diseñada para tu modelo:SQL, NoSQL, caché' },
|
||||
{ id: 'infra', label: 'INFRAESTRUCTURA', x: 80, y: 384, w: 160, h: 36, type: 'main', desc: 'Nube, contenedores, CI/CD y observabilidad operacional' },
|
||||
{ id: 'payments', label: 'PAGOS', x: 8, y: 172, w: 62, h: 28, type: 'side', desc: 'Stripe, Conekta:flujo de cobro integrado' },
|
||||
{ id: 'erp', label: 'ERP / CRM', x: 8, y: 222, w: 62, h: 28, type: 'side', desc: 'SAP, Odoo:sincronización bidireccional' },
|
||||
{ id: 'auth', label: 'ACCESO', x: 250, y: 84, w: 62, h: 28, type: 'side', desc: 'SSO, JWT, OAuth 2.0:identidad y permisos por rol' },
|
||||
{ id: 'external', label: 'EXTERNOS', x: 250, y: 134, w: 62, h: 28, type: 'side', desc: 'APIs de terceros:logística, notificaciones, reportes' },
|
||||
];
|
||||
|
||||
const EDGES: Edge[] = [
|
||||
{ from: 'frontend', to: 'api', type: 'main' },
|
||||
{ from: 'api', to: 'logic', type: 'main' },
|
||||
{ from: 'logic', to: 'db', type: 'main' },
|
||||
{ from: 'db', to: 'infra', type: 'main' },
|
||||
{ from: 'payments', to: 'logic', type: 'side' },
|
||||
{ from: 'erp', to: 'logic', type: 'side' },
|
||||
{ from: 'auth', to: 'api', type: 'side' },
|
||||
{ from: 'external', to: 'api', type: 'side' },
|
||||
];
|
||||
|
||||
function nodeCenter(n: Node) {
|
||||
return { cx: n.x + n.w / 2, cy: n.y + n.h / 2 };
|
||||
}
|
||||
|
||||
function edgeLine(edge: Edge): { x1: number; y1: number; x2: number; y2: number } {
|
||||
const from = NODES.find(n => n.id === edge.from)!;
|
||||
const to = NODES.find(n => n.id === edge.to)!;
|
||||
const fc = nodeCenter(from);
|
||||
const tc = nodeCenter(to);
|
||||
if (edge.type === 'main') {
|
||||
return { x1: fc.cx, y1: from.y + from.h, x2: tc.cx, y2: to.y };
|
||||
}
|
||||
if (from.x < to.x) {
|
||||
return { x1: from.x + from.w, y1: fc.cy, x2: to.x, y2: tc.cy };
|
||||
}
|
||||
return { x1: from.x, y1: fc.cy, x2: to.x + to.w, y2: tc.cy };
|
||||
}
|
||||
|
||||
function isConnected(nodeId: string, activeId: string | null) {
|
||||
if (!activeId || nodeId === activeId) return false;
|
||||
return EDGES.some(e =>
|
||||
(e.from === activeId && e.to === nodeId) ||
|
||||
(e.to === activeId && e.from === nodeId),
|
||||
);
|
||||
}
|
||||
|
||||
function isEdgeActive(edge: Edge, activeId: string | null) {
|
||||
if (!activeId) return false;
|
||||
return edge.from === activeId || edge.to === activeId;
|
||||
}
|
||||
|
||||
const TOOLTIP_W = 172;
|
||||
const TAIL_H = 9;
|
||||
const TAIL_W = 14;
|
||||
|
||||
function tooltipHeight(desc: string): number {
|
||||
const charsPerLine = Math.floor((TOOLTIP_W - 26) / 6.3); // ~aprox chars por línea a 10.5px
|
||||
const lines = Math.ceil(desc.length / charsPerLine);
|
||||
return 14 + 16 + lines * 17 + 14; // paddingTop + label + líneas + paddingBottom
|
||||
}
|
||||
|
||||
export function SystemMap({ isNavyBg = true }: { isNavyBg?: boolean }) {
|
||||
const [active, setActive] = useState<string | null>(null);
|
||||
|
||||
const RED = '#e50914';
|
||||
const dimStroke = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.12)';
|
||||
const defStroke = isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,31,91,0.3)';
|
||||
const connStroke = 'rgba(229,9,20,0.45)';
|
||||
const descText = isNavyBg ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.65)';
|
||||
const tooltipBg = isNavyBg ? '#0a1628' : '#ffffff';
|
||||
const tooltipBorder = isNavyBg ? 'rgba(255,255,255,0.12)' : 'rgba(0,31,91,0.15)';
|
||||
void tooltipBorder;
|
||||
const activeDesc = NODES.find(n => n.id === active);
|
||||
|
||||
return (
|
||||
<div className="w-full" style={{ overflow: 'visible' }}>
|
||||
<svg
|
||||
viewBox="0 0 320 434"
|
||||
width="100%"
|
||||
aria-label="Mapa de sistema interactivo:hover para explorar capas"
|
||||
role="img"
|
||||
style={{ maxWidth: 340, display: 'block', margin: '0 auto', overflow: 'visible' }}
|
||||
onMouseLeave={() => setActive(null)}
|
||||
>
|
||||
<defs>
|
||||
<filter id="bubble-shadow" x="-15%" y="-15%" width="130%" height="130%">
|
||||
<feDropShadow dx="0" dy="3" stdDeviation="4" floodColor="rgba(0,0,0,0.18)" />
|
||||
</filter>
|
||||
</defs>
|
||||
{/* Edges */}
|
||||
{EDGES.map((edge, i) => {
|
||||
const { x1, y1, x2, y2 } = edgeLine(edge);
|
||||
const edgeActive = isEdgeActive(edge, active);
|
||||
return (
|
||||
<line
|
||||
key={i}
|
||||
x1={x1} y1={y1} x2={x2} y2={y2}
|
||||
stroke={edgeActive ? RED : edge.type === 'side' ? connStroke : defStroke}
|
||||
strokeWidth={edgeActive ? 1.5 : 1}
|
||||
strokeDasharray={edge.type === 'side' ? '3 3' : undefined}
|
||||
style={{ transition: 'stroke 200ms, stroke-width 200ms' }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Nodes */}
|
||||
{NODES.map(node => {
|
||||
const isActive = active === node.id;
|
||||
const isConn = isConnected(node.id, active);
|
||||
const isDim = active !== null && !isActive && !isConn;
|
||||
const stroke = isActive ? RED : isConn ? 'rgba(229,9,20,0.5)' : dimStroke;
|
||||
const fillOpacity = isActive ? 0.12 : 0;
|
||||
const textColor = isActive ? RED : isDim
|
||||
? (isNavyBg ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.2)')
|
||||
: (isNavyBg ? 'rgba(255,255,255,0.65)' : 'rgba(0,31,91,0.75)');
|
||||
const { cx } = nodeCenter(node);
|
||||
|
||||
return (
|
||||
<g
|
||||
key={node.id}
|
||||
onMouseEnter={() => setActive(node.id)}
|
||||
onFocus={() => setActive(node.id)}
|
||||
onBlur={() => setActive(null)}
|
||||
style={{ cursor: 'default', outline: 'none' }}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`${node.label}: ${node.desc}`}
|
||||
>
|
||||
<rect
|
||||
x={node.x} y={node.y}
|
||||
width={node.w} height={node.h}
|
||||
fill={`rgba(229,9,20,${fillOpacity})`}
|
||||
stroke={stroke}
|
||||
strokeWidth={isActive ? 1.5 : 1}
|
||||
rx={2}
|
||||
style={{ transition: 'fill 200ms, stroke 200ms' }}
|
||||
/>
|
||||
<text
|
||||
x={cx}
|
||||
y={node.y + node.h / 2 + 1}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fill={textColor}
|
||||
fontFamily="'SF Mono','Fira Code',Consolas,monospace"
|
||||
fontSize={node.type === 'main' ? 10 : 8}
|
||||
fontWeight={600}
|
||||
letterSpacing={node.type === 'main' ? 1.2 : 0.8}
|
||||
style={{ transition: 'fill 200ms', pointerEvents: 'none' }}
|
||||
>
|
||||
{node.label}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Tooltip burbuja estilo chat */}
|
||||
{activeDesc && (() => {
|
||||
const node = NODES.find(n => n.id === active)!;
|
||||
const TOOLTIP_H = tooltipHeight(activeDesc.desc);
|
||||
const cy = node.y + node.h / 2;
|
||||
|
||||
// nodos izquierda → burbuja a la derecha
|
||||
// nodos derecha → burbuja a la izquierda
|
||||
// nodos centro → burbuja a la derecha
|
||||
const showLeft = node.x > 160; // solo los nodos de la derecha van a la izquierda
|
||||
|
||||
let tx: number, ty: number, tailPoints: string;
|
||||
|
||||
if (showLeft) {
|
||||
tx = node.x - TOOLTIP_W - TAIL_H - 4;
|
||||
ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H);
|
||||
const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16);
|
||||
tailPoints = `${tx + TOOLTIP_W},${tailCy - TAIL_W / 2} ${tx + TOOLTIP_W},${tailCy + TAIL_W / 2} ${tx + TOOLTIP_W + TAIL_H},${tailCy}`;
|
||||
} else {
|
||||
tx = node.x + node.w + TAIL_H + 4;
|
||||
ty = Math.min(Math.max(cy - TOOLTIP_H / 2, 4), 430 - TOOLTIP_H);
|
||||
const tailCy = Math.min(Math.max(cy, ty + 16), ty + TOOLTIP_H - 16);
|
||||
tailPoints = `${tx},${tailCy - TAIL_W / 2} ${tx},${tailCy + TAIL_W / 2} ${tx - TAIL_H},${tailCy}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<g style={{ pointerEvents: 'none' }}>
|
||||
<g filter="url(#bubble-shadow)">
|
||||
<rect x={tx} y={ty} width={TOOLTIP_W} height={TOOLTIP_H} fill={tooltipBg} rx={12} />
|
||||
<polygon points={tailPoints} fill={tooltipBg} />
|
||||
</g>
|
||||
<foreignObject x={tx} y={ty} width={TOOLTIP_W} height={TOOLTIP_H} style={{ overflow: 'visible' }}>
|
||||
<div style={{ padding: '10px 13px' }}>
|
||||
<p style={{ fontFamily: "'SF Mono',Consolas,monospace", fontSize: 8, letterSpacing: '0.12em', textTransform: 'uppercase', color: RED, marginBottom: 4, fontWeight: 600 }}>
|
||||
{activeDesc.label}
|
||||
</p>
|
||||
<p style={{ fontSize: 10.5, color: descText, lineHeight: 1.5, margin: 0 }}>
|
||||
{activeDesc.desc}
|
||||
</p>
|
||||
</div>
|
||||
</foreignObject>
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user