Skip to content

Commit 9f19f5e

Browse files
committed
Abstract relation graph, can handle any sequential graph
1 parent 4105f95 commit 9f19f5e

14 files changed

+117
-213
lines changed

src/components/GlobalModalProvider.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,25 @@ import { ReactFlowProvider } from "@xyflow/react"
66
import { useCallback, useState } from "react"
77
import { RFS_GlobalModalContext } from "./RFS_GlobalModalContext"
88
import { ResultViewProps } from "@elastic/react-search-ui-views"
9+
import { GraphNode } from "@/components/graph/GraphNode"
10+
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
911

1012
export function GlobalModalProvider(props: PropsWithChildren<{ resultView: ComponentType<ResultViewProps> }>) {
1113
const [relationGraphState, setRelationGraphState] = useState<{
12-
source: string[]
13-
target: string[]
14-
base: string
14+
nodes: GraphNode[]
15+
options: RelationsGraphOptions
1516
isOpen: boolean
1617
}>({
17-
source: [],
18-
target: [],
19-
base: "",
18+
nodes: [],
19+
options: {},
2020
isOpen: false
2121
})
2222

23-
const openRelationGraph = useCallback((source: string[], base: string, target: string[]) => {
23+
const openRelationGraph = useCallback((nodes: GraphNode[], options?: RelationsGraphOptions) => {
2424
setRelationGraphState({
25-
source,
26-
base,
27-
target,
28-
isOpen: true
25+
nodes,
26+
isOpen: true,
27+
options: options ?? {}
2928
})
3029
}, [])
3130

@@ -37,12 +36,11 @@ export function GlobalModalProvider(props: PropsWithChildren<{ resultView: Compo
3736
<RFS_GlobalModalContext.Provider value={{ openRelationGraph }}>
3837
<ReactFlowProvider>
3938
<RelationsGraphModal
39+
nodes={relationGraphState.nodes}
4040
isOpen={relationGraphState.isOpen}
4141
onOpenChange={onRelationGraphOpenChange}
42-
referencedBy={relationGraphState.source}
43-
references={relationGraphState.target}
44-
base={relationGraphState.base}
4542
resultView={props.resultView}
43+
options={relationGraphState.options}
4644
/>
4745

4846
{props.children}

src/components/RFS_GlobalModalContext.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { createContext } from "react"
2+
import { GraphNode } from "@/components/graph/GraphNode"
3+
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
24

35
export const RFS_GlobalModalContext = createContext<{
4-
openRelationGraph: (source: string[], base: string, target: string[]) => void
6+
openRelationGraph: (nodes: GraphNode[], options?: RelationsGraphOptions) => void
57
}>({
68
openRelationGraph: (): void => {
79
throw "GlobalModalProvider not mounted"

src/components/graph/GraphNode.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface GraphNode {
2+
type: string
3+
id: string
4+
in: string[]
5+
out: string[]
6+
data?: Record<string, unknown>
7+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { GraphNode } from "@/components/graph/GraphNode"
2+
import { toArray } from "@/components/result"
3+
4+
export class GraphNodeUtils {
5+
static buildNodesSequential(type: string, ...ids: (string | string[])[]): GraphNode[] {
6+
const nodes: GraphNode[] = []
7+
8+
for (let layer = 0; layer < ids.length; layer++) {
9+
const previousLayer = layer > 0 ? toArray(ids[layer - 1]) : []
10+
const currentLayer = toArray(ids[layer])
11+
const nextLayer = layer + 1 < ids.length ? toArray(ids[layer + 1]) : []
12+
13+
for (const node of currentLayer) {
14+
nodes.push({
15+
type,
16+
id: node,
17+
in: previousLayer,
18+
out: nextLayer
19+
})
20+
}
21+
}
22+
23+
return nodes
24+
}
25+
}

src/components/graph/PlainNode.tsx

Lines changed: 0 additions & 100 deletions
This file was deleted.

src/components/graph/RelationsGraph.tsx

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildGraphForReferences, getLayoutedElements, ResultPID } from "@/components/graph/helpers"
1+
import { buildGraphForReferences, getLayoutedElements } from "@/components/graph/helpers"
22
import {
33
Background,
44
BackgroundVariant,
@@ -14,28 +14,22 @@ import {
1414
} from "@xyflow/react"
1515
import { ComponentType, useCallback, useEffect, useMemo, useRef } from "react"
1616
import "@xyflow/react/dist/style.css"
17-
import { useStore } from "zustand"
18-
import { resultCache } from "@/lib/ResultCache"
1917
import { ResultViewWrapper } from "@/components/graph/ResultViewWrapper"
2018
import { ResultViewProps } from "@elastic/react-search-ui-views"
19+
import { GraphNode } from "@/components/graph/GraphNode"
20+
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
2121

2222
/**
2323
* Renders an interactive graph for the specified results. Results will be fetched from cache via PID. Currently intended for internal use only.
2424
*/
25-
export function RelationsGraph(props: { base: string; referencedBy: string[]; references: string[]; resultView: ComponentType<ResultViewProps> }) {
26-
const getFromCache = useStore(resultCache, (s) => s.get)
27-
25+
export function RelationsGraph(props: { nodes: GraphNode[]; options?: RelationsGraphOptions; resultView: ComponentType<ResultViewProps> }) {
2826
const { initialEdges, initialNodes } = useMemo(() => {
29-
const base: ResultPID = { pid: props.base, result: getFromCache(props.base) }
30-
const referencedBy = props.referencedBy.map((pid) => ({ pid, result: getFromCache(pid) }))
31-
const references = props.references.map((pid) => ({ pid, result: getFromCache(pid) }))
32-
33-
return buildGraphForReferences(base, referencedBy, references)
34-
}, [getFromCache, props.base, props.referencedBy, props.references])
27+
return buildGraphForReferences(props.nodes)
28+
}, [props.nodes])
3529

3630
const nodeTypes = useMemo(() => {
3731
return {
38-
plain: (nodeProps: NodeProps) => <ResultViewWrapper {...nodeProps} resultView={props.resultView} />
32+
result: (nodeProps: NodeProps) => <ResultViewWrapper {...nodeProps} resultView={props.resultView} />
3933
}
4034
}, [props.resultView])
4135

@@ -58,11 +52,11 @@ export function RelationsGraph(props: { base: string; referencedBy: string[]; re
5852

5953
window.requestAnimationFrame(() => {
6054
setTimeout(() => {
61-
fitView({ nodes: [{ id: props.base }], duration: 200, padding: 1 })
55+
fitView({ nodes: props.options?.focusedNodes?.map((n) => ({ id: n })), duration: 200, padding: 1 })
6256
updateNodeInternals(nodes.map((n) => n.id))
6357
}, 100)
6458
})
65-
}, [nodes, edges, setNodes, setEdges, fitView, props.base, updateNodeInternals])
59+
}, [nodes, edges, setNodes, setEdges, fitView, props.options?.focusedNodes, updateNodeInternals])
6660

6761
const onLayoutDebounced = useRef(onLayout)
6862
useEffect(() => {

src/components/graph/RelationsGraphModal.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
44
import * as VisuallyHidden from "@radix-ui/react-visually-hidden"
55
import { ComponentType, useCallback, useContext } from "react"
66
import { ResultViewProps } from "@elastic/react-search-ui-views"
7+
import { GraphNode } from "@/components/graph/GraphNode"
8+
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
79

810
export function RelationsGraphModal({
911
isOpen,
1012
onOpenChange,
11-
referencedBy,
12-
references,
13-
base,
14-
resultView
13+
nodes,
14+
resultView,
15+
options
1516
}: {
1617
isOpen: boolean
1718
onOpenChange: (val: boolean) => void
18-
referencedBy: string[]
19-
references: string[]
20-
base: string
19+
nodes: GraphNode[]
20+
options?: RelationsGraphOptions
2121
resultView: ComponentType<ResultViewProps>
2222
}) {
2323
const searchContext = useContext(FairDOSearchContext)
@@ -34,7 +34,7 @@ export function RelationsGraphModal({
3434
<Dialog open={isOpen} onOpenChange={onOpenChange}>
3535
<DialogContent className="rfs-h-max rfs-max-h-[min(100vh,800px)] rfs-min-h-[500px] rfs-min-w-[500px] !rfs-max-w-[min(calc(100vw-40px),1500px)] !rfs-p-0">
3636
<VisuallyHidden.Root>
37-
<DialogTitle>Relationship graph for PID {base}</DialogTitle>
37+
<DialogTitle>Relationship graph</DialogTitle>
3838
</VisuallyHidden.Root>
3939

4040
<FairDOSearchContext.Provider
@@ -46,7 +46,7 @@ export function RelationsGraphModal({
4646
config: searchContext.config
4747
}}
4848
>
49-
<RelationsGraph referencedBy={referencedBy} references={references} base={base} resultView={resultView} />
49+
<RelationsGraph nodes={nodes} resultView={resultView} options={options} />
5050
</FairDOSearchContext.Provider>
5151
</DialogContent>
5252
</Dialog>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export interface RelationsGraphOptions {
2+
focusedNodes?: string[]
3+
}

src/components/graph/ResultViewWrapper.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@ import { ResultViewProps } from "@elastic/react-search-ui-views"
44
import { FairDOSearchContext } from "@/components/FairDOSearchContext"
55
import { Button } from "@/components/ui/button"
66
import { SearchIcon } from "lucide-react"
7+
import { useStore } from "zustand/index"
8+
import { resultCache } from "@/lib/ResultCache"
9+
10+
export function ResultViewWrapper({ resultView: ResultView, id }: NodeProps & { resultView: ComponentType<ResultViewProps> }) {
11+
const get = useStore(resultCache, (s) => s.get)
12+
13+
const data = useMemo(() => {
14+
return get(id)
15+
}, [get, id])
716

8-
export function ResultViewWrapper({ resultView: ResultView, data, id }: NodeProps & { resultView: ComponentType<ResultViewProps> }) {
917
const dataEmpty = useMemo(() => {
10-
return Object.keys(data).length === 0
18+
return !data || Object.keys(data).length === 0
1119
}, [data])
1220

1321
const { searchFor } = useContext(FairDOSearchContext)
@@ -19,7 +27,7 @@ export function ResultViewWrapper({ resultView: ResultView, data, id }: NodeProp
1927
return (
2028
<div className="rfs-w-[800px] -rfs-m-2">
2129
<Handle type="target" position={Position.Left} />
22-
{dataEmpty ? (
30+
{!data || dataEmpty ? (
2331
<div className="rfs-m-2 rfs-p-4 rfs-rounded-lg rfs-bg-background rfs-border rfs-flex rfs-justify-between rfs-items-center">
2432
<div>
2533
<div>{id}</div>

0 commit comments

Comments
 (0)