diff --git a/app/api/graph/[graph]/[node]/route.ts b/app/api/graph/[graph]/[node]/route.ts
new file mode 100644
index 00000000..62803529
--- /dev/null
+++ b/app/api/graph/[graph]/[node]/route.ts
@@ -0,0 +1,35 @@
+import { NextRequest, NextResponse } from "next/server";
+import { Graph } from 'falkordb';
+import { getServerSession } from "next-auth/next";
+import authOptions, { connections } from "../../../auth/[...nextauth]/options";
+
+export async function GET(request: NextRequest, { params }: { params: { graph: string, node: string } }) {
+
+ const session = await getServerSession(authOptions)
+ const id = session?.user?.id
+ if (!id) {
+ return NextResponse.json({ message: "Not authenticated" }, { status: 401 })
+ }
+
+ let client = connections.get(id)
+ if (!client) {
+ return NextResponse.json({ message: "Not authenticated" }, { status: 401 })
+ }
+
+ const nodeId = parseInt(params.node);
+ const graphId = params.graph;
+
+ const graph = new Graph(client, graphId);
+
+ // Get node's neighbors
+ const query = `MATCH (src)-[e]-(n)
+ WHERE ID(src) = $nodeId
+ RETURN e, n`;
+
+ try {
+ let result: any = await graph.query(query, { params: { nodeId: nodeId } });
+ return NextResponse.json({ result: result }, { status: 200 })
+ } catch (err: any) {
+ return NextResponse.json({ message: err.message }, { status: 400 })
+ }
+}
diff --git a/app/graph/labels.tsx b/app/graph/labels.tsx
new file mode 100644
index 00000000..ea51a360
--- /dev/null
+++ b/app/graph/labels.tsx
@@ -0,0 +1,36 @@
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { Category, getCategoryColors } from "./model";
+import { cn } from "@/lib/utils";
+import { MinusCircle, Palette, PlusCircle } from "lucide-react";
+import { useState } from "react";
+
+export function Labels(params: { categories: Category[], className?: string, onClick: (category: Category) => void }) {
+
+ // fake stae to force reload
+ const [reload, setReload] = useState(false)
+
+ return (
+
+
+ {params.categories.map((category) => {
+ return (
+
+ {
+ params.onClick(category)
+ setReload(!reload)
+ }}
+ >
+ { category.show ? : }
+
+
+ {category.name}
+
+
+ )
+ })}
+
+
+ )
+}
\ No newline at end of file
diff --git a/app/graph/model.ts b/app/graph/model.ts
index c83989d3..6fd8b30f 100644
--- a/app/graph/model.ts
+++ b/app/graph/model.ts
@@ -1,7 +1,8 @@
export interface Category {
+ index: number,
name: string,
- index: number
+ show: boolean,
}
export interface Node {
@@ -19,18 +20,21 @@ export interface Edge {
}
const COLORS = [
- "#ff0000", // red
- "#0000ff", // blue
- "#00ff00", // green
- "#ffff00", // yellow
- "#ff00ff", // magenta
- "#00ffff", // cyan
- "#ffffff", // white
- "#000000", // black
- "#800000", // maroon
- "#808000", // olive
+ "red",
+ "blue",
+ "green",
+ "yellow",
+ "purple",
+ "fuchsia",
+ "aqua",
+ "gray",
+ "orange",
]
+export function getCategoryColors(index: number): string {
+ return index(), new Map(), new Map())
}
- public static create(results: any): Graph {
+ public static create(id: string, results: any): Graph {
let graph = Graph.empty()
graph.extend(results)
- graph.id = results.id
+ graph.id = id
return graph
}
@@ -130,8 +134,9 @@ export class Graph {
// check if category already exists in categories
let category = this.categoriesMap.get(cell.labels[0])
if (!category) {
- category = { name: cell.labels[0], index: this.categoriesMap.size }
+ category = { name: cell.labels[0], index: this.categoriesMap.size, show: true }
this.categoriesMap.set(category.name, category)
+ this.categories.push(category)
}
// check if node already exists in nodes or fake node was created
@@ -141,7 +146,7 @@ export class Graph {
id: cell.id.toString(),
name: cell.id.toString(),
value: JSON.stringify(cell),
- color: category.index < COLORS.length ? COLORS[category.index] : COLORS[0]
+ color: getCategoryColors(category.index)
}
this.nodesMap.set(cell.id, node)
this.elements.push({data:node})
diff --git a/app/graph/page.tsx b/app/graph/page.tsx
index 19b41158..4f41440e 100644
--- a/app/graph/page.tsx
+++ b/app/graph/page.tsx
@@ -8,10 +8,11 @@ import CytoscapeComponent from 'react-cytoscapejs'
import { useRef, useState } from "react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { XCircle, ZoomIn, ZoomOut } from "lucide-react";
-import { Node, Graph } from "./model";
+import { Node, Graph, Category, getCategoryColors } from "./model";
import { signOut } from "next-auth/react";
import { Toolbar } from "./toolbar";
import { Query, QueryState } from "./query";
+import { Labels } from "./labels";
// The stylesheet for the graph
const STYLESHEET: cytoscape.Stylesheet[] = [
@@ -62,7 +63,7 @@ export default function Page() {
const chartRef = useRef(null)
// A reference to the query state to allow running the user query
- const queryState = useRef(null)
+ const queryState = useRef(null)
function prepareArg(arg: string): string {
return encodeURIComponent(arg.trim())
@@ -71,7 +72,7 @@ export default function Page() {
async function runQuery(event: any) {
event.preventDefault();
let state = queryState.current;
- if(!state){
+ if (!state) {
return
}
@@ -95,22 +96,67 @@ export default function Page() {
}
let json = await result.json()
- let newGraph = Graph.create(json.result)
+ let newGraph = Graph.create(state.graphName, json.result)
setGraph(newGraph)
let chart = chartRef.current
- if(chart){
+ if (chart) {
chart.elements().remove()
chart.add(newGraph.Elements)
chart.elements().layout(LAYOUT).run();
}
}
+ // Send the user query to the server to expand a node
+ async function onFetchNode(node: Node) {
+ let 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: '/' })
+ }
+ return [] as any[]
+ }
+
+ let json = await result.json()
+ let elements = graph.extend(json.result)
+ return elements
+ }
+
+ function onCategoryClick(category: Category) {
+ let chart = chartRef.current
+ if (chart) {
+ let color = getCategoryColors(category.index)
+ let elements = chart.elements(`[color = "${color}"]`)
+
+ category.show = !category.show
+
+ if (category.show) {
+ elements.style({ display: 'element' })
+ } else {
+ elements.style({ display: 'none' })
+ }
+ chart.elements().layout(LAYOUT).run();
+ }
+ }
+
return (
queryState.current = state} />
-
+
+
+
+
{
chartRef.current = cy
@@ -119,10 +165,15 @@ export default function Page() {
cy.removeAllListeners();
// Listen to the click event on nodes for expanding the node
- cy.on('dbltap', 'node', function (evt) {
+ cy.on('dbltap', 'node', async function (evt) {
var node: Node = evt.target.json().data;
- // TODO:
- // parmas.onFetchNode(node);
+ let elements = await onFetchNode(node);
+
+ // adjust entire graph.
+ if (elements.length > 0) {
+ cy.add(elements);
+ cy.elements().layout(LAYOUT).run();
+ }
});
}}
stylesheet={STYLESHEET}
diff --git a/app/graph/toolbar.tsx b/app/graph/toolbar.tsx
index 662ad70e..34b9db0a 100644
--- a/app/graph/toolbar.tsx
+++ b/app/graph/toolbar.tsx
@@ -1,19 +1,20 @@
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { XCircle, ZoomIn, ZoomOut } from "lucide-react";
+import { cn } from "@/lib/utils"
-export function Toolbar(parmas: {
- chartRef: React.RefObject,
+export function Toolbar(params: {
+ chartRef: React.RefObject, className?: string
}) {
function handleZoomClick(changefactor: number) {
- let chart = parmas.chartRef.current
+ let chart = params.chartRef.current
if (chart) {
chart.zoom(chart.zoom() * changefactor)
}
}
function handleCenterClick() {
- let chart = parmas.chartRef.current
+ let chart = params.chartRef.current
if (chart) {
chart.fit()
chart.center()
@@ -21,10 +22,10 @@ export function Toolbar(parmas: {
}
return (
-
+
- handleZoomClick(1.1)}>
+ handleZoomClick(1.1)}>
diff --git a/tailwind.config.js b/tailwind.config.js
index 7cb7e37a..57c950f4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -7,6 +7,13 @@ module.exports = {
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
+ purge: {
+ safelist: [
+ {
+ pattern: /^bg-/,
+ },
+ ]
+ },
prefix: "",
theme: {
container: {