@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,905 @@
|
|
|
1
|
+
import React, { useMemo, useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useStore } from '../store';
|
|
3
|
+
import { Artifact, ArtifactType } from '../types';
|
|
4
|
+
import { Layers, FileText, Server, FileCode2, ShieldCheck, Stethoscope, ChevronRight, Search, GitPullRequest, Repeat, Box, Rocket, Activity, Wrench, Trash2, AlertCircle, Printer, Download, Milestone } from 'lucide-react';
|
|
5
|
+
import { MarkdownRenderer } from './MarkdownRenderer';
|
|
6
|
+
|
|
7
|
+
const OwnershipBadge = ({ artifact }: { artifact: Artifact }) => {
|
|
8
|
+
if (!artifact.layer && !artifact.ownership?.owner && !artifact.ownership?.team) return null;
|
|
9
|
+
return (
|
|
10
|
+
<div className="flex gap-2 mb-3 mt-1 pointer-events-none">
|
|
11
|
+
{artifact.layer && <span className="text-[9px] text-purple-400 border border-purple-400/20 bg-purple-400/5 px-1.5 py-0.5 rounded-sm font-mono tracking-tighter">LAYER: {artifact.layer}</span>}
|
|
12
|
+
{artifact.ownership?.owner && <span className="text-[9px] text-blue-400 border border-blue-400/20 bg-blue-400/5 px-1.5 py-0.5 rounded-sm font-mono tracking-tighter">OWNER: {artifact.ownership?.owner}</span>}
|
|
13
|
+
{artifact.ownership?.team && <span className="text-[9px] text-emerald-400 border border-emerald-400/20 bg-emerald-400/5 px-1.5 py-0.5 rounded-sm font-mono tracking-tighter">TEAM: {artifact.ownership?.team}</span>}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface GroupedArtifacts {
|
|
19
|
+
REQUIREMENT: Artifact[];
|
|
20
|
+
USE_CASE: Artifact[];
|
|
21
|
+
DESIGN: Artifact[];
|
|
22
|
+
COMPONENT: Artifact[];
|
|
23
|
+
CODE_ENTITY: Artifact[];
|
|
24
|
+
TEST: Artifact[];
|
|
25
|
+
DOCUMENTATION: Artifact[];
|
|
26
|
+
INCIDENT: Artifact[];
|
|
27
|
+
INFRASTRUCTURE: Artifact[];
|
|
28
|
+
DEPLOYMENT: Artifact[];
|
|
29
|
+
MONITORING: Artifact[];
|
|
30
|
+
MAINTENANCE: Artifact[];
|
|
31
|
+
VERSION: Artifact[];
|
|
32
|
+
SYSTEM_VERSION: Artifact[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PHASES = [
|
|
36
|
+
{ id: 'req', title: 'Requirements / Analysis', icon: FileText, types: ['REQUIREMENT', 'USE_CASE'] },
|
|
37
|
+
{ id: 'design', title: 'Technical Design', icon: Layers, types: ['DESIGN', 'COMPONENT'] },
|
|
38
|
+
{ id: 'dev', title: 'Development', icon: FileCode2, types: ['CODE_ENTITY'] },
|
|
39
|
+
{ id: 'review', title: 'Code Review', icon: GitPullRequest, types: ['CODE_ENTITY'] },
|
|
40
|
+
{ id: 'ci', title: 'Continuous Integration', icon: Repeat, types: ['INFRASTRUCTURE'] },
|
|
41
|
+
{ id: 'verif', title: 'Testing', icon: ShieldCheck, types: ['TEST'] },
|
|
42
|
+
{ id: 'build', title: 'Build / Packaging', icon: Box, types: ['INFRASTRUCTURE'] },
|
|
43
|
+
{ id: 'deploy', title: 'Deployment', icon: Rocket, types: ['DEPLOYMENT'] },
|
|
44
|
+
{ id: 'monitor', title: 'Monitoring', icon: Activity, types: ['MONITORING', 'INCIDENT'] },
|
|
45
|
+
{ id: 'maint', title: 'Maintenance / Refactoring', icon: Wrench, types: ['MAINTENANCE'] },
|
|
46
|
+
{ id: 'retire', title: 'Retirement / Replacement', icon: Trash2, types: ['MAINTENANCE'] },
|
|
47
|
+
{ id: 'versions', title: 'Releases & Versions', icon: Milestone, types: ['VERSION', 'SYSTEM_VERSION'] },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const DocumentationView: React.FC = () => {
|
|
51
|
+
const { fullGraph: graph, currentVersionId, versions, systemVersions, selectedArtifactId, setSelectedArtifact, settings, globalFilters, setGlobalFilter } = useStore();
|
|
52
|
+
|
|
53
|
+
const [selectedPhase, setSelectedPhase] = useState<string | null>(null);
|
|
54
|
+
const [selectedSubType, setSelectedSubType] = useState<string | null>(null);
|
|
55
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
56
|
+
const filterLayer = globalFilters.layer;
|
|
57
|
+
const filterOwner = globalFilters.owner;
|
|
58
|
+
const filterTeam = globalFilters.team;
|
|
59
|
+
|
|
60
|
+
const setFilterLayer = (val: string) => setGlobalFilter('layer', val);
|
|
61
|
+
const setFilterOwner = (val: string) => setGlobalFilter('owner', val);
|
|
62
|
+
const setFilterTeam = (val: string) => setGlobalFilter('team', val);
|
|
63
|
+
const [graphFilterType, setGraphFilterType] = useState<string | 'ALL'>('ALL');
|
|
64
|
+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
|
65
|
+
const [isHeaderMinified, setIsHeaderMinified] = useState(false);
|
|
66
|
+
const [showInventory, setShowInventory] = useState(false);
|
|
67
|
+
const [isImpactFocusMode, setIsImpactFocusMode] = useState(settings.defaultDocsFocusMode);
|
|
68
|
+
|
|
69
|
+
const filterOptions = useMemo(() => {
|
|
70
|
+
if (!graph || !graph.artifacts) return { layers: [], owners: [], teams: [] };
|
|
71
|
+
const layers = Array.from(new Set(graph.artifacts.map(a => a.layer).filter(Boolean))) as string[];
|
|
72
|
+
const owners = Array.from(new Set(graph.artifacts.map(a => a.ownership?.owner).filter(Boolean))) as string[];
|
|
73
|
+
const teams = Array.from(new Set(graph.artifacts.map(a => a.ownership?.team).filter(Boolean))) as string[];
|
|
74
|
+
return { layers, owners, teams };
|
|
75
|
+
}, [graph]);
|
|
76
|
+
|
|
77
|
+
const orphanArtifactIds = useMemo(() => {
|
|
78
|
+
if (!graph) return new Set<string>();
|
|
79
|
+
const linked = new Set<string>();
|
|
80
|
+
graph.relations.forEach(rel => {
|
|
81
|
+
linked.add(rel.from);
|
|
82
|
+
linked.add(rel.to);
|
|
83
|
+
});
|
|
84
|
+
const orphans = new Set<string>();
|
|
85
|
+
graph.artifacts.forEach(art => {
|
|
86
|
+
if (!linked.has(art.id)) orphans.add(art.id);
|
|
87
|
+
});
|
|
88
|
+
return orphans;
|
|
89
|
+
}, [graph]);
|
|
90
|
+
|
|
91
|
+
const reachableArtifactIds = useMemo(() => {
|
|
92
|
+
if (!selectedArtifactId || !graph) return new Set<string>();
|
|
93
|
+
|
|
94
|
+
const reachable = new Set<string>();
|
|
95
|
+
reachable.add(selectedArtifactId);
|
|
96
|
+
|
|
97
|
+
let currentLevelNodes = new Set<string>([selectedArtifactId]);
|
|
98
|
+
const maxDepth = settings.docsFocusDepth === 0 ? Infinity : settings.docsFocusDepth;
|
|
99
|
+
|
|
100
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
101
|
+
let nextLevelNodes = new Set<string>();
|
|
102
|
+
|
|
103
|
+
graph.relations.forEach(r => {
|
|
104
|
+
if (currentLevelNodes.has(r.from) || currentLevelNodes.has(r.to)) {
|
|
105
|
+
if (!reachable.has(r.from)) nextLevelNodes.add(r.from);
|
|
106
|
+
if (!reachable.has(r.to)) nextLevelNodes.add(r.to);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (nextLevelNodes.size === 0) break;
|
|
111
|
+
|
|
112
|
+
nextLevelNodes.forEach(n => reachable.add(n));
|
|
113
|
+
currentLevelNodes = nextLevelNodes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return reachable;
|
|
117
|
+
}, [selectedArtifactId, graph, settings.docsFocusDepth]);
|
|
118
|
+
|
|
119
|
+
const grouped = useMemo(() => {
|
|
120
|
+
const groups: GroupedArtifacts = {
|
|
121
|
+
REQUIREMENT: [], USE_CASE: [], DESIGN: [], COMPONENT: [],
|
|
122
|
+
CODE_ENTITY: [], TEST: [], DOCUMENTATION: [], INCIDENT: [],
|
|
123
|
+
INFRASTRUCTURE: [], DEPLOYMENT: [], MONITORING: [], MAINTENANCE: [],
|
|
124
|
+
VERSION: [], SYSTEM_VERSION: []
|
|
125
|
+
};
|
|
126
|
+
if (graph && graph.artifacts) {
|
|
127
|
+
graph.artifacts.forEach(a => {
|
|
128
|
+
const type = a.type as keyof GroupedArtifacts;
|
|
129
|
+
if (groups[type]) {
|
|
130
|
+
groups[type].push(a);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return groups;
|
|
135
|
+
}, [graph]);
|
|
136
|
+
|
|
137
|
+
const filteredGroups = useMemo(() => {
|
|
138
|
+
const filterBySearch = (list: Artifact[] = []) => {
|
|
139
|
+
if (!searchQuery.trim()) return list;
|
|
140
|
+
const term = searchQuery.toLowerCase();
|
|
141
|
+
return list.filter(a =>
|
|
142
|
+
a.title.toLowerCase().includes(term) ||
|
|
143
|
+
a.description.toLowerCase().includes(term) ||
|
|
144
|
+
a.id.toLowerCase().includes(term) ||
|
|
145
|
+
(a.subType && a.subType.toLowerCase().includes(term))
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const filterByGraph = (list: Artifact[] = []) => {
|
|
150
|
+
if (!selectedArtifactId || !isImpactFocusMode) return list;
|
|
151
|
+
return list.filter(a => reachableArtifactIds.has(a.id));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const filterByLayerAndOwnership = (list: Artifact[] = []) => {
|
|
155
|
+
return list.filter(a => {
|
|
156
|
+
if (filterLayer !== 'ALL' && a.layer !== filterLayer) return false;
|
|
157
|
+
if (filterOwner !== 'ALL' && a.ownership?.owner !== filterOwner) return false;
|
|
158
|
+
if (filterTeam !== 'ALL' && a.ownership?.team !== filterTeam) return false;
|
|
159
|
+
return true;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const applyFilters = (list: Artifact[] = []) => filterByGraph(filterByLayerAndOwnership(filterBySearch(list)));
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
REQUIREMENT: applyFilters(grouped.REQUIREMENT),
|
|
167
|
+
USE_CASE: applyFilters(grouped.USE_CASE),
|
|
168
|
+
DESIGN: applyFilters(grouped.DESIGN),
|
|
169
|
+
COMPONENT: applyFilters(grouped.COMPONENT),
|
|
170
|
+
CODE_ENTITY: applyFilters(grouped.CODE_ENTITY),
|
|
171
|
+
TEST: applyFilters(grouped.TEST),
|
|
172
|
+
DOCUMENTATION: applyFilters(grouped.DOCUMENTATION),
|
|
173
|
+
INCIDENT: applyFilters(grouped.INCIDENT),
|
|
174
|
+
INFRASTRUCTURE: applyFilters(grouped.INFRASTRUCTURE),
|
|
175
|
+
DEPLOYMENT: applyFilters(grouped.DEPLOYMENT),
|
|
176
|
+
MONITORING: applyFilters(grouped.MONITORING),
|
|
177
|
+
MAINTENANCE: applyFilters(grouped.MAINTENANCE),
|
|
178
|
+
VERSION: applyFilters(grouped.VERSION),
|
|
179
|
+
SYSTEM_VERSION: applyFilters(grouped.SYSTEM_VERSION),
|
|
180
|
+
};
|
|
181
|
+
}, [grouped, searchQuery, selectedArtifactId, reachableArtifactIds, isImpactFocusMode, filterLayer, filterOwner, filterTeam]);
|
|
182
|
+
|
|
183
|
+
const phasesData = useMemo(() => {
|
|
184
|
+
return PHASES.map(phase => {
|
|
185
|
+
let filteredArtifactsInPhase = phase.types.flatMap(type =>
|
|
186
|
+
(filteredGroups[type as keyof typeof filteredGroups] || [])
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
if (phase.id === 'dev') {
|
|
190
|
+
filteredArtifactsInPhase = filteredArtifactsInPhase.filter(a => a.subType !== 'Review');
|
|
191
|
+
} else if (phase.id === 'review') {
|
|
192
|
+
filteredArtifactsInPhase = filteredArtifactsInPhase.filter(a => a.subType === 'Review');
|
|
193
|
+
} else if (phase.id === 'ci') {
|
|
194
|
+
filteredArtifactsInPhase = filteredArtifactsInPhase.filter(a => a.subType?.includes('CI') || a.subType === 'Pipeline');
|
|
195
|
+
} else if (phase.id === 'build') {
|
|
196
|
+
filteredArtifactsInPhase = filteredArtifactsInPhase.filter(a => a.subType === 'Build' || a.subType === 'Package');
|
|
197
|
+
} else if (phase.id === 'maint') {
|
|
198
|
+
filteredArtifactsInPhase = filteredArtifactsInPhase.filter(a => a.subType !== 'Retirement');
|
|
199
|
+
} else if (phase.id === 'retire') {
|
|
200
|
+
filteredArtifactsInPhase = filteredArtifactsInPhase.filter(a => a.subType === 'Retirement');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const subTypes = Array.from(new Set(filteredArtifactsInPhase.map(a => a.subType).filter(Boolean))) as string[];
|
|
204
|
+
// We will also synthesize "COMPONENT" as a pseudo subtype for Design since we grouped DESIGN and COMPONENT together in phase 2,
|
|
205
|
+
// but only if there are COMPONENT artifacts and they don't already have subTypes.
|
|
206
|
+
if (phase.id === 'design' && (filteredGroups.COMPONENT || []).length > 0) {
|
|
207
|
+
const compSubTypes = Array.from(new Set((filteredGroups.COMPONENT || []).map(a => a.subType).filter(Boolean))) as string[];
|
|
208
|
+
for (const s of compSubTypes) {
|
|
209
|
+
if (!subTypes.includes(s)) subTypes.push(s);
|
|
210
|
+
}
|
|
211
|
+
if (compSubTypes.length === 0 && !subTypes.includes('Component')) {
|
|
212
|
+
subTypes.push('Component');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
...phase,
|
|
217
|
+
count: filteredArtifactsInPhase.length,
|
|
218
|
+
subTypes,
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}, [filteredGroups]);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (selectedArtifactId) {
|
|
225
|
+
setTimeout(() => {
|
|
226
|
+
const el = document.getElementById(selectedArtifactId);
|
|
227
|
+
if (el) {
|
|
228
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
229
|
+
}
|
|
230
|
+
}, 100);
|
|
231
|
+
}
|
|
232
|
+
}, [selectedArtifactId, filteredGroups]);
|
|
233
|
+
|
|
234
|
+
if (!graph) return <div className="p-8">Loading documentation...</div>;
|
|
235
|
+
|
|
236
|
+
const currentVersion = versions.find(v => v.id === currentVersionId);
|
|
237
|
+
|
|
238
|
+
const getRelations = (artifactId: string, type: 'to' | 'from') => {
|
|
239
|
+
return graph.relations.filter(r => type === 'to' ? r.to === artifactId : r.from === artifactId)
|
|
240
|
+
.map(r => {
|
|
241
|
+
const relatedId = type === 'to' ? r.from : r.to;
|
|
242
|
+
const relatedArt = graph.artifacts.find(a => a.id === relatedId);
|
|
243
|
+
return { relation: r, artifact: relatedArt };
|
|
244
|
+
}).filter(r => r.artifact !== undefined);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const handlePhaseClick = (phaseId: string) => {
|
|
248
|
+
if (selectedPhase === phaseId && !selectedSubType) {
|
|
249
|
+
setSelectedPhase(null); // toggle off
|
|
250
|
+
} else {
|
|
251
|
+
setSelectedPhase(phaseId);
|
|
252
|
+
setSelectedSubType(null);
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const handleSubTypeClick = (phaseId: string, subType: string) => {
|
|
257
|
+
if (selectedSubType === subType) {
|
|
258
|
+
setSelectedSubType(null);
|
|
259
|
+
setSelectedPhase(phaseId);
|
|
260
|
+
} else {
|
|
261
|
+
setSelectedPhase(phaseId);
|
|
262
|
+
setSelectedSubType(subType);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const hasReqs = (filteredGroups.REQUIREMENT?.length || 0) + (filteredGroups.USE_CASE?.length || 0) > 0;
|
|
267
|
+
const hasDesign = (filteredGroups.DESIGN?.length || 0) + (filteredGroups.COMPONENT?.length || 0) > 0;
|
|
268
|
+
const hasDev = (filteredGroups.CODE_ENTITY || []).filter(c => c.subType !== 'Review').length > 0;
|
|
269
|
+
const hasReview = (filteredGroups.CODE_ENTITY || []).filter(c => c.subType === 'Review').length > 0;
|
|
270
|
+
const hasCI = (filteredGroups.INFRASTRUCTURE || []).filter(i => i.subType?.includes('CI') || i.subType === 'Pipeline').length > 0;
|
|
271
|
+
const hasTest = (filteredGroups.TEST?.length || 0) > 0;
|
|
272
|
+
const hasBuild = (filteredGroups.INFRASTRUCTURE || []).filter(i => i.subType === 'Build' || i.subType === 'Package').length > 0;
|
|
273
|
+
const hasDeploy = (filteredGroups.DEPLOYMENT?.length || 0) > 0;
|
|
274
|
+
const hasMonit = (filteredGroups.MONITORING?.length || 0) + (filteredGroups.INCIDENT?.length || 0) > 0;
|
|
275
|
+
const hasMaint = (filteredGroups.MAINTENANCE || []).filter(m => m.subType !== 'Retirement').length > 0;
|
|
276
|
+
const hasRet = (filteredGroups.MAINTENANCE || []).filter(m => m.subType === 'Retirement').length > 0;
|
|
277
|
+
const hasVersions = (filteredGroups.VERSION?.length || 0) + (filteredGroups.SYSTEM_VERSION?.length || 0) > 0;
|
|
278
|
+
|
|
279
|
+
const shouldShow = (hasItems: boolean) => !(isImpactFocusMode && selectedArtifactId && !hasItems);
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className="h-full w-full bg-[#0a0a0a] flex overflow-hidden print-block">
|
|
283
|
+
{/* Sidebar */}
|
|
284
|
+
<div className={`shrink-0 bg-[#0c0c0c] border-r border-white/5 flex flex-col transition-all duration-300 print-hidden ${isSidebarCollapsed ? 'w-12' : 'w-64'}`}>
|
|
285
|
+
<div className="p-4 border-b border-white/5 sticky top-0 bg-[#0c0c0c]/90 backdrop-blur z-10 flex items-center justify-between">
|
|
286
|
+
{!isSidebarCollapsed && <div className="text-[10px] uppercase tracking-widest text-emerald-500 font-bold">Lifecycle Phases</div>}
|
|
287
|
+
<button
|
|
288
|
+
onClick={() => setIsSidebarCollapsed(!isSidebarCollapsed)}
|
|
289
|
+
className="p-1 hover:bg-white/10 rounded transition-colors text-white/40 hover:text-white"
|
|
290
|
+
>
|
|
291
|
+
<ChevronRight className={`transition-transform duration-300 ${isSidebarCollapsed ? '' : 'rotate-180'}`} size={16} />
|
|
292
|
+
</button>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div className={`p-4 space-y-1 overflow-y-auto custom-scrollbar flex-1 ${isSidebarCollapsed ? 'items-center px-2' : ''}`}>
|
|
296
|
+
{!isSidebarCollapsed && (
|
|
297
|
+
<button
|
|
298
|
+
className={`w-full text-left px-4 py-2 text-xs font-semibold uppercase tracking-widest rounded transition-colors mb-2 ${!selectedPhase ? 'bg-white/10 text-white' : 'text-white/40 hover:bg-white/5 hover:text-white/80'}`}
|
|
299
|
+
onClick={() => { setSelectedPhase(null); setSelectedSubType(null); }}
|
|
300
|
+
>
|
|
301
|
+
All Assets
|
|
302
|
+
</button>
|
|
303
|
+
)}
|
|
304
|
+
|
|
305
|
+
{phasesData.map((phase, idx) => {
|
|
306
|
+
const Icon = phase.icon;
|
|
307
|
+
const isPhaseSelected = selectedPhase === phase.id;
|
|
308
|
+
|
|
309
|
+
if (isSidebarCollapsed) {
|
|
310
|
+
return (
|
|
311
|
+
<button
|
|
312
|
+
key={phase.id}
|
|
313
|
+
onClick={() => handlePhaseClick(phase.id)}
|
|
314
|
+
className={`p-2 rounded transition-colors mb-1 group relative ${isPhaseSelected ? 'bg-emerald-500/10 text-emerald-400' : 'text-white/40 hover:bg-white/5'}`}
|
|
315
|
+
title={phase.title}
|
|
316
|
+
>
|
|
317
|
+
<Icon size={18} />
|
|
318
|
+
<div className="absolute left-full ml-2 px-2 py-1 bg-black border border-white/20 text-[10px] whitespace-nowrap rounded opacity-0 group-hover:opacity-100 pointer-events-none z-50">
|
|
319
|
+
{phase.title} ({phase.count})
|
|
320
|
+
</div>
|
|
321
|
+
</button>
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<div key={phase.id} className="flex flex-col mb-1">
|
|
327
|
+
<button
|
|
328
|
+
onClick={() => handlePhaseClick(phase.id)}
|
|
329
|
+
className={`w-full text-left px-3 py-2 text-xs transition-colors rounded-sm flex items-center justify-between group
|
|
330
|
+
${isPhaseSelected && !selectedSubType ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/20' : 'text-white/60 hover:bg-white/5 border border-transparent'}`}
|
|
331
|
+
>
|
|
332
|
+
<div className="flex items-center gap-3">
|
|
333
|
+
<Icon size={14} className={isPhaseSelected && !selectedSubType ? "text-emerald-400" : "text-white/40 group-hover:text-white/70"} />
|
|
334
|
+
<span>{idx + 1}. {phase.title}</span>
|
|
335
|
+
</div>
|
|
336
|
+
<span className="text-[9px] bg-white/10 px-1.5 py-0.5 rounded-sm tabular-nums opacity-60 group-hover:opacity-100">{phase.count}</span>
|
|
337
|
+
</button>
|
|
338
|
+
|
|
339
|
+
{(isPhaseSelected || selectedPhase === phase.id) && phase.subTypes.length > 0 && (
|
|
340
|
+
<div className="flex flex-col ml-6 mt-1 space-y-0.5 border-l border-white/10 pl-2">
|
|
341
|
+
{phase.subTypes.map(st => (
|
|
342
|
+
<button
|
|
343
|
+
key={st}
|
|
344
|
+
onClick={() => handleSubTypeClick(phase.id, st)}
|
|
345
|
+
className={`text-left px-2 py-1.5 text-[10px] uppercase tracking-widest transition-colors rounded-sm
|
|
346
|
+
${selectedSubType === st ? 'text-white bg-white/10 font-bold' : 'text-white/40 hover:text-white/80 hover:bg-white/5'}`}
|
|
347
|
+
>
|
|
348
|
+
{st}
|
|
349
|
+
</button>
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
</div>
|
|
354
|
+
);
|
|
355
|
+
})}
|
|
356
|
+
</div>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<div className="flex-1 overflow-y-auto relative flex flex-col group/doc">
|
|
360
|
+
<div className="flex flex-col w-full bg-[#0a0a0a] min-h-max">
|
|
361
|
+
{/* Minimized Header Controls */}
|
|
362
|
+
<div className={`sticky top-0 z-30 transition-all duration-300 bg-[#0a0a0a]/80 backdrop-blur-md px-8 lg:px-16 border-b border-white/5 flex items-center justify-between print-hidden ${isHeaderMinified ? 'py-2' : 'py-8 pt-12 pb-4'}`}>
|
|
363
|
+
<div className="flex items-center gap-4">
|
|
364
|
+
<h1 className={`font-serif italic tracking-tight transition-all duration-300 ${isHeaderMinified ? 'text-lg' : 'text-3xl'}`}>
|
|
365
|
+
{selectedSubType ? `${selectedSubType} Documentation` : selectedPhase ? phasesData.find(p => p.id === selectedPhase)?.title : 'System Documentation'}
|
|
366
|
+
</h1>
|
|
367
|
+
<div className="h-4 w-px bg-white/10" />
|
|
368
|
+
<button
|
|
369
|
+
onClick={() => setIsHeaderMinified(!isHeaderMinified)}
|
|
370
|
+
className="p-1 hover:bg-white/10 rounded transition-colors text-white/20 hover:text-white/60"
|
|
371
|
+
title={isHeaderMinified ? "Expand Header" : "Minimize Header"}
|
|
372
|
+
>
|
|
373
|
+
<ChevronRight className={`transition-transform duration-300 ${isHeaderMinified ? 'rotate-90' : '-rotate-90'}`} size={14} />
|
|
374
|
+
</button>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div className="flex flex-col md:flex-row gap-2 items-center">
|
|
378
|
+
{selectedArtifactId && (
|
|
379
|
+
<div className="flex items-center gap-2 mr-4">
|
|
380
|
+
<button
|
|
381
|
+
onClick={() => setIsImpactFocusMode(!isImpactFocusMode)}
|
|
382
|
+
className={`flex items-center gap-2 px-3 py-1 rounded-sm border transition-all text-[10px] font-mono tracking-tight cursor-pointer ${isImpactFocusMode ? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400' : 'bg-white/5 border-white/10 text-white/40 hover:text-white/60'}`}
|
|
383
|
+
>
|
|
384
|
+
<Activity size={12} className={isImpactFocusMode ? 'animate-pulse' : ''} />
|
|
385
|
+
FOCUS: {isImpactFocusMode ? 'ON' : 'OFF'}
|
|
386
|
+
</button>
|
|
387
|
+
<div className="h-4 w-[1px] bg-white/10 mx-1" />
|
|
388
|
+
<span className="text-[10px] text-white/40 font-mono italic">
|
|
389
|
+
{selectedArtifactId}
|
|
390
|
+
</span>
|
|
391
|
+
<button
|
|
392
|
+
onClick={() => setSelectedArtifact(null)}
|
|
393
|
+
className="p-1 text-white/20 hover:text-red-400 transition-colors"
|
|
394
|
+
title="Clear Selection"
|
|
395
|
+
>
|
|
396
|
+
<Trash2 size={12} />
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
<div className="flex flex-col gap-2 w-full md:w-auto">
|
|
402
|
+
<div className="flex flex-col md:flex-row gap-2">
|
|
403
|
+
<div className="relative w-full md:w-40">
|
|
404
|
+
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
405
|
+
<Search size={14} className="text-white/40" />
|
|
406
|
+
</div>
|
|
407
|
+
<input
|
|
408
|
+
type="text"
|
|
409
|
+
placeholder="Keywords..."
|
|
410
|
+
value={searchQuery}
|
|
411
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
412
|
+
className="w-full bg-[#111] border border-white/10 rounded-md py-1.5 pl-8 pr-3 text-xs text-white focus:outline-none focus:border-emerald-500/50 focus:ring-1 focus:ring-emerald-500/50 transition-colors"
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
<div className="flex gap-1 w-full lg:w-auto">
|
|
416
|
+
<select
|
|
417
|
+
value={graphFilterType}
|
|
418
|
+
onChange={(e) => setGraphFilterType(e.target.value)}
|
|
419
|
+
className={`bg-transparent py-1 px-2 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider transition-all border rounded-sm ${
|
|
420
|
+
graphFilterType !== 'ALL'
|
|
421
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
422
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
423
|
+
}`}
|
|
424
|
+
>
|
|
425
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white">All Types</option>
|
|
426
|
+
{Object.keys(grouped).map(type => (
|
|
427
|
+
<option key={type} value={type} className="bg-[#0c0c0c] text-white">{type.replace('_', ' ')}</option>
|
|
428
|
+
))}
|
|
429
|
+
</select>
|
|
430
|
+
<select
|
|
431
|
+
value={selectedArtifactId || ''}
|
|
432
|
+
onChange={(e) => setSelectedArtifact(e.target.value || null)}
|
|
433
|
+
className={`bg-transparent py-1 px-2 text-[9px] focus:outline-none cursor-pointer font-mono max-w-[120px] transition-all border rounded-sm ${
|
|
434
|
+
selectedArtifactId
|
|
435
|
+
? 'bg-emerald-500/20 border-emerald-500/40 text-emerald-400 font-bold'
|
|
436
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
437
|
+
}`}
|
|
438
|
+
>
|
|
439
|
+
<option value="" className="bg-[#0c0c0c] text-white">Artifact...</option>
|
|
440
|
+
{(graph?.artifacts || [])
|
|
441
|
+
.filter(a => graphFilterType === 'ALL' || a.type === graphFilterType)
|
|
442
|
+
.sort((a,b) => a.id.localeCompare(b.id))
|
|
443
|
+
.map(a => (
|
|
444
|
+
<option key={a.id} value={a.id} className="text-white bg-[#0c0c0c]">
|
|
445
|
+
{a.id}
|
|
446
|
+
</option>
|
|
447
|
+
))}
|
|
448
|
+
</select>
|
|
449
|
+
</div>
|
|
450
|
+
<button
|
|
451
|
+
onClick={() => window.print()}
|
|
452
|
+
className="flex items-center justify-center p-1.5 ml-2 hover:bg-white/10 rounded transition-colors text-white/40 hover:text-white print-hidden"
|
|
453
|
+
title="Print Native (System Print Dialog)"
|
|
454
|
+
>
|
|
455
|
+
<Printer size={16} />
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
<div className="flex gap-1 w-full mt-2 lg:mt-0">
|
|
459
|
+
<select
|
|
460
|
+
value={filterLayer}
|
|
461
|
+
onChange={(e) => setFilterLayer(e.target.value)}
|
|
462
|
+
className={`bg-transparent py-1 px-1 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
|
|
463
|
+
filterLayer !== 'ALL'
|
|
464
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
465
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
466
|
+
}`}
|
|
467
|
+
>
|
|
468
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">LAYER</option>
|
|
469
|
+
{filterOptions.layers.map(layer => (
|
|
470
|
+
<option key={layer} value={layer} className="bg-[#0c0c0c] text-white font-normal">{layer}</option>
|
|
471
|
+
))}
|
|
472
|
+
</select>
|
|
473
|
+
<select
|
|
474
|
+
value={filterOwner}
|
|
475
|
+
onChange={(e) => setFilterOwner(e.target.value)}
|
|
476
|
+
className={`bg-transparent py-1 px-1 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
|
|
477
|
+
filterOwner !== 'ALL'
|
|
478
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
479
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
480
|
+
}`}
|
|
481
|
+
>
|
|
482
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">OWNER</option>
|
|
483
|
+
{filterOptions.owners.map(owner => (
|
|
484
|
+
<option key={owner} value={owner} className="bg-[#0c0c0c] text-white font-normal">{owner}</option>
|
|
485
|
+
))}
|
|
486
|
+
</select>
|
|
487
|
+
<select
|
|
488
|
+
value={filterTeam}
|
|
489
|
+
onChange={(e) => setFilterTeam(e.target.value)}
|
|
490
|
+
className={`bg-transparent py-1 px-1 text-[9px] focus:outline-none cursor-pointer uppercase tracking-wider flex-1 text-center transition-all border rounded-sm ${
|
|
491
|
+
filterTeam !== 'ALL'
|
|
492
|
+
? 'bg-blue-500/20 border-blue-500/40 text-blue-400 font-bold'
|
|
493
|
+
: 'border-white/10 text-white/50 hover:border-white/20'
|
|
494
|
+
}`}
|
|
495
|
+
>
|
|
496
|
+
<option value="ALL" className="bg-[#0c0c0c] text-white font-normal">TEAM</option>
|
|
497
|
+
{filterOptions.teams.map(team => (
|
|
498
|
+
<option key={team} value={team} className="bg-[#0c0c0c] text-white font-normal">{team}</option>
|
|
499
|
+
))}
|
|
500
|
+
</select>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div className="px-8 lg:px-16 pt-4 shrink-0">
|
|
507
|
+
<div className="flex flex-col md:flex-row md:items-start gap-4 md:gap-12 mb-8 mt-2 animate-in fade-in slide-in-from-top-1 duration-300">
|
|
508
|
+
<div className="text-xs font-mono text-white/40">
|
|
509
|
+
SYS DOMAIN: <span className="text-emerald-400 font-bold tracking-widest">{selectedSubType ? `${selectedSubType} Documentation` : selectedPhase ? phasesData.find(p => p.id === selectedPhase)?.title : 'System Documentation'}</span><br/>
|
|
510
|
+
VERSION: <span className="text-white/80">{currentVersion?.name} ({currentVersionId})</span><br/>
|
|
511
|
+
DATE: {new Date().toISOString().split('T')[0]}
|
|
512
|
+
</div>
|
|
513
|
+
{systemVersions.length > 0 && (
|
|
514
|
+
<div className="flex flex-col gap-2">
|
|
515
|
+
<button
|
|
516
|
+
onClick={() => setShowInventory(!showInventory)}
|
|
517
|
+
className="text-[10px] uppercase tracking-widest text-white/20 font-bold flex items-center gap-2 hover:text-white/40 transition-colors print-hidden"
|
|
518
|
+
>
|
|
519
|
+
System Inventory
|
|
520
|
+
<ChevronRight size={10} className={`transition-transform ${showInventory ? 'rotate-90' : ''}`} />
|
|
521
|
+
</button>
|
|
522
|
+
{showInventory && (
|
|
523
|
+
<div className="flex gap-2 flex-wrap animate-in fade-in slide-in-from-top-2 duration-300">
|
|
524
|
+
{systemVersions.map(sv => (
|
|
525
|
+
<div key={sv.id} className="flex items-center gap-2 bg-white/5 border border-white/10 px-2 py-1 rounded text-[10px] tabular-nums">
|
|
526
|
+
<span className="text-emerald-500 font-bold">{sv.component}</span>
|
|
527
|
+
<span className="text-white/40 font-mono">{sv.version}</span>
|
|
528
|
+
</div>
|
|
529
|
+
))}
|
|
530
|
+
</div>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
)}
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<div className="flex-1 overflow-y-auto px-8 lg:px-16 pb-12 custom-scrollbar">
|
|
538
|
+
{/* 1. Requirements */}
|
|
539
|
+
{(!selectedPhase || selectedPhase === 'req') && shouldShow(hasReqs) && (
|
|
540
|
+
<section className="mb-16">
|
|
541
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">1. Requirements / Analysis</h2>
|
|
542
|
+
{(filteredGroups.REQUIREMENT?.length === 0 && filteredGroups.USE_CASE?.length === 0) && <p className="italic text-white/40 text-xs">No requirements found in this version.</p>}
|
|
543
|
+
{[...(filteredGroups.REQUIREMENT || []), ...(filteredGroups.USE_CASE || [])]
|
|
544
|
+
.filter(req => !selectedSubType || req.subType === selectedSubType)
|
|
545
|
+
.map(req => (
|
|
546
|
+
<div key={req.id} id={req.id} className="mb-6 bg-[#0c0c0c] border border-white/10 p-5 pl-6 border-l-[3px] border-l-blue-500/50 hover:bg-white-[0.02] transition-colors relative group">
|
|
547
|
+
<h3 className="font-serif text-lg text-white flex items-center gap-4 flex-wrap mb-4">
|
|
548
|
+
<button
|
|
549
|
+
onClick={() => setSelectedArtifact(req.id)}
|
|
550
|
+
className={`text-[10px] font-mono bg-black px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-blue-500/50 transition-colors ${selectedArtifactId === req.id ? 'text-blue-400 border-blue-500/50' : 'text-white/40'}`}
|
|
551
|
+
>
|
|
552
|
+
{req.id}
|
|
553
|
+
</button>
|
|
554
|
+
{req.subType && <span className="text-[10px] font-sans bg-blue-500/10 text-blue-300 px-2 py-1 border border-blue-500/20 uppercase tracking-widest">{req.subType}</span>}
|
|
555
|
+
{orphanArtifactIds.has(req.id) && (
|
|
556
|
+
<span className="text-[10px] bg-red-500/10 text-red-400 px-2 py-1 border border-red-500/20 uppercase tracking-widest flex items-center gap-1">
|
|
557
|
+
<AlertCircle size={10} />
|
|
558
|
+
Traceability Gap
|
|
559
|
+
</span>
|
|
560
|
+
)}
|
|
561
|
+
{req.title}
|
|
562
|
+
</h3>
|
|
563
|
+
<OwnershipBadge artifact={req} />
|
|
564
|
+
<MarkdownRenderer content={req.description} />
|
|
565
|
+
</div>
|
|
566
|
+
))}
|
|
567
|
+
</section>
|
|
568
|
+
)}
|
|
569
|
+
|
|
570
|
+
{/* 2. Technical Design */}
|
|
571
|
+
{(!selectedPhase || selectedPhase === 'design') && shouldShow(hasDesign) && (
|
|
572
|
+
<section className="mb-16">
|
|
573
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">2. Technical Design</h2>
|
|
574
|
+
{(filteredGroups.DESIGN?.length === 0 && (filteredGroups.COMPONENT?.length || 0) === 0) && <p className="italic text-white/40 text-xs text-center p-12 bg-white/5 border border-dashed border-white/10 rounded">No architectural design for this phase.</p>}
|
|
575
|
+
{(filteredGroups.DESIGN || [])
|
|
576
|
+
.filter(des => !selectedSubType || des.subType === selectedSubType)
|
|
577
|
+
.map(des => (
|
|
578
|
+
<div key={des.id} id={des.id} className="mb-6 bg-[#0c0c0c] border border-white/10 p-5 pl-6 border-l-[3px] border-l-purple-500/50 hover:bg-white-[0.02] transition-colors">
|
|
579
|
+
<h3 className="font-serif text-lg text-white mb-4 flex items-center gap-3">
|
|
580
|
+
<button
|
|
581
|
+
onClick={() => setSelectedArtifact(des.id)}
|
|
582
|
+
className={`text-[10px] font-mono bg-black px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-purple-500/50 transition-colors ${selectedArtifactId === des.id ? 'text-purple-400 border-purple-500/50' : 'text-white/40'}`}
|
|
583
|
+
>
|
|
584
|
+
{des.id}
|
|
585
|
+
</button>
|
|
586
|
+
{orphanArtifactIds.has(des.id) && (
|
|
587
|
+
<span className="text-[8px] bg-red-500/10 text-red-400 px-1.5 py-0.5 border border-red-500/20 uppercase font-mono tracking-tighter flex items-center gap-1">
|
|
588
|
+
<AlertCircle size={10} />
|
|
589
|
+
Orphan
|
|
590
|
+
</span>
|
|
591
|
+
)}
|
|
592
|
+
{des.title}
|
|
593
|
+
</h3>
|
|
594
|
+
<OwnershipBadge artifact={des} />
|
|
595
|
+
<MarkdownRenderer content={des.description} />
|
|
596
|
+
</div>
|
|
597
|
+
))}
|
|
598
|
+
</section>
|
|
599
|
+
)}
|
|
600
|
+
|
|
601
|
+
{/* 3. Development */}
|
|
602
|
+
{(!selectedPhase || selectedPhase === 'dev') && shouldShow(hasDev) && (
|
|
603
|
+
<section className="mb-16">
|
|
604
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">3. Development</h2>
|
|
605
|
+
<div className="overflow-x-auto rounded border border-white/10">
|
|
606
|
+
<table className="min-w-full border-collapse text-xs">
|
|
607
|
+
<thead className="bg-[#0c0c0c] border-b border-white/10 text-white/40 uppercase tracking-[0.2em] text-[8px] font-bold">
|
|
608
|
+
<tr>
|
|
609
|
+
<th className="p-4 text-left">Code Entity</th>
|
|
610
|
+
<th className="p-4 text-left border-l border-white/5">Mapping</th>
|
|
611
|
+
<th className="p-4 text-left border-l border-white/5 w-1/2">Logic Summary</th>
|
|
612
|
+
</tr>
|
|
613
|
+
</thead>
|
|
614
|
+
<tbody className="divide-y divide-white/5">
|
|
615
|
+
{(filteredGroups.CODE_ENTITY || [])
|
|
616
|
+
.filter(code => code.subType !== 'Review')
|
|
617
|
+
.filter(code => !selectedSubType || code.subType === selectedSubType)
|
|
618
|
+
.map(code => (
|
|
619
|
+
<tr key={code.id} id={code.id} className="bg-transparent hover:bg-white/[0.02] transition-colors group">
|
|
620
|
+
<td className="p-4 font-mono">
|
|
621
|
+
<button
|
|
622
|
+
onClick={() => setSelectedArtifact(code.id)}
|
|
623
|
+
className={`text-left hover:text-emerald-300 transition-colors ${selectedArtifactId === code.id ? 'text-emerald-400' : 'text-emerald-500/70'}`}
|
|
624
|
+
>
|
|
625
|
+
{code.title}
|
|
626
|
+
</button>
|
|
627
|
+
<OwnershipBadge artifact={code} />
|
|
628
|
+
{orphanArtifactIds.has(code.id) && (
|
|
629
|
+
<div className="flex items-center gap-1 text-[8px] text-red-400 mt-0.5 font-bold uppercase tracking-tighter">
|
|
630
|
+
<AlertCircle size={8} />
|
|
631
|
+
Unlinked Entity
|
|
632
|
+
</div>
|
|
633
|
+
)}
|
|
634
|
+
<div className="text-[8px] text-white/20 mt-1 uppercase tracking-tighter">{code.id}</div>
|
|
635
|
+
</td>
|
|
636
|
+
<td className="p-4 border-l border-white/5 text-amber-400/80">{code.subType || '-'}</td>
|
|
637
|
+
<td className="p-4 border-l border-white/5 text-white/60"><MarkdownRenderer content={code.description} /></td>
|
|
638
|
+
</tr>
|
|
639
|
+
))}
|
|
640
|
+
{((filteredGroups.CODE_ENTITY || []).filter(c => c.subType !== 'Review').length === 0) && (
|
|
641
|
+
<tr>
|
|
642
|
+
<td colSpan={3} className="p-12 text-center text-white/20 italic">No code entities documented for this version.</td>
|
|
643
|
+
</tr>
|
|
644
|
+
)}
|
|
645
|
+
</tbody>
|
|
646
|
+
</table>
|
|
647
|
+
</div>
|
|
648
|
+
</section>
|
|
649
|
+
)}
|
|
650
|
+
|
|
651
|
+
{/* 4. Code Review */}
|
|
652
|
+
{(!selectedPhase || selectedPhase === 'review') && shouldShow(hasReview) && (
|
|
653
|
+
<section className="mb-16">
|
|
654
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">4. Code Review</h2>
|
|
655
|
+
{((filteredGroups.CODE_ENTITY || []).filter(c => c.subType === 'Review').length === 0) && <p className="italic text-white/40 text-xs text-center p-8 bg-white/5 border border-dashed border-white/10 rounded">No active reviews for this version.</p>}
|
|
656
|
+
<div className="grid grid-cols-1 gap-4">
|
|
657
|
+
{(filteredGroups.CODE_ENTITY || []).filter(c => c.subType === 'Review').map(rev => (
|
|
658
|
+
<div key={rev.id} id={rev.id} className="bg-[#0c0c0c] border border-white/10 p-5 pl-6 border-l-[3px] border-l-emerald-500/50">
|
|
659
|
+
<h3 className="font-serif text-lg text-white mb-2 flex items-center gap-3">
|
|
660
|
+
<button
|
|
661
|
+
onClick={() => setSelectedArtifact(rev.id)}
|
|
662
|
+
className={`text-[10px] font-mono bg-black px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-emerald-500/50 transition-colors ${selectedArtifactId === rev.id ? 'text-emerald-400 border-emerald-500/50' : 'text-white/40'}`}
|
|
663
|
+
>
|
|
664
|
+
{rev.id}
|
|
665
|
+
</button>
|
|
666
|
+
{rev.title}
|
|
667
|
+
</h3>
|
|
668
|
+
<MarkdownRenderer content={rev.description} />
|
|
669
|
+
</div>
|
|
670
|
+
))}
|
|
671
|
+
</div>
|
|
672
|
+
</section>
|
|
673
|
+
)}
|
|
674
|
+
|
|
675
|
+
{/* 5. Continuous Integration */}
|
|
676
|
+
{(!selectedPhase || selectedPhase === 'ci') && shouldShow(hasCI) && (
|
|
677
|
+
<section className="mb-16">
|
|
678
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">5. Continuous Integration (CI)</h2>
|
|
679
|
+
{((filteredGroups.INFRASTRUCTURE || []).filter(i => i.subType?.includes('CI') || i.subType === 'Pipeline').length === 0) && <p className="italic text-white/40 text-xs p-12 bg-white/5 border border-dashed border-white/10 rounded text-center">No CI pipelines defined.</p>}
|
|
680
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
681
|
+
{(filteredGroups.INFRASTRUCTURE || []).filter(i => i.subType?.includes('CI') || i.subType === 'Pipeline').map(ci => (
|
|
682
|
+
<div key={ci.id} id={ci.id} className="bg-[#0c0c0c] border border-white/10 p-5 hover:bg-white/[0.03] transition-all">
|
|
683
|
+
<h4 className="text-white font-bold mb-2 flex items-center gap-2">
|
|
684
|
+
<Repeat size={14} className="text-emerald-400" />
|
|
685
|
+
{ci.title}
|
|
686
|
+
</h4>
|
|
687
|
+
<MarkdownRenderer content={ci.description} />
|
|
688
|
+
</div>
|
|
689
|
+
))}
|
|
690
|
+
</div>
|
|
691
|
+
</section>
|
|
692
|
+
)}
|
|
693
|
+
|
|
694
|
+
{/* 6. Testing */}
|
|
695
|
+
{(!selectedPhase || selectedPhase === 'verif') && shouldShow(hasTest) && (
|
|
696
|
+
<section className="mb-16">
|
|
697
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">6. Verification & Quality</h2>
|
|
698
|
+
<div className="space-y-4">
|
|
699
|
+
{(filteredGroups.TEST || []).map(test => (
|
|
700
|
+
<div key={test.id} id={test.id} className="bg-[#0c0c0c] border border-white/10 p-5 border-l-[3px] border-l-rose-500/50">
|
|
701
|
+
<h3 className="font-serif text-[#e0e0e0] mb-2 flex items-center gap-3">
|
|
702
|
+
<button
|
|
703
|
+
onClick={() => setSelectedArtifact(test.id)}
|
|
704
|
+
className={`text-[10px] font-mono bg-black px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-rose-500/50 transition-colors ${selectedArtifactId === test.id ? 'text-rose-400 border-rose-500/50' : 'text-white/40'}`}
|
|
705
|
+
>
|
|
706
|
+
{test.id}
|
|
707
|
+
</button>
|
|
708
|
+
{orphanArtifactIds.has(test.id) && (
|
|
709
|
+
<span className="text-[10px] bg-red-500/10 text-red-400 px-2 py-1 border border-red-500/20 uppercase tracking-widest flex items-center gap-1">
|
|
710
|
+
<AlertCircle size={10} />
|
|
711
|
+
Gap
|
|
712
|
+
</span>
|
|
713
|
+
)}
|
|
714
|
+
{test.title}
|
|
715
|
+
</h3>
|
|
716
|
+
<OwnershipBadge artifact={test} />
|
|
717
|
+
<MarkdownRenderer content={test.description} />
|
|
718
|
+
</div>
|
|
719
|
+
))}
|
|
720
|
+
{(filteredGroups.TEST || []).length === 0 && <p className="italic text-white/40 text-xs">No verification artifacts recorded.</p>}
|
|
721
|
+
</div>
|
|
722
|
+
</section>
|
|
723
|
+
)}
|
|
724
|
+
|
|
725
|
+
{/* 7. Build */}
|
|
726
|
+
{(!selectedPhase || selectedPhase === 'build') && shouldShow(hasBuild) && (
|
|
727
|
+
<section className="mb-16">
|
|
728
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">7. Build & Packaging</h2>
|
|
729
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
730
|
+
{(filteredGroups.INFRASTRUCTURE || []).filter(i => i.subType === 'Build' || i.subType === 'Package').map(b => (
|
|
731
|
+
<div key={b.id} id={b.id} className="bg-[#0c0c0c] border border-white/10 p-5 border-l-[3px] border-l-amber-500/50">
|
|
732
|
+
<h4 className="text-white font-bold mb-2 flex items-center gap-2">
|
|
733
|
+
<Box size={14} className="text-amber-400" />
|
|
734
|
+
{b.title}
|
|
735
|
+
</h4>
|
|
736
|
+
<OwnershipBadge artifact={b} />
|
|
737
|
+
<MarkdownRenderer content={b.description} />
|
|
738
|
+
</div>
|
|
739
|
+
))}
|
|
740
|
+
{((filteredGroups.INFRASTRUCTURE || []).filter(i => i.subType === 'Build' || i.subType === 'Package').length === 0) && <p className="italic text-white/40 text-xs col-span-2">Build configurations are handled externally.</p>}
|
|
741
|
+
</div>
|
|
742
|
+
</section>
|
|
743
|
+
)}
|
|
744
|
+
|
|
745
|
+
{/* 8. Deployment */}
|
|
746
|
+
{(!selectedPhase || selectedPhase === 'deploy') && shouldShow(hasDeploy) && (
|
|
747
|
+
<section className="mb-16">
|
|
748
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">8. Deployment</h2>
|
|
749
|
+
<div className="space-y-6">
|
|
750
|
+
{(filteredGroups.DEPLOYMENT || []).map(d => (
|
|
751
|
+
<div key={d.id} id={d.id} className="bg-[#0c0c0c] border border-white/10 p-6 rounded-sm border-l-[3px] border-l-sky-500/50">
|
|
752
|
+
<h3 className="text-xl font-serif italic text-white mb-4 flex items-center gap-3">
|
|
753
|
+
<Rocket size={20} className="text-sky-400" />
|
|
754
|
+
<button
|
|
755
|
+
onClick={() => setSelectedArtifact(d.id)}
|
|
756
|
+
className={`text-[10px] font-mono bg-black px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-sky-500/50 transition-colors ${selectedArtifactId === d.id ? 'text-sky-400 border-sky-500/50' : 'text-white/40'}`}
|
|
757
|
+
>
|
|
758
|
+
{d.id}
|
|
759
|
+
</button>
|
|
760
|
+
{orphanArtifactIds.has(d.id) && (
|
|
761
|
+
<span className="text-[10px] bg-red-500/10 text-red-400 px-2 py-1 border border-red-500/20 uppercase tracking-widest flex items-center gap-1">
|
|
762
|
+
<AlertCircle size={10} />
|
|
763
|
+
Unlinked
|
|
764
|
+
</span>
|
|
765
|
+
)}
|
|
766
|
+
{d.title}
|
|
767
|
+
</h3>
|
|
768
|
+
<OwnershipBadge artifact={d} />
|
|
769
|
+
<MarkdownRenderer content={d.description} />
|
|
770
|
+
</div>
|
|
771
|
+
))}
|
|
772
|
+
{(filteredGroups.DEPLOYMENT || []).length === 0 && <p className="italic text-white/40 text-xs">No deployment records found.</p>}
|
|
773
|
+
</div>
|
|
774
|
+
</section>
|
|
775
|
+
)}
|
|
776
|
+
|
|
777
|
+
{/* 9. Monitoring */}
|
|
778
|
+
{(!selectedPhase || selectedPhase === 'monitor') && shouldShow(hasMonit) && (
|
|
779
|
+
<section className="mb-16">
|
|
780
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">9. Monitoring & Observability</h2>
|
|
781
|
+
<div className="grid grid-cols-1 gap-6">
|
|
782
|
+
{(filteredGroups.MONITORING || []).map(m => (
|
|
783
|
+
<div key={m.id} id={m.id} className="bg-black/40 border border-white/10 p-6 rounded relative overflow-hidden">
|
|
784
|
+
<div className="absolute top-0 right-0 p-4 opacity-5">
|
|
785
|
+
<Activity size={80} />
|
|
786
|
+
</div>
|
|
787
|
+
<h3 className="text-white font-bold mb-3 flex items-center gap-2">
|
|
788
|
+
<Activity size={16} className="text-red-400" />
|
|
789
|
+
<button
|
|
790
|
+
onClick={() => setSelectedArtifact(m.id)}
|
|
791
|
+
className={`text-[10px] font-mono bg-black/40 px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-red-400/50 transition-colors ${selectedArtifactId === m.id ? 'text-red-400 border-red-500/50' : 'text-white/40'}`}
|
|
792
|
+
>
|
|
793
|
+
{m.id}
|
|
794
|
+
</button>
|
|
795
|
+
{orphanArtifactIds.has(m.id) && (
|
|
796
|
+
<span className="text-[10px] bg-red-500/10 text-red-400 px-2 py-1 border border-red-500/20 uppercase tracking-widest flex items-center gap-1">
|
|
797
|
+
<AlertCircle size={10} />
|
|
798
|
+
Unlinked
|
|
799
|
+
</span>
|
|
800
|
+
)}
|
|
801
|
+
{m.title}
|
|
802
|
+
</h3>
|
|
803
|
+
<OwnershipBadge artifact={m} />
|
|
804
|
+
<MarkdownRenderer content={m.description} />
|
|
805
|
+
</div>
|
|
806
|
+
))}
|
|
807
|
+
{(filteredGroups.INCIDENT || []).length > 0 && (
|
|
808
|
+
<div className="mt-8">
|
|
809
|
+
<h4 className="text-[10px] uppercase tracking-widest text-red-500/60 font-bold mb-4">Historical Incidents (Feedback Loop)</h4>
|
|
810
|
+
<div className="space-y-3">
|
|
811
|
+
{filteredGroups.INCIDENT.map(inc => (
|
|
812
|
+
<div key={inc.id} id={inc.id} className="bg-red-500/5 border border-red-500/10 p-4 rounded text-xs">
|
|
813
|
+
<span className="font-bold text-red-400 mr-2">[{inc.id}]</span>
|
|
814
|
+
<span className="text-white/80">{inc.title}</span>
|
|
815
|
+
</div>
|
|
816
|
+
))}
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
)}
|
|
820
|
+
</div>
|
|
821
|
+
</section>
|
|
822
|
+
)}
|
|
823
|
+
|
|
824
|
+
{/* 10. Maintenance */}
|
|
825
|
+
{(!selectedPhase || selectedPhase === 'maint') && shouldShow(hasMaint) && (
|
|
826
|
+
<section className="mb-16">
|
|
827
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">10. Maintenance / Refactoring</h2>
|
|
828
|
+
<div className="space-y-4">
|
|
829
|
+
{(filteredGroups.MAINTENANCE || []).filter(m => m.subType !== 'Retirement').map(m => (
|
|
830
|
+
<div key={m.id} id={m.id} className="bg-[#0c0c0c] border border-white/10 p-5 border-l-[3px] border-l-indigo-500/50">
|
|
831
|
+
<h3 className="text-white font-bold mb-2 flex items-center gap-2">
|
|
832
|
+
<Wrench size={14} className="text-indigo-400" />
|
|
833
|
+
{m.title}
|
|
834
|
+
</h3>
|
|
835
|
+
<OwnershipBadge artifact={m} />
|
|
836
|
+
<MarkdownRenderer content={m.description} />
|
|
837
|
+
</div>
|
|
838
|
+
))}
|
|
839
|
+
{((filteredGroups.MAINTENANCE || []).filter(m => m.subType !== 'Retirement').length === 0) && <p className="italic text-white/40 text-xs">System is in active health. No maintenance tasks scheduled.</p>}
|
|
840
|
+
</div>
|
|
841
|
+
</section>
|
|
842
|
+
)}
|
|
843
|
+
|
|
844
|
+
{/* 11. Retirement */}
|
|
845
|
+
{(!selectedPhase || selectedPhase === 'retire') && shouldShow(hasRet) && (
|
|
846
|
+
<section className="mb-16">
|
|
847
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">11. Retirement / Replacement</h2>
|
|
848
|
+
<div className="space-y-4">
|
|
849
|
+
{(filteredGroups.MAINTENANCE || []).filter(m => m.subType === 'Retirement').map(m => (
|
|
850
|
+
<div key={m.id} id={m.id} className="bg-[#1a0a0a] border border-red-900/30 p-5 border-l-[3px] border-l-red-900">
|
|
851
|
+
<h3 className="text-white/60 font-bold mb-2 flex items-center gap-2">
|
|
852
|
+
<Trash2 size={14} className="text-red-900" />
|
|
853
|
+
{m.title}
|
|
854
|
+
</h3>
|
|
855
|
+
<OwnershipBadge artifact={m} />
|
|
856
|
+
<MarkdownRenderer content={m.description} />
|
|
857
|
+
</div>
|
|
858
|
+
))}
|
|
859
|
+
{((filteredGroups.MAINTENANCE || []).filter(m => m.subType === 'Retirement').length === 0) && <p className="italic text-white/40 text-xs">No retired components for this version.</p>}
|
|
860
|
+
</div>
|
|
861
|
+
</section>
|
|
862
|
+
)}
|
|
863
|
+
|
|
864
|
+
{/* 12. Releases & Versions */}
|
|
865
|
+
{(!selectedPhase || selectedPhase === 'versions') && (
|
|
866
|
+
<section className="mb-16">
|
|
867
|
+
<h2 className="text-[10px] uppercase tracking-widest text-white/40 mb-6 bg-white/5 p-3 border-l-2 border-white/20">12. Releases & Versions</h2>
|
|
868
|
+
<div className="space-y-4">
|
|
869
|
+
{[...(grouped.VERSION || []), ...(grouped.SYSTEM_VERSION || [])].map(v => (
|
|
870
|
+
<div key={v.id} id={v.id} className="bg-[#0c0c0c] border border-white/10 p-5 border-l-[3px] border-l-emerald-500/50">
|
|
871
|
+
<h3 className="font-serif text-lg text-white mb-2 flex items-center gap-3">
|
|
872
|
+
<Milestone size={16} className="text-emerald-400" />
|
|
873
|
+
<button
|
|
874
|
+
onClick={() => setSelectedArtifact(v.id)}
|
|
875
|
+
className={`text-[10px] font-mono bg-black px-2 py-1 border border-white/5 uppercase tracking-widest hover:border-emerald-500/50 transition-colors ${selectedArtifactId === v.id ? 'text-emerald-400 border-emerald-500/50' : 'text-white/40'}`}
|
|
876
|
+
>
|
|
877
|
+
{v.id}
|
|
878
|
+
</button>
|
|
879
|
+
<span>{v.title}</span>
|
|
880
|
+
{v.type === 'SYSTEM_VERSION' && (
|
|
881
|
+
<span className="ml-auto text-xs font-mono text-emerald-400 border border-emerald-500/30 bg-emerald-500/10 px-2 py-1 rounded">
|
|
882
|
+
{v.component} v{v.version}
|
|
883
|
+
</span>
|
|
884
|
+
)}
|
|
885
|
+
{v.type === 'VERSION' && (
|
|
886
|
+
<span className="ml-auto text-xs font-mono text-emerald-400 border border-emerald-500/30 bg-emerald-500/10 px-2 py-1 rounded">
|
|
887
|
+
v{v.version}
|
|
888
|
+
</span>
|
|
889
|
+
)}
|
|
890
|
+
</h3>
|
|
891
|
+
<OwnershipBadge artifact={v} />
|
|
892
|
+
<MarkdownRenderer content={v.description} />
|
|
893
|
+
</div>
|
|
894
|
+
))}
|
|
895
|
+
{([...(grouped.VERSION || []), ...(grouped.SYSTEM_VERSION || [])].length === 0) && <p className="italic text-white/40 text-xs">No version artifacts.</p>}
|
|
896
|
+
</div>
|
|
897
|
+
</section>
|
|
898
|
+
)}
|
|
899
|
+
</div>
|
|
900
|
+
</div>
|
|
901
|
+
</div>
|
|
902
|
+
</div>
|
|
903
|
+
);
|
|
904
|
+
};
|
|
905
|
+
|