Skip to content

Commit

Permalink
fix #103 add nodedata view (#104)
Browse files Browse the repository at this point in the history
* fix #103 add nodedata view
* break GraphView and DataPanle to components
  • Loading branch information
gkorland authored Feb 26, 2024
1 parent c35bebd commit ab8ea77
Show file tree
Hide file tree
Showing 3 changed files with 255 additions and 158 deletions.
41 changes: 41 additions & 0 deletions app/graph/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";

export default function DataPanel({node}: {node: Node}) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Field</TableHead>
<TableHead>Value</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{
Object.entries(node).map((row, index) => (
// eslint-disable-next-line react/no-array-index-key
<TableRow key={index}>
{
Object.values(row).map((cell, cellIndex) => (
// eslint-disable-next-line react/no-array-index-key
<TableCell key={cellIndex}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="max-w-96 truncate">
{JSON.stringify(cell)}
</TooltipTrigger>
<TooltipContent>
<p>{JSON.stringify(cell)}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</TableCell>
))
}
</TableRow>
))
}
</TableBody>
</Table>
)
}
208 changes: 208 additions & 0 deletions app/graph/GraphView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import CytoscapeComponent from "react-cytoscapejs";
import { toast } from "@/components/ui/use-toast";
import cytoscape, { ElementDefinition, EventObject, NodeDataDefinition } from "cytoscape";
import { RefAttributes, useRef, useState, useImperativeHandle } from "react";
import { signOut } from "next-auth/react";
import fcose from 'cytoscape-fcose';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { ImperativePanelHandle } from "react-resizable-panels";
import Labels from "./labels";
import Toolbar from "./toolbar";
import { Category, Graph } from "./model";
import DataPanel from "./DataPanel";

const LAYOUT = {
name: "fcose",
fit: true,
padding: 30,
}

cytoscape.use(fcose);

// The stylesheet for the graph
function getStyle(darkmode: boolean) {

const style: cytoscape.Stylesheet[] = [
{
selector: "core",
style: {
'active-bg-size': 0, // hide gray circle when panning
// All of the following styles are meaningless and are specified
// to satisfy the linter...
'active-bg-color': 'blue',
'active-bg-opacity': 0.3,
"selection-box-border-color": 'blue',
"selection-box-border-width": 0,
"selection-box-opacity": 1,
"selection-box-color": 'blue',
"outside-texture-bg-color": 'blue',
"outside-texture-bg-opacity": 1,
},
},
{
selector: "node",
style: {
label: "data(name)",
"text-valign": "center",
"text-halign": "center",
"text-wrap": "ellipsis",
"text-max-width": "10rem",
shape: "ellipse",
height: "10rem",
width: "10rem",
"border-width": 0.15,
"border-opacity": 0.5,
"background-color": "data(color)",
"font-size": "3rem",
"overlay-padding": "1rem",
},
},
{
selector: "node:active",
style: {
"overlay-opacity": 0, // hide gray box around active node
},
},
{
selector: "edge",
style: {
width: 0.5,
"line-color": "#ccc",
"arrow-scale": 0.3,
"target-arrow-shape": "triangle",
label: "data(label)",
'curve-style': 'straight',
"text-background-color": darkmode ? "#020817" : "white",
"color": darkmode ? "white" : "black",
"text-background-opacity": 1,
"font-size": "3rem",
"overlay-padding": "2rem",

},
},
]
return style
}

export interface GraphViewRef {
expand: (elements: ElementDefinition[]) => void
}

interface GraphViewProps extends RefAttributes<GraphViewRef> {
graph: Graph,
darkmode: boolean
}

export function GraphView({ graph, darkmode, ref }: GraphViewProps) {

const [selectedNode, setSelectedNode] = useState<Node | null>(null);

// A reference to the chart container to allowing zooming and editing
const chartRef = useRef<cytoscape.Core | null>(null)
const dataPanel = useRef<ImperativePanelHandle>(null)

useImperativeHandle(ref, () => ({
expand: (elements) => {
const chart = chartRef.current
if (chart) {
chart.elements().remove()
chart.add(elements)
chart.elements().layout(LAYOUT).run();
}
}
}))

// Send the user query to the server to expand a node
async function onFetchNode(node: NodeDataDefinition) {
const result = await fetch(`/api/graph/${graph.Id}/${node.id}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})

if (result.status >= 300) {
toast({
title: "Error",
description: result.text(),
})
if (result.status >= 400 && result.status < 500) {
signOut({ callbackUrl: '/login' })
}
return [] as ElementDefinition[]
}

const json = await result.json()
const elements = graph.extend(json.result)
return elements
}

const onCategoryClick = (category: Category) => {
const chart = chartRef.current
if (chart) {
const elements = chart.elements(`node[category = "${category.name}"]`)

// eslint-disable-next-line no-param-reassign
category.show = !category.show

if (category.show) {
elements.style({ display: 'element' })
} else {
elements.style({ display: 'none' })
}
chart.elements().layout(LAYOUT).run();
}
}

const handleDoubleClick = async (evt: EventObject) => {
const node: Node = evt.target.json().data;
const elements = await onFetchNode(node);

// adjust entire graph.
if (chartRef.current && elements.length > 0) {
chartRef.current.add(elements);
chartRef.current.elements().layout(LAYOUT).run();
}
}

const handleTap = (evt: EventObject) => {
const node: Node = evt.target.json().data;
setSelectedNode(node);
dataPanel.current?.expand();
}

return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel className="h-full flex flex-col">
<div className="grid grid-cols-6">
<Toolbar className="col-start-1 justify-start" chartRef={chartRef} />
<Labels className="col-end-7 justify-end" categories={graph.Categories} onClick={onCategoryClick} />
</div>
<CytoscapeComponent
cy={(cy) => {
chartRef.current = cy

// Make sure no previous listeners are attached
cy.removeAllListeners();

// Listen to the double click event on nodes for expanding the node
cy.on('dbltap', 'node', handleDoubleClick);

// Listen to the click event on nodes for showing node properties
cy.on('tap', 'node', handleTap);
}}
stylesheet={getStyle(darkmode)}
elements={graph.Elements}
layout={LAYOUT}
className="w-full grow"
/>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel ref={dataPanel} maxSize={50} minSize={10} collapsible defaultSize={selectedNode ? 20 : 0} className="bg-gray-100 dark:bg-gray-800">
{selectedNode && <DataPanel node={selectedNode} />}
</ResizablePanel>
</ResizablePanelGroup>


)
}
Loading

0 comments on commit ab8ea77

Please sign in to comment.