Skip to content

Commit 83e8235

Browse files
committed
Relations graph usability improvements
1 parent 50da4f4 commit 83e8235

File tree

4 files changed

+62
-90
lines changed

4 files changed

+62
-90
lines changed

src/components/graph/RelationsGraph.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildGraphForReferences, getLayoutedElements } from "@/components/graph/helpers"
1+
import { buildGraphFromNodes, computeNodeLayout } from "@/components/graph/helpers"
22
import {
33
Background,
44
BackgroundVariant,
@@ -33,7 +33,7 @@ export function RelationsGraph(props: { nodes: GraphNode[]; options?: RelationsG
3333
}, [])
3434

3535
const { initialEdges, initialNodes } = useMemo(() => {
36-
return buildGraphForReferences(props.nodes)
36+
return buildGraphFromNodes(props.nodes)
3737
}, [props.nodes])
3838

3939
const nodeTypes = useMemo(() => {
@@ -54,7 +54,7 @@ export function RelationsGraph(props: { nodes: GraphNode[]; options?: RelationsG
5454
}, [initialEdges, initialNodes, setEdges, setNodes])
5555

5656
const onLayout = useCallback(() => {
57-
const layouted = getLayoutedElements(nodes, edges)
57+
const layouted = computeNodeLayout(nodes, edges)
5858

5959
setNodes([...layouted.nodes])
6060
setEdges([...layouted.edges])
@@ -87,6 +87,9 @@ export function RelationsGraph(props: { nodes: GraphNode[]; options?: RelationsG
8787
onEdgesChange={onEdgesChange}
8888
proOptions={{ hideAttribution: true }}
8989
colorMode={colorMode}
90+
nodesDraggable={false}
91+
nodesConnectable={false}
92+
edgesFocusable={false}
9093
>
9194
<Background color="hsl(var(--rfs-border))" variant={BackgroundVariant.Lines} />
9295
<Controls />

src/components/graph/RelationsGraphModal.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { ComponentType, useCallback, useContext } from "react"
66
import { ResultViewProps } from "@elastic/react-search-ui-views"
77
import { GraphNode } from "@/components/graph/GraphNode"
88
import { RelationsGraphOptions } from "@/components/graph/RelationsGraphOptions"
9+
import { Button } from "@/components/ui/button"
10+
import { X } from "lucide-react"
911

1012
export function RelationsGraphModal({
1113
isOpen,
@@ -32,7 +34,7 @@ export function RelationsGraphModal({
3234

3335
return (
3436
<Dialog open={isOpen} onOpenChange={onOpenChange}>
35-
<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">
37+
<DialogContent className="rfs-h-[calc(100vh-40px)] rfs-max-w-none rfs-w-[calc(100vw-40px)] !rfs-p-0" hideCloseButton>
3638
<VisuallyHidden.Root>
3739
<DialogTitle>Relationship graph</DialogTitle>
3840
</VisuallyHidden.Root>
@@ -48,6 +50,12 @@ export function RelationsGraphModal({
4850
>
4951
<RelationsGraph nodes={nodes} resultView={resultView} options={options} />
5052
</FairDOSearchContext.Provider>
53+
54+
<div className="rfs-absolute rfs-right-4 rfs-top-4">
55+
<Button variant="outline" onClick={() => onOpenChange(false)}>
56+
<X className="rfs-size-4" /> Close <span className="rfs-font-mono rfs-text-muted-foreground rfs-ml-2">Esc</span>
57+
</Button>
58+
</div>
5159
</DialogContent>
5260
</Dialog>
5361
)

src/components/graph/helpers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Edge, Node } from "@xyflow/react"
22
import Dagre from "@dagrejs/dagre"
33
import { GraphNode } from "@/components/graph/GraphNode"
44

5-
export function buildGraphForReferences(nodes: GraphNode[]) {
5+
export function buildGraphFromNodes(nodes: GraphNode[]) {
66
const initialNodes: { id: string; type: string; position: { x: number; y: number }; data: Record<string, unknown> }[] = nodes.map((node) => ({
77
type: node.type,
88
id: node.id,
@@ -25,9 +25,9 @@ export function buildGraphForReferences(nodes: GraphNode[]) {
2525
return { initialNodes, initialEdges }
2626
}
2727

28-
export function getLayoutedElements(nodes: (Node & { type: string })[], edges: Edge[]) {
28+
export function computeNodeLayout(nodes: (Node & { type: string })[], edges: Edge[]) {
2929
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}))
30-
g.setGraph({ rankdir: "LR", nodesep: 25, ranksep: 100 })
30+
g.setGraph({ rankdir: "LR", nodesep: 30, ranksep: 150 })
3131

3232
edges.forEach((edge) => g.setEdge(edge.source, edge.target))
3333
nodes.forEach((node) =>

src/components/ui/dialog.tsx

Lines changed: 44 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -15,108 +15,69 @@ const DialogPortal = DialogPrimitive.Portal
1515
const DialogClose = DialogPrimitive.Close
1616

1717
const DialogOverlay = React.forwardRef<
18-
React.ElementRef<typeof DialogPrimitive.Overlay>,
19-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
18+
React.ElementRef<typeof DialogPrimitive.Overlay>,
19+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
2020
>(({ className, ...props }, ref) => (
21-
<DialogPrimitive.Overlay
22-
ref={ref}
23-
className={cn(
24-
"rfs-fixed rfs-inset-0 rfs-z-50 rfs-bg-black/80 rfs- data-[state=open]:rfs-animate-in data-[state=closed]:rfs-animate-out data-[state=closed]:rfs-fade-out-0 data-[state=open]:rfs-fade-in-0",
25-
className
26-
)}
27-
{...props}
28-
/>
21+
<DialogPrimitive.Overlay
22+
ref={ref}
23+
className={cn(
24+
"rfs-fixed rfs-inset-0 rfs-z-50 rfs-bg-black/80 rfs- data-[state=open]:rfs-animate-in data-[state=closed]:rfs-animate-out data-[state=closed]:rfs-fade-out-0 data-[state=open]:rfs-fade-in-0",
25+
className
26+
)}
27+
{...props}
28+
/>
2929
))
3030
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
3131

3232
const DialogContent = React.forwardRef<
33-
React.ElementRef<typeof DialogPrimitive.Content>,
34-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
33+
React.ElementRef<typeof DialogPrimitive.Content>,
34+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { hideCloseButton?: boolean }
3535
>(({ className, children, ...props }, ref) => (
36-
<DialogPortal>
37-
<DialogOverlay />
38-
<DialogPrimitive.Content
39-
ref={ref}
40-
className={cn(
41-
"rfs-fixed rfs-left-[50%] rfs-top-[50%] rfs-z-50 rfs-grid rfs-w-full rfs-max-w-lg rfs-translate-x-[-50%] rfs-translate-y-[-50%] rfs-gap-4 rfs-border rfs-bg-background rfs-p-6 rfs-shadow-lg rfs-duration-200 data-[state=open]:rfs-animate-in data-[state=closed]:rfs-animate-out data-[state=closed]:rfs-fade-out-0 data-[state=open]:rfs-fade-in-0 data-[state=closed]:rfs-zoom-out-95 data-[state=open]:rfs-zoom-in-95 data-[state=closed]:rfs-slide-out-to-left-1/2 data-[state=closed]:rfs-slide-out-to-top-[48%] data-[state=open]:rfs-slide-in-from-left-1/2 data-[state=open]:rfs-slide-in-from-top-[48%] sm:rfs-rounded-lg",
42-
className
43-
)}
44-
{...props}
45-
>
46-
{children}
47-
<DialogPrimitive.Close className="rfs-absolute rfs-right-4 rfs-top-4 rfs-rounded-sm rfs-opacity-70 rfs-ring-offset-background rfs-transition-opacity hover:rfs-opacity-100 focus:rfs-outline-none focus:rfs-ring-2 focus:rfs-ring-ring focus:rfs-ring-offset-2 disabled:rfs-pointer-events-none data-[state=open]:rfs-bg-accent data-[state=open]:rfs-text-muted-foreground">
48-
<X className="rfs-h-4 rfs-w-4" />
49-
<span className="rfs-sr-only">Close</span>
50-
</DialogPrimitive.Close>
51-
</DialogPrimitive.Content>
52-
</DialogPortal>
36+
<DialogPortal>
37+
<DialogOverlay />
38+
<DialogPrimitive.Content
39+
ref={ref}
40+
className={cn(
41+
"rfs-fixed rfs-left-[50%] rfs-top-[50%] rfs-z-50 rfs-grid rfs-w-full rfs-max-w-lg rfs-translate-x-[-50%] rfs-translate-y-[-50%] rfs-gap-4 rfs-border rfs-bg-background rfs-p-6 rfs-shadow-lg rfs-duration-200 data-[state=open]:rfs-animate-in data-[state=closed]:rfs-animate-out data-[state=closed]:rfs-fade-out-0 data-[state=open]:rfs-fade-in-0 data-[state=closed]:rfs-zoom-out-95 data-[state=open]:rfs-zoom-in-95 data-[state=closed]:rfs-slide-out-to-left-1/2 data-[state=closed]:rfs-slide-out-to-top-[48%] data-[state=open]:rfs-slide-in-from-left-1/2 data-[state=open]:rfs-slide-in-from-top-[48%] sm:rfs-rounded-lg",
42+
className
43+
)}
44+
{...props}
45+
>
46+
{children}
47+
{!props.hideCloseButton && (
48+
<DialogPrimitive.Close className="rfs-absolute rfs-right-4 rfs-top-4 rfs-rounded-sm rfs-opacity-70 rfs-ring-offset-background rfs-transition-opacity hover:rfs-opacity-100 focus:rfs-outline-none focus:rfs-ring-2 focus:rfs-ring-ring focus:rfs-ring-offset-2 disabled:rfs-pointer-events-none data-[state=open]:rfs-bg-accent data-[state=open]:rfs-text-muted-foreground">
49+
<X className="rfs-h-4 rfs-w-4" />
50+
<span className="rfs-sr-only">Close</span>
51+
</DialogPrimitive.Close>
52+
)}
53+
</DialogPrimitive.Content>
54+
</DialogPortal>
5355
))
5456
DialogContent.displayName = DialogPrimitive.Content.displayName
5557

56-
const DialogHeader = ({
57-
className,
58-
...props
59-
}: React.HTMLAttributes<HTMLDivElement>) => (
60-
<div
61-
className={cn(
62-
"rfs-flex rfs-flex-col rfs-space-y-1.5 rfs-text-center sm:rfs-text-left",
63-
className
64-
)}
65-
{...props}
66-
/>
58+
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
59+
<div className={cn("rfs-flex rfs-flex-col rfs-space-y-1.5 rfs-text-center sm:rfs-text-left", className)} {...props} />
6760
)
6861
DialogHeader.displayName = "DialogHeader"
6962

70-
const DialogFooter = ({
71-
className,
72-
...props
73-
}: React.HTMLAttributes<HTMLDivElement>) => (
74-
<div
75-
className={cn(
76-
"rfs-flex rfs-flex-col-reverse sm:rfs-flex-row sm:rfs-justify-end sm:rfs-space-x-2",
77-
className
78-
)}
79-
{...props}
80-
/>
63+
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
64+
<div className={cn("rfs-flex rfs-flex-col-reverse sm:rfs-flex-row sm:rfs-justify-end sm:rfs-space-x-2", className)} {...props} />
8165
)
8266
DialogFooter.displayName = "DialogFooter"
8367

84-
const DialogTitle = React.forwardRef<
85-
React.ElementRef<typeof DialogPrimitive.Title>,
86-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87-
>(({ className, ...props }, ref) => (
88-
<DialogPrimitive.Title
89-
ref={ref}
90-
className={cn(
91-
"rfs-text-lg rfs-font-semibold rfs-leading-none rfs-tracking-tight",
92-
className
93-
)}
94-
{...props}
95-
/>
96-
))
68+
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>(
69+
({ className, ...props }, ref) => (
70+
<DialogPrimitive.Title ref={ref} className={cn("rfs-text-lg rfs-font-semibold rfs-leading-none rfs-tracking-tight", className)} {...props} />
71+
)
72+
)
9773
DialogTitle.displayName = DialogPrimitive.Title.displayName
9874

9975
const DialogDescription = React.forwardRef<
100-
React.ElementRef<typeof DialogPrimitive.Description>,
101-
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
76+
React.ElementRef<typeof DialogPrimitive.Description>,
77+
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
10278
>(({ className, ...props }, ref) => (
103-
<DialogPrimitive.Description
104-
ref={ref}
105-
className={cn("rfs-text-sm rfs-text-muted-foreground", className)}
106-
{...props}
107-
/>
79+
<DialogPrimitive.Description ref={ref} className={cn("rfs-text-sm rfs-text-muted-foreground", className)} {...props} />
10880
))
10981
DialogDescription.displayName = DialogPrimitive.Description.displayName
11082

111-
export {
112-
Dialog,
113-
DialogPortal,
114-
DialogOverlay,
115-
DialogClose,
116-
DialogTrigger,
117-
DialogContent,
118-
DialogHeader,
119-
DialogFooter,
120-
DialogTitle,
121-
DialogDescription,
122-
}
83+
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }

0 commit comments

Comments
 (0)