Skip to content

Commit

Permalink
add color buttons and expend node
Browse files Browse the repository at this point in the history
  • Loading branch information
gkorland committed Jan 16, 2024
1 parent e30d0fe commit d811ccb
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 30 deletions.
35 changes: 35 additions & 0 deletions app/api/graph/[graph]/[node]/route.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
}
36 changes: 36 additions & 0 deletions app/graph/labels.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={cn("flex flex-row", params.className)} >
<TooltipProvider>
{params.categories.map((category) => {
return (
<Tooltip key={category.index}>
<TooltipTrigger
className={`bg-${getCategoryColors(category.index)}-${category.show ? 500 : 200} rounded-lg border border-gray-300 p-2`}
onClick={() => {
params.onClick(category)
setReload(!reload)
}}
>
{ category.show ? <MinusCircle /> : <PlusCircle /> }
</TooltipTrigger>
<TooltipContent>
<p>{category.name}</p>
</TooltipContent>
</Tooltip>
)
})}
</TooltipProvider>
</div>
)
}
35 changes: 20 additions & 15 deletions app/graph/model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

export interface Category {
index: number,
name: string,
index: number
show: boolean,
}

export interface Node {
Expand All @@ -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<COLORS.length ? COLORS[index] : COLORS[0]
}

interface GraphResult {
data: any[],
metadata: any
Expand Down Expand Up @@ -79,10 +83,10 @@ export class Graph {
return new Graph("", [], [], new Map<String, Category>(), new Map<number, Node>(), new Map<number, Edge>())
}

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
}

Expand Down Expand Up @@ -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
Expand All @@ -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})
Expand Down
69 changes: 60 additions & 9 deletions app/graph/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
Expand Down Expand Up @@ -62,7 +63,7 @@ export default function Page() {
const chartRef = useRef<cytoscape.Core | null>(null)

// A reference to the query state to allow running the user query
const queryState = useRef<QueryState| null>(null)
const queryState = useRef<QueryState | null>(null)

function prepareArg(arg: string): string {
return encodeURIComponent(arg.trim())
Expand All @@ -71,7 +72,7 @@ export default function Page() {
async function runQuery(event: any) {
event.preventDefault();
let state = queryState.current;
if(!state){
if (!state) {
return
}

Expand All @@ -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 (
<div className="flex flex-col h-full">
<Query onSubmit={runQuery} query={(state) => queryState.current = state} />
<main className="h-full w-full">
<Toolbar chartRef={chartRef} />
<div className="grid grid-cols-6 gap-4">
<Toolbar className="col-start-1" chartRef={chartRef} />
<Labels className="col-start-3 col-end-4" categories={graph.Categories} onClick={onCategoryClick}/>
</div>
<CytoscapeComponent
cy={(cy) => {
chartRef.current = cy
Expand All @@ -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}
Expand Down
13 changes: 7 additions & 6 deletions app/graph/toolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,31 @@
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<cytoscape.Core>,
export function Toolbar(params: {
chartRef: React.RefObject<cytoscape.Core>, 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()
}
}

return (
<div className="flex flex-row" >
<div className={cn("flex flex-row", params.className)}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="text-gray-600 dark:text-gray-400 rounded-lg border border-gray-300 p-2" onClick={() => handleZoomClick(1.1)}>
<TooltipTrigger className="text-gray-600 rounded-lg border border-gray-300 p-2" onClick={() => handleZoomClick(1.1)}>
<ZoomIn />
</TooltipTrigger>
<TooltipContent>
Expand Down
7 changes: 7 additions & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ module.exports = {
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
purge: {
safelist: [
{
pattern: /^bg-/,
},
]
},
prefix: "",
theme: {
container: {
Expand Down

0 comments on commit d811ccb

Please sign in to comment.