@alepha/devtools 0.13.5 → 0.13.7
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/dist/{index.d.mts → index.d.ts} +250 -32
- package/dist/{index.mjs → index.js} +254 -22
- package/dist/index.js.map +1 -0
- package/package.json +12 -6
- package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
- package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
- package/src/api/schemas/DevAtomMetadata.ts +26 -0
- package/src/api/schemas/DevCommandMetadata.ts +9 -0
- package/src/api/schemas/DevEntityMetadata.ts +57 -0
- package/src/api/schemas/DevEnvMetadata.ts +22 -0
- package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
- package/src/api/schemas/DevRouteMetadata.ts +8 -0
- package/src/index.ts +23 -16
- package/src/ui/AppRouter.tsx +85 -2
- package/src/ui/components/DevAtomsViewer.tsx +636 -0
- package/src/ui/components/DevCacheInspector.tsx +423 -0
- package/src/ui/components/DevDashboard.tsx +188 -0
- package/src/ui/components/DevEnvExplorer.tsx +462 -0
- package/src/ui/components/DevLayout.tsx +65 -4
- package/src/ui/components/DevLogViewer.tsx +161 -163
- package/src/ui/components/DevQueueMonitor.tsx +51 -0
- package/src/ui/components/DevTopicsViewer.tsx +690 -0
- package/src/ui/components/actions/ActionGroup.tsx +37 -0
- package/src/ui/components/actions/ActionItem.tsx +138 -0
- package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
- package/src/ui/components/actions/MethodBadge.tsx +18 -0
- package/src/ui/components/actions/SchemaViewer.tsx +21 -0
- package/src/ui/components/actions/TryItPanel.tsx +140 -0
- package/src/ui/components/actions/constants.ts +7 -0
- package/src/ui/components/actions/helpers.ts +18 -0
- package/src/ui/components/actions/index.ts +8 -0
- package/src/ui/components/db/ColumnBadge.tsx +55 -0
- package/src/ui/components/db/DevDbStudio.tsx +485 -0
- package/src/ui/components/db/constants.ts +11 -0
- package/src/ui/components/db/index.ts +4 -0
- package/src/ui/components/db/types.ts +7 -0
- package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
- package/src/ui/components/graph/GraphControls.tsx +162 -0
- package/src/ui/components/graph/NodeDetails.tsx +181 -0
- package/src/ui/components/graph/ProviderNode.tsx +97 -0
- package/src/ui/components/graph/constants.ts +35 -0
- package/src/ui/components/graph/helpers.ts +443 -0
- package/src/ui/components/graph/index.ts +7 -0
- package/src/ui/components/graph/types.ts +28 -0
- package/src/ui/styles.css +0 -6
- package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
- package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
- package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
- package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
- /package/src/{entities → api/entities}/logs.ts +0 -0
- /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
- /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
- /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
- /package/src/{schemas → api/schemas}/DevTopicMetadata.ts +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { useInject } from "@alepha/react";
|
|
2
|
+
import { ui } from "@alepha/ui";
|
|
3
|
+
import {
|
|
4
|
+
ActionIcon,
|
|
5
|
+
Alert,
|
|
6
|
+
Badge,
|
|
7
|
+
Box,
|
|
8
|
+
Flex,
|
|
9
|
+
Group,
|
|
10
|
+
Loader,
|
|
11
|
+
Stack,
|
|
12
|
+
Text,
|
|
13
|
+
Tooltip,
|
|
14
|
+
} from "@mantine/core";
|
|
15
|
+
import {
|
|
16
|
+
IconAlertTriangle,
|
|
17
|
+
IconFocusCentered,
|
|
18
|
+
IconLock,
|
|
19
|
+
IconLockOpen,
|
|
20
|
+
IconMinus,
|
|
21
|
+
IconPlus,
|
|
22
|
+
IconTopologyRing,
|
|
23
|
+
} from "@tabler/icons-react";
|
|
24
|
+
import {
|
|
25
|
+
Background,
|
|
26
|
+
BackgroundVariant,
|
|
27
|
+
MiniMap,
|
|
28
|
+
ReactFlow,
|
|
29
|
+
ReactFlowProvider,
|
|
30
|
+
useEdgesState,
|
|
31
|
+
useNodesState,
|
|
32
|
+
useReactFlow,
|
|
33
|
+
} from "@xyflow/react";
|
|
34
|
+
import "@xyflow/react/dist/style.css";
|
|
35
|
+
import { HttpClient } from "alepha/server";
|
|
36
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
37
|
+
import { devMetadataSchema } from "../../../api/schemas/DevMetadata.ts";
|
|
38
|
+
import type { DevProviderMetadata } from "../../../api/schemas/DevProviderMetadata.ts";
|
|
39
|
+
import { getModuleColor } from "./constants.ts";
|
|
40
|
+
import { GraphControls } from "./GraphControls.tsx";
|
|
41
|
+
import {
|
|
42
|
+
applyLayout,
|
|
43
|
+
buildGraph,
|
|
44
|
+
detectCircularDependencies,
|
|
45
|
+
findDependencyChain,
|
|
46
|
+
} from "./helpers.ts";
|
|
47
|
+
import { NodeDetails } from "./NodeDetails.tsx";
|
|
48
|
+
import { ProviderNode } from "./ProviderNode.tsx";
|
|
49
|
+
import type {
|
|
50
|
+
GraphFilters,
|
|
51
|
+
LayoutType,
|
|
52
|
+
ProviderEdge,
|
|
53
|
+
ProviderNodeData,
|
|
54
|
+
ProviderNode as ProviderNodeType,
|
|
55
|
+
} from "./types.ts";
|
|
56
|
+
|
|
57
|
+
const nodeTypes = {
|
|
58
|
+
provider: ProviderNode,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const FlowControls = () => {
|
|
62
|
+
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
|
63
|
+
const [isLocked, setIsLocked] = useState(false);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Stack
|
|
67
|
+
gap={4}
|
|
68
|
+
style={{
|
|
69
|
+
position: "absolute",
|
|
70
|
+
bottom: 16,
|
|
71
|
+
left: 16,
|
|
72
|
+
zIndex: 10,
|
|
73
|
+
backgroundColor: ui.colors.elevated,
|
|
74
|
+
border: `1px solid ${ui.colors.border}`,
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
padding: 4,
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
<Tooltip label="Zoom in" position="right">
|
|
80
|
+
<ActionIcon size="sm" variant="subtle" onClick={() => zoomIn()}>
|
|
81
|
+
<IconPlus size={14} />
|
|
82
|
+
</ActionIcon>
|
|
83
|
+
</Tooltip>
|
|
84
|
+
<Tooltip label="Zoom out" position="right">
|
|
85
|
+
<ActionIcon size="sm" variant="subtle" onClick={() => zoomOut()}>
|
|
86
|
+
<IconMinus size={14} />
|
|
87
|
+
</ActionIcon>
|
|
88
|
+
</Tooltip>
|
|
89
|
+
<Tooltip label="Fit view" position="right">
|
|
90
|
+
<ActionIcon
|
|
91
|
+
size="sm"
|
|
92
|
+
variant="subtle"
|
|
93
|
+
onClick={() => fitView({ padding: 0.2 })}
|
|
94
|
+
>
|
|
95
|
+
<IconFocusCentered size={14} />
|
|
96
|
+
</ActionIcon>
|
|
97
|
+
</Tooltip>
|
|
98
|
+
<Tooltip label={isLocked ? "Unlock" : "Lock"} position="right">
|
|
99
|
+
<ActionIcon
|
|
100
|
+
size="sm"
|
|
101
|
+
variant="subtle"
|
|
102
|
+
onClick={() => setIsLocked(!isLocked)}
|
|
103
|
+
>
|
|
104
|
+
{isLocked ? <IconLock size={14} /> : <IconLockOpen size={14} />}
|
|
105
|
+
</ActionIcon>
|
|
106
|
+
</Tooltip>
|
|
107
|
+
</Stack>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const DevDependencyGraph = () => {
|
|
112
|
+
const http = useInject(HttpClient);
|
|
113
|
+
const [providers, setProviders] = useState<DevProviderMetadata[]>([]);
|
|
114
|
+
const [loading, setLoading] = useState(true);
|
|
115
|
+
const [nodes, setNodes, onNodesChange] = useNodesState<ProviderNodeType>([]);
|
|
116
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState<ProviderEdge>([]);
|
|
117
|
+
const [selectedNode, setSelectedNode] = useState<{
|
|
118
|
+
id: string;
|
|
119
|
+
data: ProviderNodeData;
|
|
120
|
+
} | null>(null);
|
|
121
|
+
const [layout, setLayout] = useState<LayoutType>("dagre");
|
|
122
|
+
const [filters, setFilters] = useState<GraphFilters>({
|
|
123
|
+
search: "",
|
|
124
|
+
module: "all",
|
|
125
|
+
hideFramework: false,
|
|
126
|
+
viewMode: "modules",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Fetch data
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
http
|
|
132
|
+
.fetch("/devtools/api/metadata", {
|
|
133
|
+
schema: { response: devMetadataSchema },
|
|
134
|
+
})
|
|
135
|
+
.then((res) => {
|
|
136
|
+
setProviders(res.data.providers);
|
|
137
|
+
setLoading(false);
|
|
138
|
+
});
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
// Get unique modules
|
|
142
|
+
const modules = useMemo(() => {
|
|
143
|
+
const moduleSet = new Set<string>();
|
|
144
|
+
for (const p of providers) {
|
|
145
|
+
if (p.module) moduleSet.add(p.module);
|
|
146
|
+
}
|
|
147
|
+
return Array.from(moduleSet).sort();
|
|
148
|
+
}, [providers]);
|
|
149
|
+
|
|
150
|
+
// Build and layout graph
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
if (providers.length === 0) return;
|
|
153
|
+
|
|
154
|
+
const { nodes: rawNodes, edges: rawEdges } = buildGraph(providers, filters);
|
|
155
|
+
const layoutedNodes = applyLayout(rawNodes, rawEdges, layout);
|
|
156
|
+
|
|
157
|
+
setNodes(layoutedNodes);
|
|
158
|
+
setEdges(rawEdges);
|
|
159
|
+
}, [providers, filters, layout, setNodes, setEdges]);
|
|
160
|
+
|
|
161
|
+
// Detect circular dependencies
|
|
162
|
+
const circularDeps = useMemo(() => {
|
|
163
|
+
return detectCircularDependencies(nodes as ProviderNodeType[], edges);
|
|
164
|
+
}, [nodes, edges]);
|
|
165
|
+
|
|
166
|
+
// Handle node click - highlight dependency chain
|
|
167
|
+
const handleNodeClick = useCallback(
|
|
168
|
+
(_: React.MouseEvent, node: ProviderNodeType) => {
|
|
169
|
+
setSelectedNode({ id: node.id, data: node.data });
|
|
170
|
+
|
|
171
|
+
const chain = findDependencyChain(
|
|
172
|
+
node.id,
|
|
173
|
+
nodes as ProviderNodeType[],
|
|
174
|
+
edges,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Update nodes with highlight state
|
|
178
|
+
setNodes((nds) =>
|
|
179
|
+
nds.map((n) => ({
|
|
180
|
+
...n,
|
|
181
|
+
data: {
|
|
182
|
+
...n.data,
|
|
183
|
+
isHighlighted: chain.has(n.id),
|
|
184
|
+
isSelected: n.id === node.id,
|
|
185
|
+
isFaded: !chain.has(n.id),
|
|
186
|
+
},
|
|
187
|
+
})),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
// Update edges with highlight state
|
|
191
|
+
setEdges((eds) =>
|
|
192
|
+
eds.map((e) => ({
|
|
193
|
+
...e,
|
|
194
|
+
animated: chain.has(e.source) && chain.has(e.target),
|
|
195
|
+
style: {
|
|
196
|
+
...e.style,
|
|
197
|
+
stroke:
|
|
198
|
+
chain.has(e.source) && chain.has(e.target)
|
|
199
|
+
? getModuleColor(
|
|
200
|
+
(nodes as ProviderNodeType[]).find((n) => n.id === e.source)
|
|
201
|
+
?.data.module,
|
|
202
|
+
)
|
|
203
|
+
: "#495057",
|
|
204
|
+
strokeWidth: chain.has(e.source) && chain.has(e.target) ? 2 : 1,
|
|
205
|
+
opacity: chain.has(e.source) && chain.has(e.target) ? 1 : 0.3,
|
|
206
|
+
},
|
|
207
|
+
})),
|
|
208
|
+
);
|
|
209
|
+
},
|
|
210
|
+
[nodes, edges, setNodes, setEdges],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Handle pane click - clear selection
|
|
214
|
+
const handlePaneClick = useCallback(() => {
|
|
215
|
+
setSelectedNode(null);
|
|
216
|
+
setNodes((nds) =>
|
|
217
|
+
nds.map((n) => ({
|
|
218
|
+
...n,
|
|
219
|
+
data: {
|
|
220
|
+
...n.data,
|
|
221
|
+
isHighlighted: false,
|
|
222
|
+
isSelected: false,
|
|
223
|
+
isFaded: false,
|
|
224
|
+
},
|
|
225
|
+
})),
|
|
226
|
+
);
|
|
227
|
+
setEdges((eds) =>
|
|
228
|
+
eds.map((e) => ({
|
|
229
|
+
...e,
|
|
230
|
+
animated: false,
|
|
231
|
+
style: { ...e.style, stroke: "#495057", strokeWidth: 1.5, opacity: 1 },
|
|
232
|
+
})),
|
|
233
|
+
);
|
|
234
|
+
}, [setNodes, setEdges]);
|
|
235
|
+
|
|
236
|
+
// Handle node click from details panel
|
|
237
|
+
const handleDetailsNodeClick = useCallback(
|
|
238
|
+
(nodeId: string) => {
|
|
239
|
+
const node = nodes.find((n) => n.id === nodeId) as
|
|
240
|
+
| ProviderNodeType
|
|
241
|
+
| undefined;
|
|
242
|
+
if (node) {
|
|
243
|
+
handleNodeClick({} as React.MouseEvent, node);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
[nodes, handleNodeClick],
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// Export as PNG
|
|
250
|
+
const handleExport = useCallback(() => {
|
|
251
|
+
// Get the react-flow viewport element
|
|
252
|
+
const viewport = document.querySelector(".react-flow__viewport");
|
|
253
|
+
if (!viewport) return;
|
|
254
|
+
|
|
255
|
+
// Use html2canvas or similar - for now just alert
|
|
256
|
+
alert("Export feature requires html2canvas library. Coming soon!");
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
if (loading) {
|
|
260
|
+
return (
|
|
261
|
+
<Flex align="center" justify="center" h="100%">
|
|
262
|
+
<Loader size="sm" />
|
|
263
|
+
</Flex>
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<Flex direction="column" gap="md" w="100%" h="100%">
|
|
269
|
+
<Group justify="space-between" wrap="nowrap">
|
|
270
|
+
<Group gap="sm">
|
|
271
|
+
<IconTopologyRing size={24} opacity={0.7} />
|
|
272
|
+
<Text size="lg" fw={500}>
|
|
273
|
+
Dependency Graph
|
|
274
|
+
</Text>
|
|
275
|
+
</Group>
|
|
276
|
+
</Group>
|
|
277
|
+
|
|
278
|
+
<GraphControls
|
|
279
|
+
filters={filters}
|
|
280
|
+
onFiltersChange={setFilters}
|
|
281
|
+
layout={layout}
|
|
282
|
+
onLayoutChange={setLayout}
|
|
283
|
+
modules={modules}
|
|
284
|
+
nodeCount={nodes.length}
|
|
285
|
+
edgeCount={edges.length}
|
|
286
|
+
onExport={handleExport}
|
|
287
|
+
/>
|
|
288
|
+
|
|
289
|
+
{circularDeps.length > 0 && (
|
|
290
|
+
<Alert
|
|
291
|
+
icon={<IconAlertTriangle size={16} />}
|
|
292
|
+
color="orange"
|
|
293
|
+
variant="light"
|
|
294
|
+
p="xs"
|
|
295
|
+
>
|
|
296
|
+
<Flex align="center" gap="xs">
|
|
297
|
+
<Text size="xs">Circular dependencies detected:</Text>
|
|
298
|
+
{circularDeps.slice(0, 3).map((cycle, i) => (
|
|
299
|
+
<Badge key={i} size="xs" color="orange" variant="light">
|
|
300
|
+
{cycle.length} nodes
|
|
301
|
+
</Badge>
|
|
302
|
+
))}
|
|
303
|
+
{circularDeps.length > 3 && (
|
|
304
|
+
<Text size="xs" c="dimmed">
|
|
305
|
+
+{circularDeps.length - 3} more
|
|
306
|
+
</Text>
|
|
307
|
+
)}
|
|
308
|
+
</Flex>
|
|
309
|
+
</Alert>
|
|
310
|
+
)}
|
|
311
|
+
|
|
312
|
+
<Box
|
|
313
|
+
style={{
|
|
314
|
+
flex: 1,
|
|
315
|
+
borderRadius: 8,
|
|
316
|
+
border: `1px solid ${ui.colors.border}`,
|
|
317
|
+
overflow: "hidden",
|
|
318
|
+
position: "relative",
|
|
319
|
+
}}
|
|
320
|
+
>
|
|
321
|
+
<ReactFlowProvider>
|
|
322
|
+
<ReactFlow
|
|
323
|
+
nodes={nodes}
|
|
324
|
+
edges={edges}
|
|
325
|
+
onNodesChange={onNodesChange}
|
|
326
|
+
onEdgesChange={onEdgesChange}
|
|
327
|
+
onNodeClick={handleNodeClick as any}
|
|
328
|
+
onPaneClick={handlePaneClick}
|
|
329
|
+
nodeTypes={nodeTypes as any}
|
|
330
|
+
fitView
|
|
331
|
+
fitViewOptions={{ padding: 0.2 }}
|
|
332
|
+
minZoom={0.1}
|
|
333
|
+
maxZoom={2}
|
|
334
|
+
proOptions={{ hideAttribution: true }}
|
|
335
|
+
>
|
|
336
|
+
<Background variant={BackgroundVariant.Dots} gap={20} size={1} />
|
|
337
|
+
<MiniMap
|
|
338
|
+
nodeColor={(node) =>
|
|
339
|
+
getModuleColor((node.data as ProviderNodeData)?.module)
|
|
340
|
+
}
|
|
341
|
+
maskColor="rgba(0, 0, 0, 0.5)"
|
|
342
|
+
style={{ backgroundColor: ui.colors.surface }}
|
|
343
|
+
/>
|
|
344
|
+
<FlowControls />
|
|
345
|
+
</ReactFlow>
|
|
346
|
+
</ReactFlowProvider>
|
|
347
|
+
|
|
348
|
+
<NodeDetails
|
|
349
|
+
node={selectedNode}
|
|
350
|
+
onClose={() => handlePaneClick()}
|
|
351
|
+
onNodeClick={handleDetailsNodeClick}
|
|
352
|
+
/>
|
|
353
|
+
</Box>
|
|
354
|
+
</Flex>
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export default DevDependencyGraph;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ActionIcon,
|
|
3
|
+
Badge,
|
|
4
|
+
Checkbox,
|
|
5
|
+
Flex,
|
|
6
|
+
Group,
|
|
7
|
+
SegmentedControl,
|
|
8
|
+
Select,
|
|
9
|
+
TextInput,
|
|
10
|
+
Tooltip,
|
|
11
|
+
} from "@mantine/core";
|
|
12
|
+
import {
|
|
13
|
+
IconBox,
|
|
14
|
+
IconBoxMultiple,
|
|
15
|
+
IconDownload,
|
|
16
|
+
IconLayoutDistributeHorizontal,
|
|
17
|
+
IconLayoutDistributeVertical,
|
|
18
|
+
IconSearch,
|
|
19
|
+
IconTopologyRing,
|
|
20
|
+
} from "@tabler/icons-react";
|
|
21
|
+
import type { GraphFilters, LayoutType, ViewMode } from "./types.ts";
|
|
22
|
+
|
|
23
|
+
interface GraphControlsProps {
|
|
24
|
+
filters: GraphFilters;
|
|
25
|
+
onFiltersChange: (filters: GraphFilters) => void;
|
|
26
|
+
layout: LayoutType;
|
|
27
|
+
onLayoutChange: (layout: LayoutType) => void;
|
|
28
|
+
modules: string[];
|
|
29
|
+
nodeCount: number;
|
|
30
|
+
edgeCount: number;
|
|
31
|
+
onExport: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const GraphControls = ({
|
|
35
|
+
filters,
|
|
36
|
+
onFiltersChange,
|
|
37
|
+
layout,
|
|
38
|
+
onLayoutChange,
|
|
39
|
+
modules,
|
|
40
|
+
nodeCount,
|
|
41
|
+
edgeCount,
|
|
42
|
+
onExport,
|
|
43
|
+
}: GraphControlsProps) => {
|
|
44
|
+
const isModuleView = filters.viewMode === "modules";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Flex gap="sm" wrap="wrap" align="center">
|
|
48
|
+
<SegmentedControl
|
|
49
|
+
size="xs"
|
|
50
|
+
value={filters.viewMode}
|
|
51
|
+
onChange={(value) =>
|
|
52
|
+
onFiltersChange({ ...filters, viewMode: value as ViewMode })
|
|
53
|
+
}
|
|
54
|
+
data={[
|
|
55
|
+
{
|
|
56
|
+
label: (
|
|
57
|
+
<Tooltip label="Modules">
|
|
58
|
+
<IconBoxMultiple size={14} />
|
|
59
|
+
</Tooltip>
|
|
60
|
+
),
|
|
61
|
+
value: "modules",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
label: (
|
|
65
|
+
<Tooltip label="Services">
|
|
66
|
+
<IconBox size={14} />
|
|
67
|
+
</Tooltip>
|
|
68
|
+
),
|
|
69
|
+
value: "providers",
|
|
70
|
+
},
|
|
71
|
+
]}
|
|
72
|
+
/>
|
|
73
|
+
|
|
74
|
+
<TextInput
|
|
75
|
+
placeholder={isModuleView ? "Search modules..." : "Search services..."}
|
|
76
|
+
leftSection={<IconSearch size={14} />}
|
|
77
|
+
value={filters.search}
|
|
78
|
+
onChange={(e) =>
|
|
79
|
+
onFiltersChange({ ...filters, search: e.currentTarget.value })
|
|
80
|
+
}
|
|
81
|
+
size="xs"
|
|
82
|
+
style={{ width: 200 }}
|
|
83
|
+
/>
|
|
84
|
+
|
|
85
|
+
{!isModuleView && (
|
|
86
|
+
<Select
|
|
87
|
+
placeholder="Filter by module"
|
|
88
|
+
value={filters.module}
|
|
89
|
+
onChange={(value) =>
|
|
90
|
+
onFiltersChange({ ...filters, module: value || "all" })
|
|
91
|
+
}
|
|
92
|
+
data={[
|
|
93
|
+
{ label: "All modules", value: "all" },
|
|
94
|
+
...modules.map((m) => ({ label: m, value: m })),
|
|
95
|
+
]}
|
|
96
|
+
size="xs"
|
|
97
|
+
style={{ width: 200 }}
|
|
98
|
+
clearable
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
<Checkbox
|
|
103
|
+
label="Hide framework"
|
|
104
|
+
checked={filters.hideFramework}
|
|
105
|
+
onChange={(e) =>
|
|
106
|
+
onFiltersChange({
|
|
107
|
+
...filters,
|
|
108
|
+
hideFramework: e.currentTarget.checked,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
size="xs"
|
|
112
|
+
/>
|
|
113
|
+
|
|
114
|
+
<SegmentedControl
|
|
115
|
+
size="xs"
|
|
116
|
+
value={layout}
|
|
117
|
+
onChange={(value) => onLayoutChange(value as LayoutType)}
|
|
118
|
+
data={[
|
|
119
|
+
{
|
|
120
|
+
label: (
|
|
121
|
+
<Tooltip label="Hierarchical">
|
|
122
|
+
<IconLayoutDistributeVertical size={14} />
|
|
123
|
+
</Tooltip>
|
|
124
|
+
),
|
|
125
|
+
value: "dagre",
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
label: (
|
|
129
|
+
<Tooltip label="Circular">
|
|
130
|
+
<IconTopologyRing size={14} />
|
|
131
|
+
</Tooltip>
|
|
132
|
+
),
|
|
133
|
+
value: "circular",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: (
|
|
137
|
+
<Tooltip label="Force">
|
|
138
|
+
<IconLayoutDistributeHorizontal size={14} />
|
|
139
|
+
</Tooltip>
|
|
140
|
+
),
|
|
141
|
+
value: "force",
|
|
142
|
+
},
|
|
143
|
+
]}
|
|
144
|
+
/>
|
|
145
|
+
|
|
146
|
+
<Group gap={4}>
|
|
147
|
+
<Badge size="xs" variant="light" color="gray">
|
|
148
|
+
{nodeCount} {isModuleView ? "modules" : "services"}
|
|
149
|
+
</Badge>
|
|
150
|
+
<Badge size="xs" variant="light" color="gray">
|
|
151
|
+
{edgeCount} edges
|
|
152
|
+
</Badge>
|
|
153
|
+
</Group>
|
|
154
|
+
|
|
155
|
+
<Tooltip label="Export as PNG">
|
|
156
|
+
<ActionIcon size="sm" variant="subtle" onClick={onExport}>
|
|
157
|
+
<IconDownload size={14} />
|
|
158
|
+
</ActionIcon>
|
|
159
|
+
</Tooltip>
|
|
160
|
+
</Flex>
|
|
161
|
+
);
|
|
162
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ui } from "@alepha/ui";
|
|
2
|
+
import {
|
|
3
|
+
ActionIcon,
|
|
4
|
+
Badge,
|
|
5
|
+
Box,
|
|
6
|
+
Divider,
|
|
7
|
+
Flex,
|
|
8
|
+
Paper,
|
|
9
|
+
ScrollArea,
|
|
10
|
+
Stack,
|
|
11
|
+
Text,
|
|
12
|
+
} from "@mantine/core";
|
|
13
|
+
import {
|
|
14
|
+
IconArrowDown,
|
|
15
|
+
IconArrowUp,
|
|
16
|
+
IconBox,
|
|
17
|
+
IconX,
|
|
18
|
+
} from "@tabler/icons-react";
|
|
19
|
+
import { getModuleColor } from "./constants.ts";
|
|
20
|
+
import type { ProviderNodeData } from "./types.ts";
|
|
21
|
+
|
|
22
|
+
interface NodeDetailsProps {
|
|
23
|
+
node: { id: string; data: ProviderNodeData } | null;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
onNodeClick: (nodeId: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const NodeDetails = ({
|
|
29
|
+
node,
|
|
30
|
+
onClose,
|
|
31
|
+
onNodeClick,
|
|
32
|
+
}: NodeDetailsProps) => {
|
|
33
|
+
if (!node) return null;
|
|
34
|
+
|
|
35
|
+
const { data } = node;
|
|
36
|
+
const moduleColor = getModuleColor(data.module);
|
|
37
|
+
const isModule = data.isModule;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Paper
|
|
41
|
+
p="md"
|
|
42
|
+
style={{
|
|
43
|
+
position: "absolute",
|
|
44
|
+
top: 16,
|
|
45
|
+
right: 16,
|
|
46
|
+
width: 280,
|
|
47
|
+
backgroundColor: ui.colors.elevated,
|
|
48
|
+
border: `1px solid ${ui.colors.border}`,
|
|
49
|
+
borderRadius: 8,
|
|
50
|
+
zIndex: 10,
|
|
51
|
+
}}
|
|
52
|
+
>
|
|
53
|
+
<Flex justify="space-between" align="start" mb="sm">
|
|
54
|
+
<Box style={{ flex: 1 }}>
|
|
55
|
+
<Text size="sm" fw={600} style={{ wordBreak: "break-word" }}>
|
|
56
|
+
{data.label}
|
|
57
|
+
</Text>
|
|
58
|
+
{!isModule && data.module && (
|
|
59
|
+
<Badge
|
|
60
|
+
size="xs"
|
|
61
|
+
variant="light"
|
|
62
|
+
mt={4}
|
|
63
|
+
style={{
|
|
64
|
+
backgroundColor: `${moduleColor}20`,
|
|
65
|
+
color: moduleColor,
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{data.module}
|
|
69
|
+
</Badge>
|
|
70
|
+
)}
|
|
71
|
+
{isModule && data.providerCount !== undefined && (
|
|
72
|
+
<Text size="xs" c="dimmed" mt={4}>
|
|
73
|
+
{data.providerCount} services
|
|
74
|
+
</Text>
|
|
75
|
+
)}
|
|
76
|
+
</Box>
|
|
77
|
+
<ActionIcon size="sm" variant="subtle" onClick={onClose}>
|
|
78
|
+
<IconX size={14} />
|
|
79
|
+
</ActionIcon>
|
|
80
|
+
</Flex>
|
|
81
|
+
|
|
82
|
+
{!isModule && data.aliases && data.aliases.length > 0 && (
|
|
83
|
+
<>
|
|
84
|
+
<Divider my="xs" />
|
|
85
|
+
<Text size="xs" c="dimmed" mb={4}>
|
|
86
|
+
Aliases
|
|
87
|
+
</Text>
|
|
88
|
+
<Flex gap={4} wrap="wrap">
|
|
89
|
+
{data.aliases.map((alias) => (
|
|
90
|
+
<Badge key={alias} size="xs" variant="outline">
|
|
91
|
+
{alias}
|
|
92
|
+
</Badge>
|
|
93
|
+
))}
|
|
94
|
+
</Flex>
|
|
95
|
+
</>
|
|
96
|
+
)}
|
|
97
|
+
|
|
98
|
+
{isModule && data.providers && data.providers.length > 0 && (
|
|
99
|
+
<>
|
|
100
|
+
<Divider my="xs" />
|
|
101
|
+
<Flex align="center" gap={4} mb={4}>
|
|
102
|
+
<IconBox size={12} opacity={0.5} />
|
|
103
|
+
<Text size="xs" c="dimmed">
|
|
104
|
+
Services ({data.providers.length})
|
|
105
|
+
</Text>
|
|
106
|
+
</Flex>
|
|
107
|
+
<ScrollArea h={100}>
|
|
108
|
+
<Stack gap={2}>
|
|
109
|
+
{data.providers.map((provider) => (
|
|
110
|
+
<Text key={provider} size="xs">
|
|
111
|
+
{provider.split(".").pop()}
|
|
112
|
+
</Text>
|
|
113
|
+
))}
|
|
114
|
+
</Stack>
|
|
115
|
+
</ScrollArea>
|
|
116
|
+
</>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<Divider my="xs" />
|
|
120
|
+
|
|
121
|
+
<ScrollArea h={isModule ? 120 : 200}>
|
|
122
|
+
<Stack gap="xs">
|
|
123
|
+
{data.dependencies.length > 0 && (
|
|
124
|
+
<Box>
|
|
125
|
+
<Flex align="center" gap={4} mb={4}>
|
|
126
|
+
<IconArrowDown size={12} opacity={0.5} />
|
|
127
|
+
<Text size="xs" c="dimmed">
|
|
128
|
+
{isModule ? "Depends on" : "Dependencies"} (
|
|
129
|
+
{data.dependencies.length})
|
|
130
|
+
</Text>
|
|
131
|
+
</Flex>
|
|
132
|
+
<Stack gap={2}>
|
|
133
|
+
{data.dependencies.map((dep) => (
|
|
134
|
+
<Text
|
|
135
|
+
key={dep}
|
|
136
|
+
size="xs"
|
|
137
|
+
style={{ cursor: "pointer" }}
|
|
138
|
+
c="blue"
|
|
139
|
+
onClick={() => onNodeClick(dep)}
|
|
140
|
+
>
|
|
141
|
+
{dep}
|
|
142
|
+
</Text>
|
|
143
|
+
))}
|
|
144
|
+
</Stack>
|
|
145
|
+
</Box>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
{data.dependents.length > 0 && (
|
|
149
|
+
<Box>
|
|
150
|
+
<Flex align="center" gap={4} mb={4}>
|
|
151
|
+
<IconArrowUp size={12} opacity={0.5} />
|
|
152
|
+
<Text size="xs" c="dimmed">
|
|
153
|
+
Used by ({data.dependents.length})
|
|
154
|
+
</Text>
|
|
155
|
+
</Flex>
|
|
156
|
+
<Stack gap={2}>
|
|
157
|
+
{data.dependents.map((dep) => (
|
|
158
|
+
<Text
|
|
159
|
+
key={dep}
|
|
160
|
+
size="xs"
|
|
161
|
+
style={{ cursor: "pointer" }}
|
|
162
|
+
c="blue"
|
|
163
|
+
onClick={() => onNodeClick(dep)}
|
|
164
|
+
>
|
|
165
|
+
{dep}
|
|
166
|
+
</Text>
|
|
167
|
+
))}
|
|
168
|
+
</Stack>
|
|
169
|
+
</Box>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{data.dependencies.length === 0 && data.dependents.length === 0 && (
|
|
173
|
+
<Text size="xs" c="dimmed">
|
|
174
|
+
No dependencies
|
|
175
|
+
</Text>
|
|
176
|
+
)}
|
|
177
|
+
</Stack>
|
|
178
|
+
</ScrollArea>
|
|
179
|
+
</Paper>
|
|
180
|
+
);
|
|
181
|
+
};
|