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 10 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
1 change: 1 addition & 0 deletions app/api/graph/[graph]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export async function DELETE(request: NextRequest, { params }: { params: { graph
const graph = client.selectGraph(graphId);

await graph.delete()
console.log(client.list());

return NextResponse.json({ message: `${graphId} graph deleted` })
}
Expand Down
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-around"
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 you?</AlertDialogTitle>
<AlertDialogDescription>
Are you absolutely sure you want to delete {deleteGraph}?
</AlertDialogDescription>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider rephrasing the delete confirmation message for clarity. It currently reads incomplete and might confuse users.

- <AlertDialogTitle>Are you absolutely sure you?</AlertDialogTitle>
+ <AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
- <AlertDialogDescription>Are you absolutely sure you want to delete {deleteGraph}?</AlertDialogDescription>
+ <AlertDialogDescription>Are you sure you want to delete {deleteGraph}?</AlertDialogDescription>

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
<AlertDialogTitle>Are you absolutely sure you?</AlertDialogTitle>
<AlertDialogDescription>
Are you absolutely sure you want to delete {deleteGraph}?
</AlertDialogDescription>
<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>
)
}
35 changes: 27 additions & 8 deletions app/graph/GraphList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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';
import { on } from 'events';

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 +34,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 +60,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} />
)
}
43 changes: 27 additions & 16 deletions app/graph/GraphView.tsx
Original file line number Diff line number Diff line change
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 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
4 changes: 2 additions & 2 deletions app/graph/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export default function Page() {
// Proposed abstraction for improved modularity
if (!validateGraphSelection(state.graphName)) return false;

const q = state.query.trim() || "MATCH (n) OPTIONAL MATCH (n)-[e]-(m) RETURN n,e,m limit 100";
const q = state.query?.trim() || "MATCH (n) OPTIONAL MATCH (n)-[e]-(m) RETURN n,e,m limit 100";

const result = await fetch(`/api/graph?graph=${prepareArg(state.graphName)}&query=${prepareArg(q)}`, {
method: 'GET',
Expand Down Expand Up @@ -97,7 +97,7 @@ export default function Page() {
className="border rounded-lg border-gray-300 p-2"
onSubmit={runQuery}
onQueryUpdate={(state) => { queryState.current = state }}
onDeleteGraph={() => setGraph(Graph.empty())}
onDelete={() => setGraph(Graph.empty())}
/>
<div className="h-1 grow border flex flex-col gap-2 border-gray-300 rounded-lg p-2">
{
Expand Down
Loading
Loading