From 6ad15022240df7a4c8b706d13c9ea1cb3704b1e5 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Sat, 25 Apr 2026 13:42:15 +0530 Subject: [PATCH] fix(explorer): address grouped view review blockers - Fix `import.meta.env.DEV` crash in graphSceneState.ts that broke the entire test:graph-workspace suite (module load fails in Node.js/tsx) - Export `resolveGroupedDisplayNodeId` from graphSceneState.ts and remove the identical copy in GraphWorkspace.tsx - Add `checkGroupedViewAvailability` helper (Louvain only, no centrality) so grouped view availability can be checked cheaply on every graph change - Gate full community graph build (`groupedDisplayCandidate`) on `viewMode === 'grouped'` to avoid running Louvain + centrality on every graph version tick when the user is not in grouped view - Remove dead ternary in `focusNode` where both branches returned `nodeId` - Add 7 new tests covering resolveGroupedDisplayNodeId, resolveGroupedDisplayStateSnapshot, and checkGroupedViewAvailability Co-Authored-By: ZohaibHassan16 <109234410+ZohaibHassan16@users.noreply.github.com> Co-Authored-By: KaifAhmad1 --- .../GraphWorkspace/GraphWorkspace.tsx | 78 ++++------- .../GraphWorkspace/graphSceneState.ts | 16 ++- .../tests/graphSceneState.display.test.ts | 124 +++++++++++++++++- 3 files changed, 160 insertions(+), 58 deletions(-) diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index 6484fee0..06475e17 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -12,7 +12,7 @@ import { useLoadGraph, useReloadGraph } from "./useLoadGraph"; import { GraphLoadingOverlay } from "./GraphLoadingOverlay"; import { createGraphLoadProgress, getGraphLoadTitle } from "./graphLoading"; import { GRAPH_THEME, withAlpha } from "./graphTheme"; -import { resolveDisplayGraph, resolveDisplayStateSnapshot, resolveGroupedDisplayStateSnapshot } from "./graphSceneState"; +import { checkGroupedViewAvailability, resolveDisplayGraph, resolveDisplayStateSnapshot, resolveGroupedDisplayNodeId, resolveGroupedDisplayStateSnapshot } from "./graphSceneState"; import { type GraphPlugin, type GraphPluginActionRequest, @@ -548,47 +548,6 @@ type FocusResolution = { reason: string | null; }; -function resolveGroupedDisplayNodeId( - displayGraph: GraphSceneRuntime["displayGraph"], - nodeId: string, -): string | null { - if (!nodeId) { - return null; - } - - if (displayGraph.hasNode(nodeId)) { - return nodeId; - } - - let resolvedNodeId: string | null = null; - displayGraph.forEachNode((candidateId, attrs) => { - if (resolvedNodeId) { - return; - } - - const communityGroup = (attrs as NodeAttributes).properties?.__communityGroup as - | { - anchorNodeId?: string | null; - memberNodeIds?: string[]; - sampleNodeIds?: string[]; - } - | undefined; - if (!communityGroup) { - return; - } - - if ( - communityGroup.anchorNodeId === nodeId - || communityGroup.sampleNodeIds?.includes(nodeId) - || communityGroup.memberNodeIds?.includes(nodeId) - ) { - resolvedNodeId = candidateId; - } - }); - - return resolvedNodeId; -} - function buildSelectedEdgeState( edgeId: string, displayGraph: typeof graph | Graph, @@ -953,15 +912,19 @@ export function GraphWorkspace() { ); const inspectableNodeId = focusedSelectionResolution.resolvedNodeId ?? ""; const canActivateFocusedMode = Boolean(focusedSelectionResolution.resolvedNodeId); + const { available: groupedViewAvailable, reason: groupedViewReason } = useMemo( + () => checkGroupedViewAvailability(), + [graphVersion], + ); const groupedDisplayCandidate = useMemo( - () => resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", { - aggregationEnabled, - collapsedNeighborhoodNodeIds, - }), - [aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion], + () => viewMode === "grouped" + ? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", { + aggregationEnabled, + collapsedNeighborhoodNodeIds, + }) + : null, + [viewMode, aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion], ); - const groupedViewAvailable = groupedDisplayCandidate.state.groupedViewAvailable; - const groupedViewReason = groupedDisplayCandidate.state.groupedViewReason; const requestViewMode = useCallback((nextViewMode: GraphViewMode) => { if (nextViewMode === "focused") { @@ -986,7 +949,11 @@ export function GraphWorkspace() { return; } - const groupedDisplayGraph = groupedDisplayCandidate.graph; + const groupedDisplayGraph = groupedDisplayCandidate?.graph + ?? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", { + aggregationEnabled, + collapsedNeighborhoodNodeIds, + }).graph; const nextGroupedSelection = [ lastGroupedSelectedNodeId, selectedNodeId, @@ -1016,7 +983,7 @@ export function GraphWorkspace() { collapsedNeighborhoodNodeIds, focusedNodeId, graphVersion, - groupedDisplayCandidate.graph, + groupedDisplayCandidate, groupedViewAvailable, groupedViewReason, lastGroupedSelectedNodeId, @@ -1030,9 +997,7 @@ export function GraphWorkspace() { } const currentDisplayGraph = pluginRuntimeRef.current?.displayGraph ?? graph; - const nextSelectedNodeId = viewMode === "focused" && graph.hasNode(nodeId) - ? nodeId - : nodeId; + const nextSelectedNodeId = nodeId; if (!graph.hasNode(nodeId) && currentDisplayGraph.hasNode(nodeId)) { setLastGroupedSelectedNodeId(nodeId); @@ -1218,7 +1183,10 @@ export function GraphWorkspace() { const displayResult = useMemo( () => ( viewMode === "grouped" - ? groupedDisplayCandidate + ? (groupedDisplayCandidate ?? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", { + aggregationEnabled, + collapsedNeighborhoodNodeIds, + })) : resolveDisplayGraph(structuralSelectedNodeId, structuralActivePath, structuralActivePathEdgeIds, viewMode, { aggregationEnabled, collapsedNeighborhoodNodeIds, diff --git a/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts index c954babd..401331ba 100644 --- a/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +++ b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts @@ -35,7 +35,7 @@ const COLLAPSE_VISIBLE_NEIGHBORS = 8; const GROUP_SAMPLE_MEMBERS = 8; const AGGREGATED_EDGE_PREFIX = "__agg__:"; const COMMUNITY_NODE_PREFIX = "__community__:"; -const DEBUG_GRAPH_SCENE_STATE = import.meta.env.DEV; +const DEBUG_GRAPH_SCENE_STATE = typeof import.meta !== "undefined" && import.meta.env?.DEV === true; type GraphRef = typeof graph | Graph; @@ -159,7 +159,7 @@ function createEmptyDisplayState( }; } -function resolveGroupedDisplayNodeId( +export function resolveGroupedDisplayNodeId( displayGraph: GraphRef, nodeId: string, ): string | null { @@ -201,6 +201,18 @@ function resolveGroupedDisplayNodeId( return resolvedNodeId; } +export function checkGroupedViewAvailability(): { available: boolean; reason: string | null } { + const base = computeGraphAnalyticsBase(graph, { + computeCommunities: true, + computeCentrality: false, + }); + const available = base.communitiesByNode.size > 0; + return { + available, + reason: available ? null : "Grouped view is unavailable until communities can be detected.", + }; +} + function rankGroupedNeighbors( displayGraph: GraphRef, nodeId: string, diff --git a/explorer/tests/graphSceneState.display.test.ts b/explorer/tests/graphSceneState.display.test.ts index b4865026..5639939e 100644 --- a/explorer/tests/graphSceneState.display.test.ts +++ b/explorer/tests/graphSceneState.display.test.ts @@ -6,7 +6,12 @@ import { batchMergeNodes, clearGraph, } from "../src/store/graphStore.ts"; -import { resolveDisplayGraph } from "../src/workspaces/GraphWorkspace/graphSceneState.ts"; +import { + checkGroupedViewAvailability, + resolveDisplayGraph, + resolveGroupedDisplayNodeId, + resolveGroupedDisplayStateSnapshot, +} from "../src/workspaces/GraphWorkspace/graphSceneState.ts"; function addNode(id: string, semanticGroup = "entity") { batchMergeNodes([ @@ -135,3 +140,120 @@ test("resolveDisplayGraph grouped view emits community nodes and edges", () => { assert.equal(hasCommunityEdge, true); }); + +// ── resolveGroupedDisplayNodeId ────────────────────────────────────────────── + +test("resolveGroupedDisplayNodeId returns null for empty nodeId", () => { + const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false }); + assert.equal(resolveGroupedDisplayNodeId(displayGraph, ""), null); +}); + +test("resolveGroupedDisplayNodeId returns nodeId when it exists directly in display graph", () => { + addNode("x"); + const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false }); + assert.equal(resolveGroupedDisplayNodeId(displayGraph, "x"), "x"); +}); + +test("resolveGroupedDisplayNodeId resolves base node to its community node", () => { + const left = ["a1", "a2", "a3", "a4"]; + const right = ["b1", "b2", "b3", "b4"]; + [...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right")); + + let edgeIndex = 0; + for (let i = 0; i < left.length; i += 1) { + for (let j = 0; j < left.length; j += 1) { + if (i !== j) addEdge(`l-${edgeIndex++}`, left[i], left[j], 3); + } + } + for (let i = 0; i < right.length; i += 1) { + for (let j = 0; j < right.length; j += 1) { + if (i !== j) addEdge(`r-${edgeIndex++}`, right[i], right[j], 3); + } + } + addEdge("bridge-1", "a1", "b1", 0.1); + + const { graph: displayGraph } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true }); + const communityNodes = displayGraph.nodes().filter((n) => n.startsWith("__community__")); + assert.ok(communityNodes.length >= 2, "expected community nodes"); + + const resolved = resolveGroupedDisplayNodeId(displayGraph, "a1"); + assert.ok(resolved !== null, "should resolve a1 to a community node"); + assert.ok(resolved!.startsWith("__community__"), "resolved id should be a community node"); +}); + +// ── resolveGroupedDisplayStateSnapshot ────────────────────────────────────── + +test("resolveGroupedDisplayStateSnapshot returns none-kind when no node selected", () => { + addNode("p"); + addNode("q"); + addEdge("e1", "p", "q"); + const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false }); + const state = resolveGroupedDisplayStateSnapshot(displayGraph, "", { + groupedViewAvailable: true, + groupedViewReason: null, + }); + assert.equal(state.selectedNodeKind, "none"); + assert.equal(state.selectedRootNodeId, null); +}); + +test("resolveGroupedDisplayStateSnapshot maps selected base node to community in grouped graph", () => { + const left = ["c1", "c2", "c3", "c4"]; + const right = ["d1", "d2", "d3", "d4"]; + [...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right")); + + let edgeIndex = 0; + for (let i = 0; i < left.length; i += 1) { + for (let j = 0; j < left.length; j += 1) { + if (i !== j) addEdge(`lc-${edgeIndex++}`, left[i], left[j], 3); + } + } + for (let i = 0; i < right.length; i += 1) { + for (let j = 0; j < right.length; j += 1) { + if (i !== j) addEdge(`rc-${edgeIndex++}`, right[i], right[j], 3); + } + } + addEdge("bridge-c1", "c1", "d1", 0.1); + addEdge("bridge-c2", "c2", "d2", 0.1); + + const { graph: displayGraph } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true }); + const state = resolveGroupedDisplayStateSnapshot(displayGraph, "c1", { + groupedViewAvailable: true, + groupedViewReason: null, + selectedNodeKind: "grouped", + }); + + assert.ok(state.selectedRootNodeId !== null, "should resolve to a community node"); + assert.ok(state.selectedRootNodeId!.startsWith("__community__"), "root should be a community node"); + assert.equal(state.groupedViewAvailable, true); +}); + +// ── checkGroupedViewAvailability ───────────────────────────────────────────── + +test("checkGroupedViewAvailability returns unavailable on empty graph", () => { + const result = checkGroupedViewAvailability(); + assert.equal(result.available, false); + assert.ok(typeof result.reason === "string" && result.reason.length > 0); +}); + +test("checkGroupedViewAvailability returns available when communities exist", () => { + const left = ["e1", "e2", "e3", "e4"]; + const right = ["f1", "f2", "f3", "f4"]; + [...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right")); + + let edgeIndex = 0; + for (let i = 0; i < left.length; i += 1) { + for (let j = 0; j < left.length; j += 1) { + if (i !== j) addEdge(`le-${edgeIndex++}`, left[i], left[j], 3); + } + } + for (let i = 0; i < right.length; i += 1) { + for (let j = 0; j < right.length; j += 1) { + if (i !== j) addEdge(`re-${edgeIndex++}`, right[i], right[j], 3); + } + } + addEdge("bridge-e1", "e1", "f1", 0.1); + + const result = checkGroupedViewAvailability(); + assert.equal(result.available, true); + assert.equal(result.reason, null); +});