This commit is contained in:
Ole
2026-05-31 20:25:41 +00:00
commit 0a07ab8593
275 changed files with 52660 additions and 0 deletions
@@ -0,0 +1,288 @@
import { useDune } from "@cognite/dune";
import { useCallback, useMemo, useRef, useState } from "react";
import type { GraphCanvasRef } from "reagraph";
import { useGraphSelection } from "./useGraphSelection";
import { useNodeBuffer } from "./useNodeBuffer";
import { useGraphDataPipeline } from "./useGraphDataPipeline";
import { buildReagraphTheme, mergeVisualConfig } from "./graph-config";
import { fetchConnectedNodes } from "./graph-service";
import type { GraphNode, GraphEdge, LayoutType } from "./types";
import { createInstanceId, parseInstanceId } from "./types";
import { useDataModelLoader } from "./useDataModelLoader";
import { useSeedNode } from "./useSeedNode";
import { GraphViewerCanvas } from "./GraphViewerCanvas";
import {
DEFAULT_LITE_FEATURES,
type LiteFeatureFlags,
type UseGraphViewerConfig,
type UseGraphViewerReturn,
} from "./types";
/**
* `useGraphViewer` -- the single entry point for embedding a CDF graph viewer.
*
* Returns a self-contained `<GraphCanvas>` component plus state and controls.
*
* @example
* ```tsx
* const { GraphCanvas, isLoading, error } = useGraphViewer({
* dataModel: { space: "my-space", externalId: "my-dm", version: "1" },
* instance: { space: "my-inst-space", externalId: "pump-001" },
* });
*
* return <GraphCanvas className="h-full w-full" />;
* ```
*/
export function useGraphViewer(config: UseGraphViewerConfig): UseGraphViewerReturn {
const { sdk } = useDune();
const opts = config.options ?? {};
const maxNodes = opts.maxNodes ?? 1000;
const initialConnectionLimit = opts.initialConnectionLimit ?? 100;
const features: LiteFeatureFlags = { ...DEFAULT_LITE_FEATURES, ...opts.features };
const whitelistedRelationProps = useMemo(
() =>
opts.whitelistedRelationProps
? new Set(opts.whitelistedRelationProps)
: undefined,
[JSON.stringify(opts.whitelistedRelationProps)]
);
const visualConfig = useMemo(
() => mergeVisualConfig(opts.visualConfig),
[opts.visualConfig]
);
const themeConfig = useMemo(
() => buildReagraphTheme(opts.themeConfig),
[opts.themeConfig]
);
const [layout, setLayout] = useState<LayoutType>(opts.layout ?? "forceDirected2d");
const [selections, setSelections] = useState<string[]>([]);
const [selectedNodeType, setSelectedNodeType] = useState<string | null>(null);
const {
selectedNode,
selectedEdge,
selectNode,
selectEdge,
clearSelection,
} = useGraphSelection();
const {
nodes: bufferNodes,
edges: bufferEdges,
addNodes,
addEdges,
touchNode,
clear: clearBuffer,
} = useNodeBuffer(maxNodes);
const {
dataModel,
isLoading: isDataModelLoading,
error: dataModelError,
} = useDataModelLoader(config.dataModel);
const {
isLoading: isSeedLoading,
error: seedError,
loadInstance,
} = useSeedNode({
dataModel,
initialInstance: config.instance,
addNodes,
addEdges,
clearBuffer,
whitelistedRelationProps,
coreReverseQueries: opts.coreReverseQueries,
viewPriorityConfig: opts.viewPriorityConfig,
initialConnectionLimit,
});
const graphRef = useRef<GraphCanvasRef>(null);
const {
graphData,
displayedGraphData,
reagraphNodes,
reagraphEdges,
displayedStats,
} = useGraphDataPipeline({
bufferNodes,
bufferConnections: bufferEdges,
visualConfig,
});
const [isExpanding, setIsExpanding] = useState(false);
const expandNode = useCallback(
async (nodeId: string) => {
if (!sdk || !dataModel) return;
try {
setIsExpanding(true);
const { space, externalId } = parseInstanceId(nodeId);
const existingIds = new Set(
bufferNodes.map((n) => createInstanceId(n.space, n.externalId))
);
const result = await fetchConnectedNodes(
sdk,
space,
externalId,
existingIds,
dataModel,
initialConnectionLimit,
whitelistedRelationProps,
opts.coreReverseQueries,
opts.viewPriorityConfig
);
addNodes(result.newNodes);
addEdges(result.newEdges);
touchNode(nodeId);
} finally {
setIsExpanding(false);
}
},
[
sdk,
dataModel,
bufferNodes,
addNodes,
addEdges,
touchNode,
initialConnectionLimit,
whitelistedRelationProps,
opts.coreReverseQueries,
opts.viewPriorityConfig,
]
);
const handleNodeClick = useCallback(
(node: GraphNode) => {
selectNode(node);
setSelections([node.id]);
setSelectedNodeType(null);
touchNode(node.id);
},
[selectNode, setSelections, touchNode]
);
const handleEdgeClick = useCallback(
(edge: GraphEdge) => {
selectEdge(edge);
setSelections([edge.id]);
setSelectedNodeType(null);
},
[selectEdge, setSelections]
);
const handleCanvasClick = useCallback(() => {
clearSelection();
setSelections([]);
setSelectedNodeType(null);
}, [clearSelection]);
const handleNodeTypeClick = useCallback(
(typeKey: string) => {
if (selectedNodeType === typeKey) {
setSelectedNodeType(null);
setSelections([]);
clearSelection();
} else {
const nodeIds = displayedGraphData.nodes
.filter((n) => {
const key = n.data?.type
? `${n.data.type.space}:${n.data.type.externalId}`
: "unknown";
return key === typeKey;
})
.map((n) => n.id);
setSelectedNodeType(typeKey);
setSelections(nodeIds);
clearSelection();
}
},
[selectedNodeType, displayedGraphData.nodes, clearSelection]
);
const handleClearNodeTypeSelection = useCallback(() => {
setSelectedNodeType(null);
setSelections([]);
}, []);
const GraphCanvasComponent = useMemo(() => {
const Component: React.FC<{ className?: string }> = ({ className }) => (
<GraphViewerCanvas
reagraphNodes={reagraphNodes}
reagraphEdges={reagraphEdges}
displayedGraphData={displayedGraphData}
layout={layout}
theme={themeConfig}
selections={selections}
selectedNode={selectedNode}
selectedEdge={selectedEdge}
features={features}
selectedNodeType={selectedNodeType}
graphRef={graphRef}
onNodeClick={handleNodeClick}
onEdgeClick={handleEdgeClick}
onCanvasClick={handleCanvasClick}
onExpandNode={expandNode}
onNodeTypeClick={handleNodeTypeClick}
onClearNodeTypeSelection={handleClearNodeTypeSelection}
className={className}
/>
);
Component.displayName = "GraphViewerCanvas";
return Component;
}, [
reagraphNodes,
reagraphEdges,
displayedGraphData,
layout,
themeConfig,
selections,
selectedNode,
selectedEdge,
features,
selectedNodeType,
graphRef,
handleNodeClick,
handleEdgeClick,
handleCanvasClick,
expandNode,
handleNodeTypeClick,
handleClearNodeTypeSelection,
]);
const isLoading = isDataModelLoading || isSeedLoading || isExpanding;
const error = dataModelError || seedError;
return {
GraphCanvas: GraphCanvasComponent,
isLoading,
error,
graphData,
stats: displayedStats,
layout,
setLayout,
selections,
setSelections,
selectedNode,
selectedEdge,
expandNode,
loadInstance,
fitView: () => graphRef.current?.fitNodesInView(),
zoomIn: () => graphRef.current?.zoomIn(),
zoomOut: () => graphRef.current?.zoomOut(),
clear: clearBuffer,
graphRef,
};
}