@donartcha/openlag 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +82 -0
- package/bin/openlag.js +2 -0
- package/dist/assets/arc-4YUHkXo3.js +1 -0
- package/dist/assets/architectureDiagram-3BPJPVTR-WeGmL7HM.js +36 -0
- package/dist/assets/blockDiagram-GPEHLZMM-CtV7ubAx.js +132 -0
- package/dist/assets/c4Diagram-AAUBKEIU-DqYDW5c3.js +10 -0
- package/dist/assets/channel-Tsel3-MK.js +1 -0
- package/dist/assets/chunk-2J33WTMH-BE8P9tjh.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-Bi7oLGF5.js +1 -0
- package/dist/assets/chunk-55IACEB6-D9Xhxp_r.js +1 -0
- package/dist/assets/chunk-727SXJPM-Dz8jKE60.js +206 -0
- package/dist/assets/chunk-AQP2D5EJ-BzmM0IeH.js +231 -0
- package/dist/assets/chunk-FMBD7UC4-Cvl5dpcx.js +15 -0
- package/dist/assets/chunk-ND2GUHAM-Dz2efqnq.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CwblgSnQ.js +1 -0
- package/dist/assets/classDiagram-4FO5ZUOK-Bgm-_cW8.js +1 -0
- package/dist/assets/classDiagram-v2-Q7XG4LA2-Bgm-_cW8.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-h_A3nZUx.js +1 -0
- package/dist/assets/cytoscape.esm-D_LviqZs.js +331 -0
- package/dist/assets/dagre-BM42HDAG-CN_B2Doz.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-2AECGRRQ-C9TAFwjG.js +43 -0
- package/dist/assets/diagram-5GNKFQAL-BThljQLo.js +10 -0
- package/dist/assets/diagram-KO2AKTUF-bRPq25Se.js +3 -0
- package/dist/assets/diagram-LMA3HP47-BubLCIus.js +24 -0
- package/dist/assets/diagram-OG6HWLK6-CJpfhIsS.js +24 -0
- package/dist/assets/erDiagram-TEJ5UH35-6Xkza9wL.js +85 -0
- package/dist/assets/flowDiagram-I6XJVG4X-Bq_to3hX.js +162 -0
- package/dist/assets/ganttDiagram-6RSMTGT7-C3CmvYl7.js +292 -0
- package/dist/assets/gitGraphDiagram-PVQCEYII-C93LTfrl.js +106 -0
- package/dist/assets/graph-CAnANduQ.js +1 -0
- package/dist/assets/index-0RMQQ34p.css +1 -0
- package/dist/assets/index-ByxguSZe.js +729 -0
- package/dist/assets/infoDiagram-5YYISTIA-CMfuwygl.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/ishikawaDiagram-YF4QCWOH-CbJ5ojDF.js +70 -0
- package/dist/assets/journeyDiagram-JHISSGLW-C_Xz8YyT.js +139 -0
- package/dist/assets/kanban-definition-UN3LZRKU-GVv_iRMq.js +89 -0
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-DGIYPm2g.js +1 -0
- package/dist/assets/linear-BNEtUH2J.js +1 -0
- package/dist/assets/mindmap-definition-RKZ34NQL-DIsL0XSF.js +96 -0
- package/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/assets/pieDiagram-4H26LBE5-CSCTSOjk.js +30 -0
- package/dist/assets/quadrantDiagram-W4KKPZXB-CQQ9OaFY.js +7 -0
- package/dist/assets/requirementDiagram-4Y6WPE33-Cjn3la_S.js +84 -0
- package/dist/assets/sankeyDiagram-5OEKKPKP-DoVspvVc.js +40 -0
- package/dist/assets/sequenceDiagram-3UESZ5HK-UsoGmL4w.js +162 -0
- package/dist/assets/stateDiagram-AJRCARHV-DLmf7Dc8.js +1 -0
- package/dist/assets/stateDiagram-v2-BHNVJYJU-jkiDZ_3u.js +1 -0
- package/dist/assets/timeline-definition-PNZ67QCA-HfyRxZ8p.js +120 -0
- package/dist/assets/vennDiagram-CIIHVFJN-B6pM3L33.js +34 -0
- package/dist/assets/wardley-L42UT6IY-B-LdKtrI.js +173 -0
- package/dist/assets/wardleyDiagram-YWT4CUSO-BD45zhOu.js +78 -0
- package/dist/assets/xychartDiagram-2RQKCTM6-zsDMbUiS.js +7 -0
- package/dist/cli/openlag.js +1793 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +84 -0
- package/scripts/cli/build.ts +34 -0
- package/scripts/cli/dev.ts +35 -0
- package/scripts/cli/generate.ts +92 -0
- package/scripts/cli/init.ts +427 -0
- package/scripts/cli/lint.ts +29 -0
- package/scripts/cli/openlag.ts +110 -0
- package/scripts/cli/vite-bin.ts +8 -0
- package/scripts/core/parser/diagnostic.ts +34 -0
- package/scripts/core/parser/normalizer.ts +27 -0
- package/scripts/core/parser/scanner.ts +30 -0
- package/scripts/core/parser/schemas.ts +23 -0
- package/scripts/core/parser/types.ts +30 -0
- package/scripts/core/parser.ts +127 -0
- package/scripts/generate-relations.ts +53 -0
- package/scripts/lint/lint-engine.ts +85 -0
- package/scripts/lint/lint-profiles.ts +49 -0
- package/scripts/lint/lint-rules.ts +174 -0
- package/scripts/lint/lint-types.ts +43 -0
- package/src/App.tsx +164 -0
- package/src/components/DocumentationView.tsx +905 -0
- package/src/components/GraphView.tsx +529 -0
- package/src/components/GuideView.tsx +535 -0
- package/src/components/ImpactView.tsx +365 -0
- package/src/components/MarkdownRenderer.tsx +120 -0
- package/src/components/OrphansView.tsx +360 -0
- package/src/components/SettingsView.tsx +146 -0
- package/src/core/generated/relation-definitions.ts +622 -0
- package/src/core/graph/GraphQueryLayer.ts +194 -0
- package/src/core/registry/ArtifactRegistry.ts +19 -0
- package/src/core/registry/RelationRegistry.ts +27 -0
- package/src/core/semantic/artifact-layers.ts +43 -0
- package/src/core/semantic/ownership-rules.ts +13 -0
- package/src/core/semantic/types.ts +11 -0
- package/src/index.css +121 -0
- package/src/lib/reportUtils.ts +59 -0
- package/src/main.tsx +10 -0
- package/src/store.ts +146 -0
- package/src/types.ts +77 -0
- package/vite.config.ts +31 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import React, { useMemo, useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import { ReactFlow, MiniMap, Controls, Background, useNodesState, useEdgesState, Node, Edge, MarkerType, Handle, Position, Panel, ReactFlowProvider, useReactFlow } from '@xyflow/react';
|
|
3
|
+
import dagre from 'dagre';
|
|
4
|
+
import { Search, X, AlertCircle, ChevronUp, ChevronDown } from 'lucide-react';
|
|
5
|
+
import '@xyflow/react/dist/style.css';
|
|
6
|
+
import { useStore } from '../store';
|
|
7
|
+
import { ArtifactType } from '../types';
|
|
8
|
+
|
|
9
|
+
const typeColors: Record<ArtifactType, string> = {
|
|
10
|
+
REQUIREMENT: 'text-blue-400 border-blue-400',
|
|
11
|
+
USE_CASE: 'text-indigo-400 border-indigo-400',
|
|
12
|
+
DESIGN: 'text-purple-400 border-purple-400',
|
|
13
|
+
COMPONENT: 'text-amber-400 border-amber-400',
|
|
14
|
+
CODE_ENTITY: 'text-emerald-400 border-emerald-400',
|
|
15
|
+
TEST: 'text-rose-400 border-rose-400',
|
|
16
|
+
DOCUMENTATION: 'text-slate-400 border-slate-400',
|
|
17
|
+
INCIDENT: 'text-red-400 border-red-400',
|
|
18
|
+
INFRASTRUCTURE: 'text-cyan-400 border-cyan-400',
|
|
19
|
+
DEPLOYMENT: 'text-sky-400 border-sky-400',
|
|
20
|
+
MONITORING: 'text-orange-400 border-orange-400',
|
|
21
|
+
MAINTENANCE: 'text-violet-400 border-violet-400',
|
|
22
|
+
PROJECT: 'text-stone-400 border-stone-400',
|
|
23
|
+
EPIC: 'text-indigo-500 border-indigo-500',
|
|
24
|
+
FEATURE: 'text-blue-500 border-blue-500',
|
|
25
|
+
BUSINESS_RULE: 'text-teal-400 border-teal-400',
|
|
26
|
+
DECISION: 'text-pink-400 border-pink-400',
|
|
27
|
+
TEST_CASE: 'text-rose-500 border-rose-500',
|
|
28
|
+
CHANGE: 'text-lime-400 border-lime-400',
|
|
29
|
+
BUG: 'text-red-500 border-red-500',
|
|
30
|
+
RISK: 'text-fuchsia-400 border-fuchsia-400',
|
|
31
|
+
GLOSSARY_TERM: 'text-gray-400 border-gray-400',
|
|
32
|
+
API: 'text-emerald-500 border-emerald-500',
|
|
33
|
+
DATABASE_ENTITY: 'text-orange-500 border-orange-500',
|
|
34
|
+
SYSTEM_VERSION: 'text-yellow-500 border-yellow-500',
|
|
35
|
+
VERSION: 'text-teal-500 border-teal-500',
|
|
36
|
+
LIBRARY: 'text-blue-400 border-blue-400',
|
|
37
|
+
ENVIRONMENT: 'text-indigo-400 border-indigo-400',
|
|
38
|
+
CHECK: 'text-red-400 border-red-400',
|
|
39
|
+
PROCESS: 'text-fuchsia-400 border-fuchsia-400',
|
|
40
|
+
PIPELINE: 'text-sky-400 border-sky-400',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const CustomNode = ({ data, selected }: any) => {
|
|
44
|
+
const colorClass = typeColors[data.type as ArtifactType] || 'text-white/40 border-white/40';
|
|
45
|
+
const isDimmed = data.dimmed;
|
|
46
|
+
const isOrphan = data.isOrphan;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className={`p-4 shadow-xl border bg-[#151515] w-[260px] cursor-pointer transition-all duration-300
|
|
50
|
+
${selected ? 'ring-1 ring-white/50 border-white/50 z-10' : 'border-white/10 hover:border-white/30'}
|
|
51
|
+
${isDimmed ? 'opacity-30 grayscale saturate-0' : 'opacity-100'}
|
|
52
|
+
${isOrphan ? 'border-red-500/50' : 'border-white/10'}
|
|
53
|
+
border-l-[3px]`}
|
|
54
|
+
style={{ borderLeftColor: isOrphan ? '#ef4444' : 'currentColor' }}
|
|
55
|
+
>
|
|
56
|
+
<Handle type="target" position={Position.Top} id="t-top" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-emerald-500/50 border-0'}`} style={{ left: '30%' }} />
|
|
57
|
+
<Handle type="source" position={Position.Top} id="s-top" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-blue-500/50 border-0'}`} style={{ left: '70%' }} />
|
|
58
|
+
<div className={`flex justify-between items-start mb-3 ${colorClass}`}>
|
|
59
|
+
<div className="flex flex-col gap-0.5">
|
|
60
|
+
<div className="flex items-center gap-1.5">
|
|
61
|
+
<span className="text-[9px] font-bold uppercase tracking-widest">{data.type}</span>
|
|
62
|
+
{isOrphan && (
|
|
63
|
+
<div className="text-red-500" title="Orphan Artifact (No relationships)">
|
|
64
|
+
<AlertCircle size={10} />
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
{data.subType && <span className="text-[8px] font-sans uppercase tracking-wider opacity-60">{data.subType}</span>}
|
|
69
|
+
{data.layer && <span className="text-[7px] font-mono tracking-wider opacity-40">{data.layer}</span>}
|
|
70
|
+
</div>
|
|
71
|
+
<span className="text-[10px] font-mono opacity-60 shrink-0 ml-2 border border-white/10 px-1">{data.id}</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="font-serif text-[#e0e0e0] text-sm mb-2 leading-tight">{data.title}</div>
|
|
74
|
+
<div className="flex flex-col gap-1">
|
|
75
|
+
<div className="text-[11px] leading-relaxed opacity-60 h-8 overflow-hidden text-ellipsis line-clamp-2">{data.description}</div>
|
|
76
|
+
{data.ownership?.owner && <div className="text-[8px] text-emerald-500 font-mono">OWNER: {data.ownership.owner}</div>}
|
|
77
|
+
{data.ownership?.team && <div className="text-[8px] text-blue-400 font-mono">TEAM: {data.ownership.team}</div>}
|
|
78
|
+
</div>
|
|
79
|
+
<Handle type="target" position={Position.Bottom} id="t-bot" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-emerald-500/50 border-0'}`} style={{ left: '70%' }} />
|
|
80
|
+
<Handle type="source" position={Position.Bottom} id="s-bot" className={`w-2 h-2 rounded-none ${isDimmed ? 'opacity-0' : 'bg-blue-500/50 border-0'}`} style={{ left: '30%' }} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const nodeTypes = {
|
|
86
|
+
artifact: CustomNode,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const NODE_WIDTH = 260;
|
|
90
|
+
const NODE_HEIGHT = 140;
|
|
91
|
+
|
|
92
|
+
const getLayoutedElements = (nodes: Node[], edges: Edge[], direction = 'TB') => {
|
|
93
|
+
const dagreGraph = new dagre.graphlib.Graph();
|
|
94
|
+
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
|
95
|
+
|
|
96
|
+
// Aumentar la separación para reducir cruces y dar espacio a las etiquetas
|
|
97
|
+
dagreGraph.setGraph({ rankdir: direction, align: 'UL', edgesep: 120, ranksep: 160, nodesep: 100 });
|
|
98
|
+
|
|
99
|
+
nodes.forEach((node) => {
|
|
100
|
+
if (!node.hidden) {
|
|
101
|
+
dagreGraph.setNode(node.id, { width: NODE_WIDTH, height: NODE_HEIGHT });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
edges.forEach((edge) => {
|
|
106
|
+
if (!edge.hidden) {
|
|
107
|
+
dagreGraph.setEdge(edge.source, edge.target);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
dagre.layout(dagreGraph);
|
|
112
|
+
|
|
113
|
+
const newNodes = nodes.map((node) => {
|
|
114
|
+
if (node.hidden) {
|
|
115
|
+
return { ...node, position: { x: 0, y: 0 } };
|
|
116
|
+
}
|
|
117
|
+
const nodeWithPosition = dagreGraph.node(node.id);
|
|
118
|
+
return {
|
|
119
|
+
...node,
|
|
120
|
+
position: {
|
|
121
|
+
x: nodeWithPosition.x - NODE_WIDTH / 2,
|
|
122
|
+
y: nodeWithPosition.y - NODE_HEIGHT / 2,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return { nodes: newNodes, edges };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const Legend = () => {
|
|
131
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
132
|
+
return (
|
|
133
|
+
<Panel position="bottom-center" className="m-4 z-50">
|
|
134
|
+
<div className="bg-[#0c0c0c] border border-white/10 p-3 shadow-2xl rounded-md flex flex-col gap-2 w-48">
|
|
135
|
+
<div className="flex items-center justify-between cursor-pointer" onClick={() => setIsOpen(!isOpen)}>
|
|
136
|
+
<span className="text-[10px] uppercase font-bold tracking-widest text-white/50 hover:text-white transition-colors">Legend</span>
|
|
137
|
+
{isOpen ? <ChevronDown size={12} className="text-white/50" /> : <ChevronUp size={12} className="text-white/50" />}
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{isOpen && (
|
|
141
|
+
<div className="flex flex-col gap-3 pt-2 border-t border-white/10 max-h-[60vh] overflow-y-auto" style={{ scrollbarWidth: 'thin' }}>
|
|
142
|
+
<div className="flex flex-col gap-1.5">
|
|
143
|
+
<div className="text-[9px] uppercase font-bold text-white/30">Relation Strength</div>
|
|
144
|
+
<div className="flex items-center gap-2">
|
|
145
|
+
<div className="w-4 h-[2px] bg-[#f87171]"></div>
|
|
146
|
+
<span className="text-[9px] text-white/70 font-mono">STRONG (Solid)</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="flex items-center gap-2">
|
|
149
|
+
<div className="w-4 h-[2px]" style={{ borderTop: '2px dashed #38bdf8' }}></div>
|
|
150
|
+
<span className="text-[9px] text-white/70 font-mono">MEDIUM (Dashed)</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div className="flex items-center gap-2">
|
|
153
|
+
<div className="w-4 h-[1px]" style={{ borderTop: '2px dotted #94a3b8' }}></div>
|
|
154
|
+
<span className="text-[9px] text-white/70 font-mono">WEAK (Dotted)</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="flex flex-col gap-1.5">
|
|
159
|
+
<div className="text-[9px] uppercase font-bold text-white/30">Node Connectors</div>
|
|
160
|
+
<div className="flex items-center gap-2">
|
|
161
|
+
<div className="w-2 h-2 rounded bg-blue-500/50"></div>
|
|
162
|
+
<span className="text-[9px] text-white/70 font-mono">Outputs (Source)</span>
|
|
163
|
+
</div>
|
|
164
|
+
<div className="flex items-center gap-2">
|
|
165
|
+
<div className="w-2 h-2 rounded bg-emerald-500/50"></div>
|
|
166
|
+
<span className="text-[9px] text-white/70 font-mono">Inputs (Target)</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
|
|
170
|
+
<div className="flex flex-col gap-1.5">
|
|
171
|
+
<div className="text-[9px] uppercase font-bold text-white/30">Artifact Types</div>
|
|
172
|
+
{Object.entries(typeColors).map(([type, colorClass]) => (
|
|
173
|
+
<div key={type} className="flex items-center gap-2">
|
|
174
|
+
<div className={`w-2 h-2 rounded-full border border-current bg-current ${colorClass.split(' ')[0]}`}></div>
|
|
175
|
+
<span className={`text-[9px] font-mono ${colorClass.split(' ')[0]}`}>{type}</span>
|
|
176
|
+
</div>
|
|
177
|
+
))}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</Panel>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const GraphFlow: React.FC = () => {
|
|
187
|
+
const { fullGraph, currentSubgraph: graph, selectedArtifactId, setSelectedArtifact, setView, settings, globalFilters, setGlobalFilter } = useStore();
|
|
188
|
+
const { setCenter } = useReactFlow();
|
|
189
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
190
|
+
|
|
191
|
+
const orphanIds = useMemo(() => {
|
|
192
|
+
if (!fullGraph) return new Set<string>();
|
|
193
|
+
const linked = new Set<string>();
|
|
194
|
+
fullGraph.relations.forEach(rel => {
|
|
195
|
+
linked.add(rel.from);
|
|
196
|
+
linked.add(rel.to);
|
|
197
|
+
});
|
|
198
|
+
const orphans = new Set<string>();
|
|
199
|
+
fullGraph.artifacts.forEach(art => {
|
|
200
|
+
if (!linked.has(art.id)) orphans.add(art.id);
|
|
201
|
+
});
|
|
202
|
+
return orphans;
|
|
203
|
+
}, [fullGraph]);
|
|
204
|
+
|
|
205
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
206
|
+
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
207
|
+
const filterLayer = globalFilters.layer;
|
|
208
|
+
const filterOwner = globalFilters.owner;
|
|
209
|
+
const filterTeam = globalFilters.team;
|
|
210
|
+
|
|
211
|
+
const setFilterLayer = (val: string) => setGlobalFilter('layer', val);
|
|
212
|
+
const setFilterOwner = (val: string) => setGlobalFilter('owner', val);
|
|
213
|
+
const setFilterTeam = (val: string) => setGlobalFilter('team', val);
|
|
214
|
+
|
|
215
|
+
const filterOptions = useMemo(() => {
|
|
216
|
+
if (!fullGraph || !fullGraph.artifacts) return { layers: [], owners: [], teams: [] };
|
|
217
|
+
const layers = Array.from(new Set(fullGraph.artifacts.map(a => a.layer).filter(Boolean))) as string[];
|
|
218
|
+
const owners = Array.from(new Set(fullGraph.artifacts.map(a => a.ownership?.owner).filter(Boolean))) as string[];
|
|
219
|
+
const teams = Array.from(new Set(fullGraph.artifacts.map(a => a.ownership?.team).filter(Boolean))) as string[];
|
|
220
|
+
return { layers, owners, teams };
|
|
221
|
+
}, [fullGraph]);
|
|
222
|
+
|
|
223
|
+
const filteredArtifacts = useMemo(() => {
|
|
224
|
+
if (!graph) return [];
|
|
225
|
+
return graph.artifacts.filter(a => {
|
|
226
|
+
// layer/owner are already filtered by projectSubgraph in the query layer,
|
|
227
|
+
// but we apply searchTerm here specifically.
|
|
228
|
+
if (!searchTerm) return true;
|
|
229
|
+
|
|
230
|
+
const term = searchTerm.toLowerCase();
|
|
231
|
+
return a.title.toLowerCase().includes(term) ||
|
|
232
|
+
a.type.toLowerCase().includes(term) ||
|
|
233
|
+
(a.subType && a.subType.toLowerCase().includes(term)) ||
|
|
234
|
+
a.id.toLowerCase().includes(term);
|
|
235
|
+
});
|
|
236
|
+
}, [graph, searchTerm]);
|
|
237
|
+
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
if (selectedArtifactId && nodes.length > 0) {
|
|
240
|
+
const selectedNode = nodes.find(n => n.id === selectedArtifactId);
|
|
241
|
+
if (selectedNode) {
|
|
242
|
+
setCenter(selectedNode.position.x + NODE_WIDTH / 2, selectedNode.position.y + NODE_HEIGHT / 2, { zoom: 1, duration: 800 });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}, [selectedArtifactId, nodes, setCenter]);
|
|
246
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (!graph) return;
|
|
250
|
+
|
|
251
|
+
let connectedNodes = new Set<string>();
|
|
252
|
+
let connectedEdges = new Set<string>();
|
|
253
|
+
|
|
254
|
+
if (selectedArtifactId) {
|
|
255
|
+
connectedNodes.add(selectedArtifactId);
|
|
256
|
+
|
|
257
|
+
let currentLevelNodes = new Set<string>([selectedArtifactId]);
|
|
258
|
+
|
|
259
|
+
const maxDepth = settings.graphFocusDepth === 0 ? Infinity : settings.graphFocusDepth;
|
|
260
|
+
|
|
261
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
262
|
+
let nextLevelNodes = new Set<string>();
|
|
263
|
+
let edgesToAdd = new Set<string>();
|
|
264
|
+
|
|
265
|
+
graph.relations.forEach(r => {
|
|
266
|
+
if (currentLevelNodes.has(r.from) || currentLevelNodes.has(r.to)) {
|
|
267
|
+
edgesToAdd.add(r.id);
|
|
268
|
+
if (!connectedNodes.has(r.from)) nextLevelNodes.add(r.from);
|
|
269
|
+
if (!connectedNodes.has(r.to)) nextLevelNodes.add(r.to);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (nextLevelNodes.size === 0) break; // Optimization: stop if no new nodes
|
|
274
|
+
|
|
275
|
+
nextLevelNodes.forEach(n => connectedNodes.add(n));
|
|
276
|
+
edgesToAdd.forEach(e => connectedEdges.add(e));
|
|
277
|
+
currentLevelNodes = nextLevelNodes;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let initialNodes: Node[] = graph.artifacts.map((a) => {
|
|
282
|
+
// Find connections for dimming logic
|
|
283
|
+
const isSelected = selectedArtifactId === a.id;
|
|
284
|
+
let isConnectedToSelected = false;
|
|
285
|
+
|
|
286
|
+
if (selectedArtifactId) {
|
|
287
|
+
isConnectedToSelected = connectedNodes.has(a.id);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const isFilteredOut = (filterLayer !== 'ALL' && a.layer !== filterLayer) ||
|
|
291
|
+
(filterOwner !== 'ALL' && a.ownership?.owner !== filterOwner) ||
|
|
292
|
+
(filterTeam !== 'ALL' && a.ownership?.team !== filterTeam);
|
|
293
|
+
|
|
294
|
+
const isDimmed = isFilteredOut || (selectedArtifactId !== null && !isSelected && !isConnectedToSelected);
|
|
295
|
+
const isOrphan = orphanIds.has(a.id);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
id: a.id,
|
|
299
|
+
type: 'artifact',
|
|
300
|
+
position: { x: 0, y: 0 },
|
|
301
|
+
data: { ...a, dimmed: isDimmed, isOrphan }
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
let initialEdges: Edge[] = graph.relations.map((r) => {
|
|
306
|
+
let isConnectedToSelected = true;
|
|
307
|
+
if (selectedArtifactId) {
|
|
308
|
+
isConnectedToSelected = connectedEdges.has(r.id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sourceNode = graph.artifacts.find(a => a.id === r.from);
|
|
312
|
+
const targetNode = graph.artifacts.find(a => a.id === r.to);
|
|
313
|
+
const isFilteredOut = (filterLayer !== 'ALL' && (sourceNode?.layer !== filterLayer || targetNode?.layer !== filterLayer)) ||
|
|
314
|
+
(filterOwner !== 'ALL' && (sourceNode?.ownership?.owner !== filterOwner || targetNode?.ownership?.owner !== filterOwner)) ||
|
|
315
|
+
(filterTeam !== 'ALL' && (sourceNode?.ownership?.team !== filterTeam || targetNode?.ownership?.team !== filterTeam));
|
|
316
|
+
|
|
317
|
+
const isDimmed = isFilteredOut || (selectedArtifactId !== null && !isConnectedToSelected);
|
|
318
|
+
|
|
319
|
+
const isWeak = r.strength === 'WEAK';
|
|
320
|
+
const isStrong = r.strength === 'STRONG';
|
|
321
|
+
|
|
322
|
+
let edgeColor = '#38bdf8'; // MEDIUM (sky-400)
|
|
323
|
+
if (isStrong) edgeColor = '#f87171'; // STRONG (red-400)
|
|
324
|
+
if (isWeak) edgeColor = '#94a3b8'; // WEAK (slate-400)
|
|
325
|
+
|
|
326
|
+
if (isDimmed) {
|
|
327
|
+
edgeColor = 'rgba(255,255,255,0.05)';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
id: r.id,
|
|
332
|
+
source: r.from,
|
|
333
|
+
target: r.to,
|
|
334
|
+
sourceHandle: 's-bot',
|
|
335
|
+
targetHandle: 't-top',
|
|
336
|
+
label: r.category ? `${r.type} (${r.category})` : r.type,
|
|
337
|
+
type: 'default',
|
|
338
|
+
animated: selectedArtifactId !== null && isConnectedToSelected,
|
|
339
|
+
markerEnd: {
|
|
340
|
+
type: MarkerType.ArrowClosed,
|
|
341
|
+
color: edgeColor
|
|
342
|
+
},
|
|
343
|
+
style: {
|
|
344
|
+
stroke: edgeColor,
|
|
345
|
+
strokeWidth: isDimmed ? 1 : (selectedArtifactId && isConnectedToSelected ? (isStrong ? 3 : (isWeak ? 1.5 : 2.5)) : (isStrong ? 2 : (isWeak ? 1 : 1.5))),
|
|
346
|
+
strokeDasharray: isWeak ? '2 4' : (isStrong ? '0' : '5'),
|
|
347
|
+
transition: 'all 0.3s ease'
|
|
348
|
+
},
|
|
349
|
+
labelStyle: {
|
|
350
|
+
fill: isDimmed ? 'transparent' : 'rgba(255,255,255,0.8)',
|
|
351
|
+
fontSize: 8,
|
|
352
|
+
fontWeight: isStrong ? 700 : 400,
|
|
353
|
+
letterSpacing: '1px'
|
|
354
|
+
},
|
|
355
|
+
labelBgStyle: {
|
|
356
|
+
fill: isDimmed ? 'transparent' : '#111111',
|
|
357
|
+
opacity: isDimmed ? 0 : 1,
|
|
358
|
+
stroke: isDimmed ? 'transparent' : 'rgba(255,255,255,0.1)',
|
|
359
|
+
strokeWidth: 1
|
|
360
|
+
},
|
|
361
|
+
labelBgBorderRadius: 4,
|
|
362
|
+
labelBgPadding: [8, 4],
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const layouted = getLayoutedElements(initialNodes, initialEdges, 'TB');
|
|
367
|
+
|
|
368
|
+
setNodes(layouted.nodes);
|
|
369
|
+
setEdges(layouted.edges);
|
|
370
|
+
}, [graph, selectedArtifactId, settings.graphFocusDepth, setNodes, setEdges, orphanIds, filterLayer, filterOwner, filterTeam]);
|
|
371
|
+
|
|
372
|
+
const onNodeClick = useCallback((_: any, node: Node) => {
|
|
373
|
+
setSelectedArtifact(node.id);
|
|
374
|
+
setIsDropdownOpen(false);
|
|
375
|
+
}, [setSelectedArtifact]);
|
|
376
|
+
|
|
377
|
+
const onPaneClick = useCallback(() => {
|
|
378
|
+
setSelectedArtifact(null);
|
|
379
|
+
setIsDropdownOpen(false);
|
|
380
|
+
}, [setSelectedArtifact]);
|
|
381
|
+
|
|
382
|
+
if (!graph) return <div className="p-8 text-white/50">Loading graph...</div>;
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div className="h-full w-full bg-[#0a0a0a] relative">
|
|
386
|
+
<ReactFlow
|
|
387
|
+
nodes={nodes}
|
|
388
|
+
edges={edges}
|
|
389
|
+
onNodesChange={onNodesChange}
|
|
390
|
+
onEdgesChange={onEdgesChange}
|
|
391
|
+
onNodeClick={onNodeClick}
|
|
392
|
+
onPaneClick={onPaneClick}
|
|
393
|
+
nodeTypes={nodeTypes}
|
|
394
|
+
fitView
|
|
395
|
+
colorMode="dark"
|
|
396
|
+
minZoom={0.2}
|
|
397
|
+
elevateNodesOnSelect={true}
|
|
398
|
+
>
|
|
399
|
+
<Legend />
|
|
400
|
+
<Panel position="top-left" className="m-4 z-50">
|
|
401
|
+
<div className="bg-[#0c0c0c] border border-white/10 p-3 shadow-2xl w-80 flex flex-col gap-2 rounded-md transition-all">
|
|
402
|
+
<div className="flex items-center gap-3 border-b border-white/5 pb-2">
|
|
403
|
+
<Search size={16} className="text-white/40 shrink-0" />
|
|
404
|
+
<input
|
|
405
|
+
type="text"
|
|
406
|
+
placeholder="Search by Type, Title or ID..."
|
|
407
|
+
className="bg-transparent border-none text-xs text-white outline-none w-full placeholder:text-white/20 font-sans"
|
|
408
|
+
value={searchTerm}
|
|
409
|
+
onChange={e => { setSearchTerm(e.target.value); setIsDropdownOpen(true); }}
|
|
410
|
+
onFocus={() => setIsDropdownOpen(true)}
|
|
411
|
+
/>
|
|
412
|
+
{selectedArtifactId && (
|
|
413
|
+
<button
|
|
414
|
+
onClick={() => { setSelectedArtifact(null); setSearchTerm(''); setIsDropdownOpen(false); }}
|
|
415
|
+
className="text-white/40 hover:text-emerald-400 transition-colors shrink-0 p-1"
|
|
416
|
+
title="Clear Selection"
|
|
417
|
+
>
|
|
418
|
+
<X size={14} />
|
|
419
|
+
</button>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{isDropdownOpen && (
|
|
424
|
+
<div className="max-h-[50vh] overflow-y-auto flex flex-col mt-1 space-y-1 pr-1" style={{ scrollbarWidth: 'thin' }}>
|
|
425
|
+
{orphanIds.size > 0 && !searchTerm && (
|
|
426
|
+
<button
|
|
427
|
+
onClick={() => setView('orphans')}
|
|
428
|
+
className="flex items-center gap-2 p-2 bg-red-500/10 border border-red-500/20 text-red-400 text-[10px] rounded mb-2 hover:bg-red-500/20 transition-all uppercase tracking-widest font-bold"
|
|
429
|
+
>
|
|
430
|
+
<AlertCircle size={14} />
|
|
431
|
+
{orphanIds.size} Orphan Artifacts Found
|
|
432
|
+
</button>
|
|
433
|
+
)}
|
|
434
|
+
{filteredArtifacts.length === 0 ? (
|
|
435
|
+
<div className="text-xs text-white/40 p-2 italic">No artifacts found.</div>
|
|
436
|
+
) : (
|
|
437
|
+
filteredArtifacts.map(a => (
|
|
438
|
+
<button
|
|
439
|
+
key={a.id}
|
|
440
|
+
className={`text-left text-xs p-2 hover:bg-white/10 border-l-[3px] transition-colors flex flex-col gap-1 rounded-r-md
|
|
441
|
+
${selectedArtifactId === a.id ? `${typeColors[a.type]?.split(' ')[1] || 'border-emerald-500'} bg-white/5` : 'border-transparent hover:border-white/30'}`}
|
|
442
|
+
onClick={() => {
|
|
443
|
+
setSelectedArtifact(a.id);
|
|
444
|
+
setIsDropdownOpen(false);
|
|
445
|
+
}}
|
|
446
|
+
>
|
|
447
|
+
<div className="text-[9px] uppercase tracking-widest text-[#e0e0e0] opacity-50 font-bold flex justify-between">
|
|
448
|
+
<div className="flex flex-col gap-0.5">
|
|
449
|
+
<span>{a.type}</span>
|
|
450
|
+
{a.subType && <span className="text-[8px] opacity-70">{a.subType}</span>}
|
|
451
|
+
</div>
|
|
452
|
+
<span className="font-mono">{a.id}</span>
|
|
453
|
+
</div>
|
|
454
|
+
<div className={`truncate font-serif text-sm ${typeColors[a.type]?.split(' ')[0] || 'text-emerald-400'}`}>{a.title}</div>
|
|
455
|
+
</button>
|
|
456
|
+
))
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
460
|
+
|
|
461
|
+
<div className="flex gap-1 pt-2">
|
|
462
|
+
<select
|
|
463
|
+
value={filterLayer}
|
|
464
|
+
onChange={(e) => setFilterLayer(e.target.value)}
|
|
465
|
+
className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
|
|
466
|
+
filterLayer !== 'ALL'
|
|
467
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
468
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
469
|
+
}`}
|
|
470
|
+
>
|
|
471
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">LAYER</option>
|
|
472
|
+
{filterOptions.layers.map(layer => (
|
|
473
|
+
<option key={layer} value={layer} className="bg-[#0c0c0c] text-white font-normal">{layer}</option>
|
|
474
|
+
))}
|
|
475
|
+
</select>
|
|
476
|
+
<select
|
|
477
|
+
value={filterOwner}
|
|
478
|
+
onChange={(e) => setFilterOwner(e.target.value)}
|
|
479
|
+
className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
|
|
480
|
+
filterOwner !== 'ALL'
|
|
481
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
482
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
483
|
+
}`}
|
|
484
|
+
>
|
|
485
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">OWNER</option>
|
|
486
|
+
{filterOptions.owners.map(owner => (
|
|
487
|
+
<option key={owner} value={owner} className="bg-[#0c0c0c] text-white font-normal">{owner}</option>
|
|
488
|
+
))}
|
|
489
|
+
</select>
|
|
490
|
+
<select
|
|
491
|
+
value={filterTeam}
|
|
492
|
+
onChange={(e) => setFilterTeam(e.target.value)}
|
|
493
|
+
className={`bg-transparent py-1 px-1 text-[8px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
|
|
494
|
+
filterTeam !== 'ALL'
|
|
495
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
496
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
497
|
+
}`}
|
|
498
|
+
>
|
|
499
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">TEAM</option>
|
|
500
|
+
{filterOptions.teams.map(team => (
|
|
501
|
+
<option key={team} value={team} className="bg-[#0c0c0c] text-white font-normal">{team}</option>
|
|
502
|
+
))}
|
|
503
|
+
</select>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</Panel>
|
|
507
|
+
|
|
508
|
+
<MiniMap
|
|
509
|
+
style={{ backgroundColor: '#0c0c0c', border: '1px solid rgba(255,255,255,0.1)' }}
|
|
510
|
+
maskColor="rgba(0,0,0,0.5)"
|
|
511
|
+
nodeColor="#333"
|
|
512
|
+
/>
|
|
513
|
+
<Controls
|
|
514
|
+
style={{ backgroundColor: '#151515', border: '1px solid rgba(255,255,255,0.1)', fill: '#fff' }}
|
|
515
|
+
/>
|
|
516
|
+
<Background color="#ffffff" gap={20} size={1} style={{ opacity: 0.03 }} />
|
|
517
|
+
</ReactFlow>
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
export const GraphView: React.FC = () => {
|
|
523
|
+
return (
|
|
524
|
+
<ReactFlowProvider>
|
|
525
|
+
<GraphFlow />
|
|
526
|
+
</ReactFlowProvider>
|
|
527
|
+
);
|
|
528
|
+
};
|
|
529
|
+
|