Skip to content

Commit 5514e1c

Browse files
WiP
1 parent 1b40c63 commit 5514e1c

File tree

2 files changed

+211
-1
lines changed

2 files changed

+211
-1
lines changed

public/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@
166166
"componentsTitle": "Components",
167167
"crossplaneTitle": "Crossplane",
168168
"gitOpsTitle": "GitOps",
169-
"landscapersTitle": "Landscapers"
169+
"landscapersTitle": "Landscapers",
170+
"graphTitle": "Graph"
170171
},
171172
"ToastContext": {
172173
"errorMessage": "useToast must be used within a ToastProvider"

src/components/Graphs/Flow.tsx

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import React, { useCallback } from 'react';
2+
import ReactFlow, {
3+
Background,
4+
Controls,
5+
Edge,
6+
Node,
7+
Connection,
8+
addEdge,
9+
useNodesState,
10+
useEdgesState,
11+
MarkerType,
12+
Position,
13+
} from 'reactflow';
14+
import dagre from 'dagre';
15+
import 'reactflow/dist/style.css';
16+
17+
const nodeWidth = 200;
18+
const nodeHeight = 80;
19+
20+
const sourceColors: Record<string, string> = {
21+
'gitops-btp': '#28a745',
22+
'gitops-btp-source': '#f0ad4e',
23+
'gitops-btp-source-two': '#f06e4e',
24+
'gitops-ias-argo-source': '#e67aff',
25+
};
26+
27+
type TreeNodeData = {
28+
id: string;
29+
label: string;
30+
parentId?: string;
31+
type?: string;
32+
source?: string;
33+
status?: 'ok' | 'error';
34+
};
35+
36+
const data: TreeNodeData[] = [
37+
{ id: '1', label: 'GlobalAccount', source: 'gitops-btp-source' },
38+
{ id: '2', label: 'Subaccount', parentId: '1', source: 'gitops-btp-source' },
39+
{
40+
id: '3',
41+
label: 'Entitlement A',
42+
parentId: '2',
43+
source: 'gitops-argo-source',
44+
},
45+
{
46+
id: '4',
47+
label: 'Entitlement B',
48+
parentId: '2',
49+
source: 'gitops-argo-source',
50+
status: 'error',
51+
},
52+
{ id: '5', label: 'GlobalAccount 2', source: 'gitops-ias-argo-source' },
53+
{
54+
id: '6',
55+
label: 'Subaccount 2',
56+
parentId: '5',
57+
source: 'gitops-ias-argo-source',
58+
},
59+
{ id: '7', label: 'App A', parentId: '6', source: 'gitops-btp-source-two' },
60+
{ id: '8', label: 'App B', parentId: '6', source: 'gitops-btp-source-two' },
61+
];
62+
63+
const createGraphLayout = (
64+
nodeData: TreeNodeData[],
65+
): { nodes: Node[]; edges: Edge[] } => {
66+
const dagreGraph = new dagre.graphlib.Graph();
67+
dagreGraph.setDefaultEdgeLabel(() => ({}));
68+
dagreGraph.setGraph({ rankdir: 'TB' });
69+
70+
const nodes: Node[] = nodeData.map((n) => {
71+
const color = sourceColors[n.source || ''] || '#ccc';
72+
const node: Node = {
73+
id: n.id,
74+
data: {
75+
label: n.label,
76+
type: n.type,
77+
source: n.source,
78+
status: n.status,
79+
},
80+
style: {
81+
border: `2px solid ${color}`,
82+
borderRadius: 8,
83+
padding: 10,
84+
backgroundColor: '#fff',
85+
boxShadow: n.status === 'error' ? '0 0 0 2px red inset' : '',
86+
},
87+
width: nodeWidth,
88+
height: nodeHeight,
89+
position: { x: 0, y: 0 },
90+
};
91+
92+
dagreGraph.setNode(n.id, { width: nodeWidth, height: nodeHeight });
93+
94+
return node;
95+
});
96+
97+
const edges: Edge[] = nodeData
98+
.filter((n) => n.parentId)
99+
.map((n) => {
100+
dagreGraph.setEdge(n.parentId!, n.id);
101+
return {
102+
id: `e-${n.parentId}-${n.id}`,
103+
source: n.parentId!,
104+
target: n.id,
105+
markerEnd: { type: MarkerType.ArrowClosed },
106+
};
107+
});
108+
109+
dagre.layout(dagreGraph);
110+
111+
nodes.forEach((node) => {
112+
const pos = dagreGraph.node(node.id);
113+
node.position = {
114+
x: pos.x - nodeWidth / 2,
115+
y: pos.y - nodeHeight / 2,
116+
};
117+
node.sourcePosition = Position.Bottom;
118+
node.targetPosition = Position.Top;
119+
});
120+
121+
return { nodes, edges };
122+
};
123+
124+
const Legend = ({ sources }: { sources: string[] }) => (
125+
<div
126+
style={{
127+
padding: '1rem',
128+
minWidth: 240,
129+
maxWidth: 300,
130+
maxHeight: 280,
131+
border: '1px solid #ccc',
132+
borderRadius: 8,
133+
backgroundColor: '#fff',
134+
margin: '1rem',
135+
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.05)',
136+
overflow: 'auto',
137+
alignSelf: 'flex-start',
138+
}}
139+
>
140+
<h4 style={{ marginBottom: 10 }}>Legenda:</h4>
141+
{sources.map((source) => (
142+
<div
143+
key={source}
144+
style={{
145+
display: 'flex',
146+
alignItems: 'center',
147+
marginBottom: 8,
148+
}}
149+
>
150+
<div
151+
style={{
152+
width: 16,
153+
height: 16,
154+
backgroundColor: sourceColors[source],
155+
marginRight: 8,
156+
borderRadius: 3,
157+
border: '1px solid #999',
158+
}}
159+
/>
160+
<span>{source}</span>
161+
</div>
162+
))}
163+
</div>
164+
);
165+
166+
const Flow: React.FC = () => {
167+
const { nodes: layoutedNodes, edges: layoutedEdges } =
168+
createGraphLayout(data);
169+
const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
170+
const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
171+
172+
const onConnect = useCallback(
173+
(params: Edge | Connection) => setEdges((eds) => addEdge(params, eds)),
174+
[setEdges],
175+
);
176+
177+
return (
178+
<div
179+
style={{
180+
display: 'flex',
181+
height: '600px',
182+
border: '1px solid #ddd',
183+
borderRadius: 8,
184+
overflow: 'hidden',
185+
fontFamily: 'sans-serif',
186+
backgroundColor: '#fafafa',
187+
}}
188+
>
189+
<div style={{ flex: 1 }}>
190+
<ReactFlow
191+
nodes={nodes}
192+
edges={edges}
193+
fitView
194+
nodesDraggable={false}
195+
nodesConnectable={false}
196+
elementsSelectable={false}
197+
zoomOnScroll={true}
198+
panOnDrag={true}
199+
>
200+
<Controls />
201+
<Background />
202+
</ReactFlow>
203+
</div>
204+
<Legend sources={Object.keys(sourceColors)} />
205+
</div>
206+
);
207+
};
208+
209+
export default Flow;

0 commit comments

Comments
 (0)