@alepha/devtools 0.13.6 → 0.13.8
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/assets/devtools/asset.BZV40eAE.css +1 -0
- package/assets/devtools/asset.Bhpm0ujk.css +1 -0
- package/assets/devtools/chunk.17gtbQUO.js +1 -0
- package/assets/devtools/chunk.1mem8WHh.js +1 -0
- package/assets/devtools/chunk.B0aYes_4.js +1 -0
- package/assets/devtools/chunk.BULoWCgJ.js +7 -0
- package/assets/devtools/chunk.BYrPfJRg.js +1 -0
- package/assets/devtools/chunk.BjFrJKj1.js +1 -0
- package/assets/devtools/chunk.BlqFPyLh.js +1 -0
- package/assets/devtools/chunk.BmJ7-uBd.js +1 -0
- package/assets/devtools/chunk.BzE7YYkj.js +6 -0
- package/assets/devtools/chunk.C49FcqzR.js +1 -0
- package/assets/devtools/chunk.CBGZOsrp.js +9 -0
- package/assets/devtools/chunk.CHRbA_gU.js +1 -0
- package/assets/devtools/chunk.Ct58VlQl.js +1 -0
- package/assets/devtools/chunk.Cw2RCl4F.js +1 -0
- package/assets/devtools/chunk.CyQeq1kA.js +1 -0
- package/assets/devtools/chunk.CzbujtK7.js +1 -0
- package/assets/devtools/chunk.DA9XnVAa.js +1 -0
- package/assets/devtools/chunk.DEUHUxKv.js +1 -0
- package/assets/devtools/chunk.DGW-W4Kc.js +1 -0
- package/assets/devtools/chunk.DHWcJNNS.js +1 -0
- package/assets/devtools/chunk.DIfRZc20.js +1 -0
- package/assets/devtools/chunk.DJJIo7HU.js +1 -0
- package/assets/devtools/chunk.DJOi4_So.js +1 -0
- package/assets/devtools/chunk.DR0SHXXd.js +1 -0
- package/assets/devtools/chunk.DinJSUfH.js +1 -0
- package/assets/devtools/chunk.DooL4OcT.js +1 -0
- package/assets/devtools/chunk.Dry2LXOT.js +1 -0
- package/assets/devtools/chunk.IVvrfXp1.js +1 -0
- package/assets/devtools/chunk.OlMI8g2F.js +1 -0
- package/assets/devtools/chunk.Pj_uCbSv.js +1 -0
- package/assets/devtools/chunk.RTodzvo0.js +2 -0
- package/assets/devtools/chunk.YXYL4YAO.js +2 -0
- package/assets/devtools/chunk.fpKvkQeU.js +1 -0
- package/assets/devtools/chunk.qDx9cjbN.js +1 -0
- package/assets/devtools/chunk.rJToME5k.js +1 -0
- package/assets/devtools/chunk.rohGhT-A.js +1 -0
- package/assets/devtools/chunk.uyVen0u2.js +1 -0
- package/assets/devtools/entry.BY4L2Uc6.js +75 -0
- package/assets/devtools/index.html +11 -0
- package/dist/index.d.ts +249 -32
- package/dist/index.js +253 -22
- package/dist/index.js.map +1 -1
- package/package.json +18 -12
- 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,97 @@
|
|
|
1
|
+
import { ui } from "@alepha/ui";
|
|
2
|
+
import { Badge, Box, Flex, Text } from "@mantine/core";
|
|
3
|
+
import { Handle, Position } from "@xyflow/react";
|
|
4
|
+
import { getModuleColor } from "./constants.ts";
|
|
5
|
+
import type { ProviderNodeData } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
interface ProviderNodeProps {
|
|
8
|
+
data: ProviderNodeData;
|
|
9
|
+
selected?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ProviderNode = ({ data, selected }: ProviderNodeProps) => {
|
|
13
|
+
const moduleColor = getModuleColor(data.module);
|
|
14
|
+
const isFaded = data.isFaded && !data.isHighlighted;
|
|
15
|
+
const isModule = data.isModule;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Box
|
|
19
|
+
p="xs"
|
|
20
|
+
style={{
|
|
21
|
+
borderRadius: isModule ? 12 : 8,
|
|
22
|
+
border: `2px solid ${selected || data.isSelected ? moduleColor : ui.colors.border}`,
|
|
23
|
+
backgroundColor: ui.colors.surface,
|
|
24
|
+
minWidth: isModule ? 200 : 160,
|
|
25
|
+
opacity: isFaded ? 0.3 : 1,
|
|
26
|
+
transition: "opacity 0.2s, border-color 0.2s",
|
|
27
|
+
boxShadow: data.isHighlighted ? `0 0 10px ${moduleColor}` : undefined,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<Handle
|
|
31
|
+
type="target"
|
|
32
|
+
position={Position.Top}
|
|
33
|
+
style={{
|
|
34
|
+
background: moduleColor,
|
|
35
|
+
width: 8,
|
|
36
|
+
height: 8,
|
|
37
|
+
border: "none",
|
|
38
|
+
}}
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<Flex direction="column" gap={4}>
|
|
42
|
+
{isModule ? (
|
|
43
|
+
<>
|
|
44
|
+
<Text size="xs" fw={600} style={{ wordBreak: "break-word" }}>
|
|
45
|
+
{data.label}
|
|
46
|
+
</Text>
|
|
47
|
+
<Text size="xs" c="dimmed">
|
|
48
|
+
{data.providerCount} services
|
|
49
|
+
</Text>
|
|
50
|
+
</>
|
|
51
|
+
) : (
|
|
52
|
+
<>
|
|
53
|
+
<Text size="xs" fw={600} style={{ wordBreak: "break-word" }}>
|
|
54
|
+
{data.label.split(".").pop()}
|
|
55
|
+
</Text>
|
|
56
|
+
{data.module && (
|
|
57
|
+
<Badge
|
|
58
|
+
size="xs"
|
|
59
|
+
variant="light"
|
|
60
|
+
style={{
|
|
61
|
+
backgroundColor: `${moduleColor}20`,
|
|
62
|
+
color: moduleColor,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{data.module}
|
|
66
|
+
</Badge>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
<Flex gap={4}>
|
|
72
|
+
{data.dependencies.length > 0 && (
|
|
73
|
+
<Text size="xs" c="dimmed">
|
|
74
|
+
{data.dependencies.length} deps
|
|
75
|
+
</Text>
|
|
76
|
+
)}
|
|
77
|
+
{data.dependents.length > 0 && (
|
|
78
|
+
<Text size="xs" c="dimmed">
|
|
79
|
+
{data.dependents.length} refs
|
|
80
|
+
</Text>
|
|
81
|
+
)}
|
|
82
|
+
</Flex>
|
|
83
|
+
</Flex>
|
|
84
|
+
|
|
85
|
+
<Handle
|
|
86
|
+
type="source"
|
|
87
|
+
position={Position.Bottom}
|
|
88
|
+
style={{
|
|
89
|
+
background: moduleColor,
|
|
90
|
+
width: 8,
|
|
91
|
+
height: 8,
|
|
92
|
+
border: "none",
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
</Box>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const MODULE_COLORS: Record<string, string> = {
|
|
2
|
+
"alepha.core": "#868e96",
|
|
3
|
+
"alepha.server": "#228be6",
|
|
4
|
+
"alepha.security": "#fa5252",
|
|
5
|
+
"alepha.orm": "#40c057",
|
|
6
|
+
"alepha.cache": "#fab005",
|
|
7
|
+
"alepha.queue": "#fd7e14",
|
|
8
|
+
"alepha.topic": "#be4bdb",
|
|
9
|
+
"alepha.bucket": "#15aabf",
|
|
10
|
+
"alepha.scheduler": "#e64980",
|
|
11
|
+
"alepha.logger": "#74c0fc",
|
|
12
|
+
"alepha.devtools": "#845ef7",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_MODULE_COLOR = "#495057";
|
|
16
|
+
|
|
17
|
+
export const getModuleColor = (module?: string): string => {
|
|
18
|
+
if (!module) return DEFAULT_MODULE_COLOR;
|
|
19
|
+
|
|
20
|
+
// Check for exact match first
|
|
21
|
+
if (MODULE_COLORS[module]) return MODULE_COLORS[module];
|
|
22
|
+
|
|
23
|
+
// Check for prefix match (e.g., "alepha.core.something" -> "alepha.core")
|
|
24
|
+
for (const [key, color] of Object.entries(MODULE_COLORS)) {
|
|
25
|
+
if (module.startsWith(key)) return color;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Generate consistent color from module name
|
|
29
|
+
let hash = 0;
|
|
30
|
+
for (let i = 0; i < module.length; i++) {
|
|
31
|
+
hash = module.charCodeAt(i) + ((hash << 5) - hash);
|
|
32
|
+
}
|
|
33
|
+
const hue = Math.abs(hash % 360);
|
|
34
|
+
return `hsl(${hue}, 60%, 50%)`;
|
|
35
|
+
};
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import type { DevProviderMetadata } from "../../../api/schemas/DevProviderMetadata.ts";
|
|
2
|
+
import type {
|
|
3
|
+
GraphFilters,
|
|
4
|
+
LayoutType,
|
|
5
|
+
ProviderEdge,
|
|
6
|
+
ProviderNode,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export const buildProviderGraph = (
|
|
10
|
+
providers: DevProviderMetadata[],
|
|
11
|
+
filters: GraphFilters,
|
|
12
|
+
): { nodes: ProviderNode[]; edges: ProviderEdge[] } => {
|
|
13
|
+
// Build dependents map (reverse of dependencies)
|
|
14
|
+
const dependentsMap = new Map<string, string[]>();
|
|
15
|
+
for (const provider of providers) {
|
|
16
|
+
for (const dep of provider.dependencies) {
|
|
17
|
+
const dependents = dependentsMap.get(dep) || [];
|
|
18
|
+
dependents.push(provider.name);
|
|
19
|
+
dependentsMap.set(dep, dependents);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Filter providers
|
|
24
|
+
const filtered = providers.filter((p) => {
|
|
25
|
+
if (filters.hideFramework && p.module?.startsWith("alepha.")) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (
|
|
29
|
+
filters.module &&
|
|
30
|
+
filters.module !== "all" &&
|
|
31
|
+
p.module !== filters.module
|
|
32
|
+
) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (filters.search) {
|
|
36
|
+
const searchLower = filters.search.toLowerCase();
|
|
37
|
+
return (
|
|
38
|
+
p.name.toLowerCase().includes(searchLower) ||
|
|
39
|
+
p.module?.toLowerCase().includes(searchLower)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const filteredNames = new Set(filtered.map((p) => p.name));
|
|
46
|
+
|
|
47
|
+
// Create nodes
|
|
48
|
+
const nodes: ProviderNode[] = filtered.map((provider) => ({
|
|
49
|
+
id: provider.name,
|
|
50
|
+
type: "provider",
|
|
51
|
+
position: { x: 0, y: 0 },
|
|
52
|
+
data: {
|
|
53
|
+
label: provider.name,
|
|
54
|
+
module: provider.module,
|
|
55
|
+
dependencies: provider.dependencies,
|
|
56
|
+
dependents: dependentsMap.get(provider.name) || [],
|
|
57
|
+
aliases: provider.aliases,
|
|
58
|
+
isModule: false,
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
// Create edges (only between visible nodes)
|
|
63
|
+
const edges: ProviderEdge[] = [];
|
|
64
|
+
for (const provider of filtered) {
|
|
65
|
+
for (const dep of provider.dependencies) {
|
|
66
|
+
if (filteredNames.has(dep)) {
|
|
67
|
+
edges.push({
|
|
68
|
+
id: `${provider.name}->${dep}`,
|
|
69
|
+
source: provider.name,
|
|
70
|
+
target: dep,
|
|
71
|
+
animated: false,
|
|
72
|
+
style: { stroke: "#495057", strokeWidth: 1.5 },
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { nodes, edges };
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const buildModuleGraph = (
|
|
82
|
+
providers: DevProviderMetadata[],
|
|
83
|
+
filters: GraphFilters,
|
|
84
|
+
): { nodes: ProviderNode[]; edges: ProviderEdge[] } => {
|
|
85
|
+
// Group providers by module
|
|
86
|
+
const moduleMap = new Map<string, DevProviderMetadata[]>();
|
|
87
|
+
for (const provider of providers) {
|
|
88
|
+
const module = provider.module || "Other";
|
|
89
|
+
if (filters.hideFramework && module.startsWith("alepha.")) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (filters.search) {
|
|
93
|
+
const searchLower = filters.search.toLowerCase();
|
|
94
|
+
if (!module.toLowerCase().includes(searchLower)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const list = moduleMap.get(module) || [];
|
|
99
|
+
list.push(provider);
|
|
100
|
+
moduleMap.set(module, list);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Build module dependencies and dependents
|
|
104
|
+
const moduleDeps = new Map<string, Set<string>>();
|
|
105
|
+
const moduleDependents = new Map<string, Set<string>>();
|
|
106
|
+
|
|
107
|
+
for (const [module, moduleProviders] of moduleMap) {
|
|
108
|
+
moduleDeps.set(module, new Set());
|
|
109
|
+
moduleDependents.set(module, new Set());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [module, moduleProviders] of moduleMap) {
|
|
113
|
+
for (const provider of moduleProviders) {
|
|
114
|
+
for (const dep of provider.dependencies) {
|
|
115
|
+
// Find which module this dependency belongs to
|
|
116
|
+
const depProvider = providers.find((p) => p.name === dep);
|
|
117
|
+
if (depProvider) {
|
|
118
|
+
const depModule = depProvider.module || "Other";
|
|
119
|
+
if (depModule !== module && moduleMap.has(depModule)) {
|
|
120
|
+
moduleDeps.get(module)?.add(depModule);
|
|
121
|
+
moduleDependents.get(depModule)?.add(module);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Filter by selected module
|
|
129
|
+
let filteredModules = Array.from(moduleMap.keys());
|
|
130
|
+
if (filters.module && filters.module !== "all") {
|
|
131
|
+
filteredModules = filteredModules.filter((m) => m === filters.module);
|
|
132
|
+
}
|
|
133
|
+
const filteredModuleSet = new Set(filteredModules);
|
|
134
|
+
|
|
135
|
+
// Create module nodes
|
|
136
|
+
const nodes: ProviderNode[] = filteredModules.map((module) => {
|
|
137
|
+
const moduleProviders = moduleMap.get(module) || [];
|
|
138
|
+
return {
|
|
139
|
+
id: module,
|
|
140
|
+
type: "provider",
|
|
141
|
+
position: { x: 0, y: 0 },
|
|
142
|
+
data: {
|
|
143
|
+
label: module,
|
|
144
|
+
module: module,
|
|
145
|
+
dependencies: Array.from(moduleDeps.get(module) || []),
|
|
146
|
+
dependents: Array.from(moduleDependents.get(module) || []),
|
|
147
|
+
providers: moduleProviders.map((p) => p.name),
|
|
148
|
+
providerCount: moduleProviders.length,
|
|
149
|
+
isModule: true,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Create edges between modules
|
|
155
|
+
const edges: ProviderEdge[] = [];
|
|
156
|
+
const edgeSet = new Set<string>();
|
|
157
|
+
|
|
158
|
+
for (const module of filteredModules) {
|
|
159
|
+
for (const dep of moduleDeps.get(module) || []) {
|
|
160
|
+
if (filteredModuleSet.has(dep)) {
|
|
161
|
+
const edgeId = `${module}->${dep}`;
|
|
162
|
+
if (!edgeSet.has(edgeId)) {
|
|
163
|
+
edgeSet.add(edgeId);
|
|
164
|
+
edges.push({
|
|
165
|
+
id: edgeId,
|
|
166
|
+
source: module,
|
|
167
|
+
target: dep,
|
|
168
|
+
animated: false,
|
|
169
|
+
style: { stroke: "#495057", strokeWidth: 1.5 },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { nodes, edges };
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const buildGraph = (
|
|
180
|
+
providers: DevProviderMetadata[],
|
|
181
|
+
filters: GraphFilters,
|
|
182
|
+
): { nodes: ProviderNode[]; edges: ProviderEdge[] } => {
|
|
183
|
+
if (filters.viewMode === "modules") {
|
|
184
|
+
return buildModuleGraph(providers, filters);
|
|
185
|
+
}
|
|
186
|
+
return buildProviderGraph(providers, filters);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const applyDagreLayout = (
|
|
190
|
+
nodes: ProviderNode[],
|
|
191
|
+
edges: ProviderEdge[],
|
|
192
|
+
direction: "TB" | "LR" = "TB",
|
|
193
|
+
): ProviderNode[] => {
|
|
194
|
+
// Simple grid layout as fallback (dagre would need additional library)
|
|
195
|
+
const nodeWidth = 180;
|
|
196
|
+
const nodeHeight = 60;
|
|
197
|
+
const horizontalSpacing = 50;
|
|
198
|
+
const verticalSpacing = 80;
|
|
199
|
+
|
|
200
|
+
// Build adjacency list and calculate levels
|
|
201
|
+
const adj = new Map<string, string[]>();
|
|
202
|
+
const inDegree = new Map<string, number>();
|
|
203
|
+
|
|
204
|
+
for (const node of nodes) {
|
|
205
|
+
adj.set(node.id, []);
|
|
206
|
+
inDegree.set(node.id, 0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const edge of edges) {
|
|
210
|
+
adj.get(edge.source)?.push(edge.target);
|
|
211
|
+
inDegree.set(edge.target, (inDegree.get(edge.target) || 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Topological sort to get levels
|
|
215
|
+
const levels = new Map<string, number>();
|
|
216
|
+
const queue: string[] = [];
|
|
217
|
+
|
|
218
|
+
for (const [id, degree] of inDegree) {
|
|
219
|
+
if (degree === 0) {
|
|
220
|
+
queue.push(id);
|
|
221
|
+
levels.set(id, 0);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
while (queue.length > 0) {
|
|
226
|
+
const current = queue.shift()!;
|
|
227
|
+
const currentLevel = levels.get(current) || 0;
|
|
228
|
+
|
|
229
|
+
for (const neighbor of adj.get(current) || []) {
|
|
230
|
+
const newDegree = (inDegree.get(neighbor) || 0) - 1;
|
|
231
|
+
inDegree.set(neighbor, newDegree);
|
|
232
|
+
|
|
233
|
+
if (newDegree === 0) {
|
|
234
|
+
queue.push(neighbor);
|
|
235
|
+
levels.set(neighbor, currentLevel + 1);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Handle cycles - assign remaining nodes to max level + 1
|
|
241
|
+
const maxLevel = Math.max(...Array.from(levels.values()), 0);
|
|
242
|
+
for (const node of nodes) {
|
|
243
|
+
if (!levels.has(node.id)) {
|
|
244
|
+
levels.set(node.id, maxLevel + 1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Group nodes by level
|
|
249
|
+
const levelGroups = new Map<number, string[]>();
|
|
250
|
+
for (const [id, level] of levels) {
|
|
251
|
+
const group = levelGroups.get(level) || [];
|
|
252
|
+
group.push(id);
|
|
253
|
+
levelGroups.set(level, group);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Position nodes
|
|
257
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
258
|
+
const positioned: ProviderNode[] = [];
|
|
259
|
+
|
|
260
|
+
for (const [level, ids] of levelGroups) {
|
|
261
|
+
const levelWidth = ids.length * (nodeWidth + horizontalSpacing);
|
|
262
|
+
const startX = -levelWidth / 2;
|
|
263
|
+
|
|
264
|
+
ids.forEach((id, index) => {
|
|
265
|
+
const node = nodeMap.get(id);
|
|
266
|
+
if (node) {
|
|
267
|
+
positioned.push({
|
|
268
|
+
...node,
|
|
269
|
+
position: {
|
|
270
|
+
x:
|
|
271
|
+
direction === "TB"
|
|
272
|
+
? startX + index * (nodeWidth + horizontalSpacing)
|
|
273
|
+
: level * (nodeWidth + horizontalSpacing),
|
|
274
|
+
y:
|
|
275
|
+
direction === "TB"
|
|
276
|
+
? level * (nodeHeight + verticalSpacing)
|
|
277
|
+
: startX + index * (nodeHeight + verticalSpacing),
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return positioned;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const applyForceLayout = (nodes: ProviderNode[]): ProviderNode[] => {
|
|
288
|
+
// Simple circular layout for force simulation
|
|
289
|
+
const radius = Math.max(200, nodes.length * 30);
|
|
290
|
+
const angleStep = (2 * Math.PI) / nodes.length;
|
|
291
|
+
|
|
292
|
+
return nodes.map((node, index) => ({
|
|
293
|
+
...node,
|
|
294
|
+
position: {
|
|
295
|
+
x: Math.cos(index * angleStep) * radius,
|
|
296
|
+
y: Math.sin(index * angleStep) * radius,
|
|
297
|
+
},
|
|
298
|
+
}));
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export const applyCircularLayout = (
|
|
302
|
+
nodes: ProviderNode[],
|
|
303
|
+
groupByModule: boolean = true,
|
|
304
|
+
): ProviderNode[] => {
|
|
305
|
+
if (!groupByModule) {
|
|
306
|
+
return applyForceLayout(nodes);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Group by module
|
|
310
|
+
const moduleGroups = new Map<string, ProviderNode[]>();
|
|
311
|
+
for (const node of nodes) {
|
|
312
|
+
const module = node.data.module || "Other";
|
|
313
|
+
const group = moduleGroups.get(module) || [];
|
|
314
|
+
group.push(node);
|
|
315
|
+
moduleGroups.set(module, group);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const modules = Array.from(moduleGroups.keys()).sort();
|
|
319
|
+
const moduleAngleStep = (2 * Math.PI) / modules.length;
|
|
320
|
+
const moduleRadius = Math.max(300, modules.length * 80);
|
|
321
|
+
|
|
322
|
+
const positioned: ProviderNode[] = [];
|
|
323
|
+
|
|
324
|
+
modules.forEach((module, moduleIndex) => {
|
|
325
|
+
const moduleNodes = moduleGroups.get(module) || [];
|
|
326
|
+
const moduleAngle = moduleIndex * moduleAngleStep;
|
|
327
|
+
const moduleCenterX = Math.cos(moduleAngle) * moduleRadius;
|
|
328
|
+
const moduleCenterY = Math.sin(moduleAngle) * moduleRadius;
|
|
329
|
+
|
|
330
|
+
const innerRadius = Math.max(100, moduleNodes.length * 25);
|
|
331
|
+
const nodeAngleStep = (2 * Math.PI) / moduleNodes.length;
|
|
332
|
+
|
|
333
|
+
moduleNodes.forEach((node, nodeIndex) => {
|
|
334
|
+
const nodeAngle = nodeIndex * nodeAngleStep;
|
|
335
|
+
positioned.push({
|
|
336
|
+
...node,
|
|
337
|
+
position: {
|
|
338
|
+
x: moduleCenterX + Math.cos(nodeAngle) * innerRadius,
|
|
339
|
+
y: moduleCenterY + Math.sin(nodeAngle) * innerRadius,
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
return positioned;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
export const applyLayout = (
|
|
349
|
+
nodes: ProviderNode[],
|
|
350
|
+
edges: ProviderEdge[],
|
|
351
|
+
layout: LayoutType,
|
|
352
|
+
): ProviderNode[] => {
|
|
353
|
+
switch (layout) {
|
|
354
|
+
case "dagre":
|
|
355
|
+
return applyDagreLayout(nodes, edges);
|
|
356
|
+
case "force":
|
|
357
|
+
return applyForceLayout(nodes);
|
|
358
|
+
case "circular":
|
|
359
|
+
return applyCircularLayout(nodes);
|
|
360
|
+
default:
|
|
361
|
+
return applyDagreLayout(nodes, edges);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
export const findDependencyChain = (
|
|
366
|
+
nodeId: string,
|
|
367
|
+
nodes: ProviderNode[],
|
|
368
|
+
edges: ProviderEdge[],
|
|
369
|
+
): Set<string> => {
|
|
370
|
+
const chain = new Set<string>([nodeId]);
|
|
371
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
372
|
+
|
|
373
|
+
// Find all dependencies (downstream)
|
|
374
|
+
const findDeps = (id: string) => {
|
|
375
|
+
const node = nodeMap.get(id);
|
|
376
|
+
if (!node) return;
|
|
377
|
+
for (const dep of node.data.dependencies) {
|
|
378
|
+
if (!chain.has(dep)) {
|
|
379
|
+
chain.add(dep);
|
|
380
|
+
findDeps(dep);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
// Find all dependents (upstream)
|
|
386
|
+
const findDependents = (id: string) => {
|
|
387
|
+
const node = nodeMap.get(id);
|
|
388
|
+
if (!node) return;
|
|
389
|
+
for (const dep of node.data.dependents) {
|
|
390
|
+
if (!chain.has(dep)) {
|
|
391
|
+
chain.add(dep);
|
|
392
|
+
findDependents(dep);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
findDeps(nodeId);
|
|
398
|
+
findDependents(nodeId);
|
|
399
|
+
|
|
400
|
+
return chain;
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
export const detectCircularDependencies = (
|
|
404
|
+
nodes: ProviderNode[],
|
|
405
|
+
edges: ProviderEdge[],
|
|
406
|
+
): string[][] => {
|
|
407
|
+
const cycles: string[][] = [];
|
|
408
|
+
const visited = new Set<string>();
|
|
409
|
+
const recStack = new Set<string>();
|
|
410
|
+
const adj = new Map<string, string[]>();
|
|
411
|
+
|
|
412
|
+
for (const node of nodes) {
|
|
413
|
+
adj.set(node.id, []);
|
|
414
|
+
}
|
|
415
|
+
for (const edge of edges) {
|
|
416
|
+
adj.get(edge.source)?.push(edge.target);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const dfs = (node: string, path: string[]): void => {
|
|
420
|
+
visited.add(node);
|
|
421
|
+
recStack.add(node);
|
|
422
|
+
path.push(node);
|
|
423
|
+
|
|
424
|
+
for (const neighbor of adj.get(node) || []) {
|
|
425
|
+
if (!visited.has(neighbor)) {
|
|
426
|
+
dfs(neighbor, [...path]);
|
|
427
|
+
} else if (recStack.has(neighbor)) {
|
|
428
|
+
const cycleStart = path.indexOf(neighbor);
|
|
429
|
+
cycles.push([...path.slice(cycleStart), neighbor]);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
recStack.delete(node);
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
for (const node of nodes) {
|
|
437
|
+
if (!visited.has(node.id)) {
|
|
438
|
+
dfs(node.id, []);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return cycles;
|
|
443
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { getModuleColor, MODULE_COLORS } from "./constants.ts";
|
|
2
|
+
export { DevDependencyGraph } from "./DevDependencyGraph.tsx";
|
|
3
|
+
export { GraphControls } from "./GraphControls.tsx";
|
|
4
|
+
export * from "./helpers.ts";
|
|
5
|
+
export { NodeDetails } from "./NodeDetails.tsx";
|
|
6
|
+
export { ProviderNode } from "./ProviderNode.tsx";
|
|
7
|
+
export * from "./types.ts";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Edge, Node } from "@xyflow/react";
|
|
2
|
+
|
|
3
|
+
export interface ProviderNodeData extends Record<string, unknown> {
|
|
4
|
+
label: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
dependencies: string[];
|
|
7
|
+
dependents: string[];
|
|
8
|
+
aliases?: string[];
|
|
9
|
+
providers?: string[]; // For module nodes, list of providers in this module
|
|
10
|
+
providerCount?: number; // For module nodes
|
|
11
|
+
isHighlighted?: boolean;
|
|
12
|
+
isSelected?: boolean;
|
|
13
|
+
isFaded?: boolean;
|
|
14
|
+
isModule?: boolean; // True if this node represents a module
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ProviderNode = Node<ProviderNodeData, "provider">;
|
|
18
|
+
export type ProviderEdge = Edge;
|
|
19
|
+
|
|
20
|
+
export type LayoutType = "dagre" | "force" | "circular";
|
|
21
|
+
export type ViewMode = "modules" | "providers";
|
|
22
|
+
|
|
23
|
+
export interface GraphFilters {
|
|
24
|
+
search: string;
|
|
25
|
+
module: string;
|
|
26
|
+
hideFramework: boolean;
|
|
27
|
+
viewMode: ViewMode;
|
|
28
|
+
}
|
package/src/ui/styles.css
CHANGED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on March 27, 2020 */
|
|
2
|
-
|
|
3
|
-
@font-face {
|
|
4
|
-
font-family: "wotfardregular";
|
|
5
|
-
src: url("wotfard-regular-webfont.eot");
|
|
6
|
-
src:
|
|
7
|
-
url("wotfard-regular-webfont.eot?#iefix") format("embedded-opentype"),
|
|
8
|
-
url("wotfard-regular-webfont.woff2") format("woff2"),
|
|
9
|
-
url("wotfard-regular-webfont.ttf") format("truetype");
|
|
10
|
-
font-weight: normal;
|
|
11
|
-
font-style: normal;
|
|
12
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|