@alepha/devtools 0.13.6 → 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.
Files changed (62) hide show
  1. package/dist/index.d.ts +249 -32
  2. package/dist/index.js +253 -22
  3. package/dist/index.js.map +1 -1
  4. package/package.json +12 -6
  5. package/src/{DevToolsProvider.ts → api/DevToolsProvider.ts} +29 -1
  6. package/src/{providers → api/providers}/DevToolsMetadataProvider.ts +210 -2
  7. package/src/api/schemas/DevAtomMetadata.ts +26 -0
  8. package/src/api/schemas/DevCommandMetadata.ts +9 -0
  9. package/src/api/schemas/DevEntityMetadata.ts +57 -0
  10. package/src/api/schemas/DevEnvMetadata.ts +22 -0
  11. package/src/{schemas → api/schemas}/DevMetadata.ts +10 -1
  12. package/src/api/schemas/DevRouteMetadata.ts +8 -0
  13. package/src/index.ts +23 -16
  14. package/src/ui/AppRouter.tsx +85 -2
  15. package/src/ui/components/DevAtomsViewer.tsx +636 -0
  16. package/src/ui/components/DevCacheInspector.tsx +423 -0
  17. package/src/ui/components/DevDashboard.tsx +188 -0
  18. package/src/ui/components/DevEnvExplorer.tsx +462 -0
  19. package/src/ui/components/DevLayout.tsx +65 -4
  20. package/src/ui/components/DevLogViewer.tsx +161 -163
  21. package/src/ui/components/DevQueueMonitor.tsx +51 -0
  22. package/src/ui/components/DevTopicsViewer.tsx +690 -0
  23. package/src/ui/components/actions/ActionGroup.tsx +37 -0
  24. package/src/ui/components/actions/ActionItem.tsx +138 -0
  25. package/src/ui/components/actions/DevActionsExplorer.tsx +132 -0
  26. package/src/ui/components/actions/MethodBadge.tsx +18 -0
  27. package/src/ui/components/actions/SchemaViewer.tsx +21 -0
  28. package/src/ui/components/actions/TryItPanel.tsx +140 -0
  29. package/src/ui/components/actions/constants.ts +7 -0
  30. package/src/ui/components/actions/helpers.ts +18 -0
  31. package/src/ui/components/actions/index.ts +8 -0
  32. package/src/ui/components/db/ColumnBadge.tsx +55 -0
  33. package/src/ui/components/db/DevDbStudio.tsx +485 -0
  34. package/src/ui/components/db/constants.ts +11 -0
  35. package/src/ui/components/db/index.ts +4 -0
  36. package/src/ui/components/db/types.ts +7 -0
  37. package/src/ui/components/graph/DevDependencyGraph.tsx +358 -0
  38. package/src/ui/components/graph/GraphControls.tsx +162 -0
  39. package/src/ui/components/graph/NodeDetails.tsx +181 -0
  40. package/src/ui/components/graph/ProviderNode.tsx +97 -0
  41. package/src/ui/components/graph/constants.ts +35 -0
  42. package/src/ui/components/graph/helpers.ts +443 -0
  43. package/src/ui/components/graph/index.ts +7 -0
  44. package/src/ui/components/graph/types.ts +28 -0
  45. package/src/ui/styles.css +0 -6
  46. package/src/ui/resources/wotfardregular/stylesheet.css +0 -12
  47. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.eot +0 -0
  48. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.ttf +0 -0
  49. package/src/ui/resources/wotfardregular/wotfard-regular-webfont.woff2 +0 -0
  50. /package/src/{entities → api/entities}/logs.ts +0 -0
  51. /package/src/{providers → api/providers}/DevToolsDatabaseProvider.ts +0 -0
  52. /package/src/{repositories → api/repositories}/LogRepository.ts +0 -0
  53. /package/src/{schemas → api/schemas}/DevActionMetadata.ts +0 -0
  54. /package/src/{schemas → api/schemas}/DevBucketMetadata.ts +0 -0
  55. /package/src/{schemas → api/schemas}/DevCacheMetadata.ts +0 -0
  56. /package/src/{schemas → api/schemas}/DevModuleMetadata.ts +0 -0
  57. /package/src/{schemas → api/schemas}/DevPageMetadata.ts +0 -0
  58. /package/src/{schemas → api/schemas}/DevProviderMetadata.ts +0 -0
  59. /package/src/{schemas → api/schemas}/DevQueueMetadata.ts +0 -0
  60. /package/src/{schemas → api/schemas}/DevRealmMetadata.ts +0 -0
  61. /package/src/{schemas → api/schemas}/DevSchedulerMetadata.ts +0 -0
  62. /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
+ };