Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #176 re-style #177

Merged
merged 20 commits into from
Apr 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 112 additions & 65 deletions app/components/combobox.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"use client"

import { useState, Dispatch, createRef } from "react"
import { Check, ChevronsUpDown } from "lucide-react"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"

import { Check, ChevronsUpDown, Trash2 } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger
} from "@/components/ui/dialog"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Expand All @@ -20,20 +26,32 @@ import {
} from "@/components/ui/popover"
import { Separator } from "@/components/ui/separator"
import { Input } from "@/components/ui/input"

import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
AlertDialogFooter
} from "@/components/ui/alert-dialog"

/* eslint-disable react/require-default-props */
interface ComboboxProps {
className?: string,
type?: string,
options: string[],
addOption?: Dispatch<string>|null,
addOption?: Dispatch<string> | null,
deleteOption?: (graphName :string) => void,
selectedValue: string,
setSelectedValue: Dispatch<string>
}

export default function Combobox({ className='', type='', options, addOption=null, selectedValue, setSelectedValue }: ComboboxProps) {
export default function Combobox({ className = '', type = '', options, addOption = null, deleteOption, selectedValue, setSelectedValue }: ComboboxProps) {
const [open, setOpen] = useState(false)
const [deleteGraph, setDeleteGraph] = useState<string>("")
const inputRef = createRef<HTMLInputElement>()

// read the text in the create input box and add it to the list of options
Expand All @@ -47,6 +65,13 @@ export default function Combobox({ className='', type='', options, addOption=nul
}
}

const onDeleteOption = (graphName: string) => {
setOpen(false)
if (deleteOption) {
deleteOption(graphName)
}
}

const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Enter") {
onAddOption();
Expand All @@ -55,65 +80,87 @@ export default function Combobox({ className='', type='', options, addOption=nul

const entityType = type ?? ""
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={`w-[200px] justify-between ${className} `}
>
{selectedValue
? options.find((option) => option === selectedValue)
: `Select ${entityType}...`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
key={option}
onSelect={(currentValue) => {
if (currentValue !== selectedValue) {
setSelectedValue(option)
<AlertDialog>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={`w-[200px] justify-between ${className} `}
>
{selectedValue
? options.find((option) => option === selectedValue)
: `Select ${entityType}...`}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{options.map((option) => (
<CommandItem
className="w-full flex flex-row justify-between px-6"
key={option}
onSelect={(currentValue) => {
if (currentValue !== selectedValue) {
setSelectedValue(option)
}
setOpen(false)
}}
>
<div className="flex flex-row">
<Check
className={cn(
"mr-2 h-4 w-4",
selectedValue === option ? "opacity-100" : "opacity-0"
)}
/>
{option}
</div>
{
deleteOption &&
<AlertDialogTrigger onClick={() => setDeleteGraph(option)}>
<Trash2 />
</AlertDialogTrigger>
}
setOpen(false)
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedValue === option ? "opacity-100" : "opacity-0"
)}
/>
{option}
</CommandItem>
))}
<Separator orientation="horizontal" />

{addOption &&
<Dialog>
<DialogTrigger>
<CommandItem>Create new {entityType}...</CommandItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create a new {entityType}?</DialogTitle>
<DialogDescription>
<Input type="text" ref={inputRef} id="create" name="create" onKeyDown={handleKeyDown} placeholder={`${entityType} name ...`} />
</DialogDescription>
</DialogHeader>
<Button className="p-4" type="submit" onClick={onAddOption}>Create</Button>
</DialogContent>
</Dialog>
}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
</CommandItem>
))}
<Separator orientation="horizontal" />
{addOption &&
<Dialog>
<DialogTrigger>
<CommandItem>Create new {entityType}...</CommandItem>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create a new {entityType}?</DialogTitle>
<DialogDescription>
<Input type="text" ref={inputRef} id="create" name="create" onKeyDown={handleKeyDown} placeholder={`${entityType} name ...`} />
</DialogDescription>
</DialogHeader>
<Button className="p-4" type="submit" onClick={onAddOption}>Create</Button>
</DialogContent>
</Dialog>
}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {deleteGraph}?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={() => onDeleteOption(deleteGraph)}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
9 changes: 5 additions & 4 deletions app/graph/DataPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";

interface Props {
Expand All @@ -17,9 +17,10 @@ const excludedProperties = new Set([
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function DataPanel({ object }: Props) {
const rowClass = "dark:hover:bg-slate-700 hover:bg-gray-400 border-y-[1px] border-y-gray-700"

const type = object.source ? "edge" : "node"
return (
<Table>
<TableCaption>{type} properties</TableCaption>
<TableHeader>
<TableRow className={rowClass}>
<TableHead>Field</TableHead>
Expand All @@ -38,10 +39,10 @@ export default function DataPanel({ object }: Props) {
const text = cellIndex === 1 ? JSON.parse(strCell) : strCell
return (
// eslint-disable-next-line react/no-array-index-key
<TableCell key={cellIndex}>
<TableCell className="max-w-10" key={cellIndex}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="max-w-96 truncate">
<TooltipTrigger className="w-full truncate" >
{text}
</TooltipTrigger>
<TooltipContent>
Expand Down
34 changes: 26 additions & 8 deletions app/graph/GraphList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useState, useEffect, Dispatch, SetStateAction } from 'react';
import { useToast } from "@/components/ui/use-toast"
import { useToast } from "@/components/ui/use-toast"
import Combobox from '../components/combobox';

interface Props {
onSelectedGraph: Dispatch<SetStateAction<string>>,
onDelete: boolean,
onDelete: () => void,
}
// A component that renders an input box for Cypher queries
export default function GraphsList({onSelectedGraph, onDelete}: Props) {
export default function GraphsList({ onSelectedGraph, onDelete }: Props) {

const [graphs, setGraphs] = useState<string[]>([]);
const [selectedGraph, setSelectedGraph] = useState("");
Expand All @@ -33,10 +33,21 @@ export default function GraphsList({onSelectedGraph, onDelete}: Props) {
})
}, [toast])

useEffect(() => {
setGraphs((prevGraphs: string[]) => [...prevGraphs.filter((graph) => graph !== selectedGraph)])
setSelectedGraph('')
}, [onDelete])
const handelDelete = (graphName: string) => {
fetch(`/api/graph/${encodeURIComponent(graphName)}`, {
method: 'DELETE',
}).then(() =>
toast({
title: 'Graph Deleted',
description: `Graph ${graphName} deleted`,
})
).catch((error) => {
toast({
title: "Error",
description: error.message,
})
})
}

const setSelectedValue = (graph: string) => {
setSelectedGraph(graph)
Expand All @@ -48,7 +59,14 @@ export default function GraphsList({onSelectedGraph, onDelete}: Props) {
setSelectedValue(newGraph)
}

const deleteOption = (graphName: string) => {
setGraphs((prevGraphs: string[]) => [...prevGraphs.filter(graph => graph !== graphName)]);
setSelectedValue("")
handelDelete(graphName)
onDelete()
}

return (
<Combobox type="Graph" options={graphs} addOption={addOption} selectedValue={selectedGraph} setSelectedValue={setSelectedValue} />
<Combobox type="Graph" options={graphs} addOption={addOption} deleteOption={deleteOption} selectedValue={selectedGraph} setSelectedValue={setSelectedValue} />
)
}
45 changes: 28 additions & 17 deletions app/graph/GraphView.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import CytoscapeComponent from "react-cytoscapejs";
import { toast } from "@/components/ui/use-toast";
import cytoscape, { ElementDefinition, EventObject, NodeDataDefinition } from "cytoscape";
import { useRef, useState, useImperativeHandle, forwardRef, useEffect } from "react";
import { useRef, useState, useImperativeHandle, forwardRef } from "react";
import { signOut } from "next-auth/react";
import fcose from 'cytoscape-fcose';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
Expand Down Expand Up @@ -98,18 +98,12 @@ const GraphView = forwardRef(({ graph, darkmode }: GraphViewProps, ref) => {

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [selectedObject, setSelectedObject] = useState<any | null>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [isCollapsed, setIsCollapsed] = useState<boolean>(true);

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

useEffect(() => {
if (isOpen) {
dataPanel.current?.expand()
} else dataPanel.current?.collapse()
}, [isOpen])

useImperativeHandle(ref, () => ({
expand: (elements: ElementDefinition[]) => {
const chart = chartRef.current
Expand All @@ -121,6 +115,16 @@ const GraphView = forwardRef(({ graph, darkmode }: GraphViewProps, ref) => {
}
}))

const onExpand = () => {
if (dataPanel.current) {
if (dataPanel.current.isCollapsed()) {
dataPanel.current.expand()
} else {
dataPanel.current.collapse()
}
}
}

// 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}`, {
Expand Down Expand Up @@ -177,7 +181,7 @@ const GraphView = forwardRef(({ graph, darkmode }: GraphViewProps, ref) => {
const handleTap = (evt: EventObject) => {
const object = evt.target.json().data;
setSelectedObject(object);
setIsOpen(true);
dataPanel.current?.expand()
}

return (
Expand Down Expand Up @@ -212,16 +216,23 @@ const GraphView = forwardRef(({ graph, darkmode }: GraphViewProps, ref) => {
<ResizableHandle />
{
selectedObject &&
<button type="button" onClick={() => setIsOpen(prev => !prev)} className="fixed right-5 top-[50%]">
{isOpen ? <ChevronRight /> : <ChevronLeft />}
<button title={isCollapsed ? "open" : "close"} type="button" onClick={() => onExpand()} className="fixed right-5 top-[50%]">
{!isCollapsed ? <ChevronRight /> : <ChevronLeft />}
</button>
}
{
isOpen &&
<ResizablePanel id="panel" ref={dataPanel} maxSize={50} minSize={20} collapsible defaultSize={selectedObject ? 20 : 0} className="bg-gray-100 dark:bg-gray-800">
{selectedObject && <DataPanel object={selectedObject} />}
</ResizablePanel>
}
<ResizablePanel
id="panel"
ref={dataPanel}
maxSize={50}
minSize={20}
onCollapse={() => { setIsCollapsed(true) }}
onExpand={() => { setIsCollapsed(false) }}
collapsible
defaultSize={selectedObject ? 20 : 0}
className="bg-gray-100 dark:bg-gray-800"
>
{selectedObject && <DataPanel object={selectedObject} />}
</ResizablePanel>
</ResizablePanelGroup>
)
});
Expand Down
Loading
Loading