diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index 5dffd2df..e40243eb 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -129,11 +129,11 @@ function CustomNodeComponent({ const name = data.label ?? "unnamed"; const truncatedName = - name.length > 24 ? name.slice(0, 22) + "..." : name; + name.length > 60 ? name.slice(0, 58) + "..." : name; return (
data.onNodeClick?.(id)} > {/* Left color bar */} @@ -168,11 +168,11 @@ function CustomNodeComponent({
-
+
{truncatedName}
-
+
{data.summary}
diff --git a/understand-anything-plugin/packages/dashboard/src/utils/__tests__/containers.test.ts b/understand-anything-plugin/packages/dashboard/src/utils/__tests__/containers.test.ts index b5d6d407..92d9b061 100644 --- a/understand-anything-plugin/packages/dashboard/src/utils/__tests__/containers.test.ts +++ b/understand-anything-plugin/packages/dashboard/src/utils/__tests__/containers.test.ts @@ -107,10 +107,36 @@ describe("deriveContainers — community fallback", () => { } } const { containers } = deriveContainers(nodes, edges); + // A single folder covering the whole set is now kept as ONE folder + // container (named after the folder) instead of being split into + // anonymous Louvain communities. + expect(containers.length).toBe(1); + expect(containers[0].strategy).toBe("folder"); + expect(containers[0].name).toBe("services"); + }); + + it("names community clusters by member files when no folder signal exists", () => { + // Flat paths (no directories) force the community fallback. + const nodes = Array.from({ length: 10 }, (_, i) => + node(`n${i}`, `n${i}.go`), + ); + const edges: GraphEdge[] = []; + for (const i of [0, 1, 2, 3, 4]) { + for (const j of [0, 1, 2, 3, 4]) { + if (i !== j) edges.push({ source: `n${i}`, target: `n${j}`, type: "calls" } as GraphEdge); + } + } + for (const i of [5, 6, 7, 8, 9]) { + for (const j of [5, 6, 7, 8, 9]) { + if (i !== j) edges.push({ source: `n${i}`, target: `n${j}`, type: "calls" } as GraphEdge); + } + } + const { containers } = deriveContainers(nodes, edges); expect(containers.length).toBeGreaterThanOrEqual(2); for (const c of containers) { expect(c.strategy).toBe("community"); - expect(c.name).toMatch(/^Cluster [A-Z]$/); + // Member-derived label, e.g. "n0 · n1 · n2 +2" — not "Cluster A". + expect(c.name).toMatch(/·|\+/); } }); diff --git a/understand-anything-plugin/packages/dashboard/src/utils/containers.ts b/understand-anything-plugin/packages/dashboard/src/utils/containers.ts index dd885219..2afe8af5 100644 --- a/understand-anything-plugin/packages/dashboard/src/utils/containers.ts +++ b/understand-anything-plugin/packages/dashboard/src/utils/containers.ts @@ -79,6 +79,10 @@ function shouldFallbackToCommunity( rooted: string[], totalNodes: number, ): boolean { + // A single folder covering the whole set is a meaningful unit (e.g. a + // Redux slice folder like src/store/meetingTypes) — keep it as ONE named + // container instead of splitting into anonymous Louvain communities. + if (groups.size === 1 && rooted.length === 0) return false; const bucketCount = groups.size + (rooted.length > 0 ? 1 : 0); if (bucketCount < MIN_BUCKET_COUNT) return true; for (const ids of groups.values()) { @@ -113,11 +117,33 @@ export function deriveContainers( byCommunity.set(cid, arr); } const sorted = [...byCommunity.entries()].sort((a, b) => a[0] - b[0]); + // Name community clusters by their member files instead of "Cluster A/B/C". + const nodeById = new Map(nodes.map((n) => [n.id, n])); + const FILE_LEVEL = new Set([ + "file", "config", "document", "service", "pipeline", + "table", "schema", "resource", "endpoint", + ]); + const labelFor = (ids: string[]): string => { + let members = ids + .map((id) => nodeById.get(id)) + .filter((n): n is GraphNode => Boolean(n)); + const fileMembers = members.filter((m) => FILE_LEVEL.has(m.type)); + if (fileMembers.length > 0) members = fileMembers; + const bases = [ + ...new Set( + members.map((m) => + (m.name ?? m.id).replace(/\.(tsx?|jsx?|java|py|go|rb|cs|kt)$/i, ""), + ), + ), + ]; + const head = bases.slice(0, 3).join(" · "); + return bases.length > 3 ? `${head} +${bases.length - 3}` : head; + }; containers = sorted.map(([cid, ids], i) => ({ id: `container:cluster-${cid}`, - // A-Z for the first 26, then numeric. Avoids `String.fromCharCode(65+i)` - // wrapping into `[`, `\`, `]` ... once the cluster count exceeds 26. - name: i < 26 ? `Cluster ${String.fromCharCode(65 + i)}` : `Cluster ${i + 1}`, + name: + labelFor(ids) || + (i < 26 ? `Cluster ${String.fromCharCode(65 + i)}` : `Cluster ${i + 1}`), nodeIds: ids, strategy: "community" as const, })); diff --git a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts index b35328c5..4d2c89e8 100644 --- a/understand-anything-plugin/packages/dashboard/src/utils/layout.ts +++ b/understand-anything-plugin/packages/dashboard/src/utils/layout.ts @@ -12,8 +12,8 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3-force"; import type { Node, Edge } from "@xyflow/react"; import type { ElkInput } from "./elk-layout"; -export const NODE_WIDTH = 280; -export const NODE_HEIGHT = 120; +export const NODE_WIDTH = 330; +export const NODE_HEIGHT = 150; export const LAYER_CLUSTER_WIDTH = 320; export const LAYER_CLUSTER_HEIGHT = 180; export const PORTAL_NODE_WIDTH = 240;