diff --git a/gui/app/(dashboard)/actions.ts b/gui/app/(dashboard)/actions.ts index fab27412e9..bced0635b3 100644 --- a/gui/app/(dashboard)/actions.ts +++ b/gui/app/(dashboard)/actions.ts @@ -53,3 +53,82 @@ export const showDatabase = async (params: { database_name: string }) => { console.log('🚀 ~ error:', error); } }; + +//#region table + +export const listTable = async (database_name: string) => { + try { + const x = await get( + `${ApiUrl.databases}/${database_name}/${ApiUrl.tables}` + ); + return x; + } catch (error) { + console.log('🚀 ~ error:', error); + } +}; + +export const createTable = async ({ + database_name, + table_name, + create_option, + fields +}: { + database_name: string; + table_name: string; + fields: Array<{ name: string; type: string; default?: string }>; + create_option: CreateOption; +}) => { + try { + const x = await post( + `${ApiUrl.databases}/${database_name}/${ApiUrl.tables}/${table_name}`, + { + create_option, + fields + } + ); + return x; + } catch (error) { + console.log('🚀 ~ error:', error); + } +}; + +export const dropTable = async ({ + database_name, + table_name, + drop_option +}: { + database_name: string; + table_name: string; + drop_option: DropOption; +}) => { + try { + const x = await drop( + `${ApiUrl.databases}/${database_name}/${ApiUrl.tables}/${table_name}`, + { + drop_option + } + ); + return x; + } catch (error) { + console.log('🚀 ~ error:', error); + } +}; + +export const showTable = async ({ + database_name, + table_name +}: { + database_name: string; + table_name: string; +}) => { + try { + const x = await get( + `${ApiUrl.databases}/${database_name}/${ApiUrl.tables}/${table_name}` + ); + return x; + } catch (error) { + console.log('🚀 ~ error:', error); + } +}; + +//#endregion diff --git a/gui/app/(dashboard)/database/context-menu.tsx b/gui/app/(dashboard)/database/context-menu.tsx new file mode 100644 index 0000000000..95c5b40c02 --- /dev/null +++ b/gui/app/(dashboard)/database/context-menu.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { + ContextMenuContent, + ContextMenuItem +} from '@/components/ui/context-menu'; +import { useSeDialogState } from '@/lib/hooks'; +import { TableCreatingDialog } from './table-creating-dialog'; +import AddIcon from '/public/add.svg'; + +export function InfinityContextMenuContent({ + databaseName +}: { + databaseName: string; +}) { + const { showDialog, visible, hideDialog, switchVisible } = useSeDialogState(); + return ( + <> + + +
+ Add +
+
+
+ + + ); +} diff --git a/gui/app/(dashboard)/database/page.tsx b/gui/app/(dashboard)/database/page.tsx index 692719075d..d0048320c1 100644 --- a/gui/app/(dashboard)/database/page.tsx +++ b/gui/app/(dashboard)/database/page.tsx @@ -1,4 +1,84 @@ -export default async function ProductsPage({ +import SideMenu, { MenuItem } from '@/components/ui/side-menu'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from '@/components/ui/table'; +import { listDatabase, listTable } from '../actions'; +import { InfinityContextMenuContent } from './context-menu'; + +const invoices = [ + { + invoice: 'INV001', + paymentStatus: 'Paid', + totalAmount: '$250.00', + paymentMethod: 'Credit Card' + }, + { + invoice: 'INV002', + paymentStatus: 'Pending', + totalAmount: '$150.00', + paymentMethod: 'PayPal' + }, + { + invoice: 'INV003', + paymentStatus: 'Unpaid', + totalAmount: '$350.00', + paymentMethod: 'Bank Transfer' + }, + { + invoice: 'INV004', + paymentStatus: 'Paid', + totalAmount: '$450.00', + paymentMethod: 'Credit Card' + }, + { + invoice: 'INV005', + paymentStatus: 'Paid', + totalAmount: '$550.00', + paymentMethod: 'PayPal' + }, + { + invoice: 'INV006', + paymentStatus: 'Pending', + totalAmount: '$200.00', + paymentMethod: 'Bank Transfer' + }, + { + invoice: 'INV007', + paymentStatus: 'Unpaid', + totalAmount: '$300.00', + paymentMethod: 'Credit Card' + } +]; + +function InfinityTable() { + return ( + + + + Name + Type + Default + + + + {invoices.map((invoice) => ( + + {invoice.invoice} + {invoice.paymentStatus} + {invoice.paymentMethod} + + ))} + +
+ ); +} + +export default async function DatabasePage({ searchParams }: { searchParams: { q: string; offset: string }; @@ -6,5 +86,50 @@ export default async function ProductsPage({ const search = searchParams.q ?? ''; const offset = searchParams.offset ?? 0; - return
database
; + const items: MenuItem[] = [ + { + key: 'sub1', + label: 'Navigation', + children: [ + { + key: 'g1', + label: 'Item 1' + }, + { + key: 'g2', + label: 'Item 2' + } + ] + } + ]; + + const ret = await listDatabase(); + if (ret.databases.length > 1) { + const latestDatabase = ret.databases.at(-1); + const tables = await listTable(latestDatabase); + console.log('🚀 ~ ret:', tables); + items.push({ + key: latestDatabase, + label: latestDatabase, + children: tables.tables + }); + } + + return ( +
+
+ ( + + )} + > +
+
+ +
+
+ ); } diff --git a/gui/app/(dashboard)/database/table-creating-dialog.tsx b/gui/app/(dashboard)/database/table-creating-dialog.tsx new file mode 100644 index 0000000000..f99be95179 --- /dev/null +++ b/gui/app/(dashboard)/database/table-creating-dialog.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@/components/ui/dialog'; +import { IDialogProps } from '@/lib/interfaces'; +import React from 'react'; +import { TableCreatingForm } from './table-creating-form'; + +export function TableCreatingDialog({ + children, + visible, + switchVisible, + hideDialog +}: React.PropsWithChildren>) { + return ( + + {children} + + + Create Table + +
+ +
+ + + +
+
+ ); +} diff --git a/gui/app/(dashboard)/database/table-creating-form.tsx b/gui/app/(dashboard)/database/table-creating-form.tsx new file mode 100644 index 0000000000..d66302bfb8 --- /dev/null +++ b/gui/app/(dashboard)/database/table-creating-form.tsx @@ -0,0 +1,110 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useRouter } from 'next/navigation'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { toast } from '@/components/hooks/use-toast'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select'; +import { CreateOption } from '@/lib/constant/common'; +import { createDatabase } from '../actions'; + +export const FormSchema = z.object({ + name: z + .string({ + required_error: 'Please input name' + }) + .trim(), + fields: z.array(), + create_option: z.nativeEnum(CreateOption) +}); + +interface IProps { + hide(): void; +} + +export function TableCreatingForm({ hide }: IProps) { + const router = useRouter(); + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + create_option: CreateOption.Error + } + }); + + async function onSubmit(data: z.infer) { + const ret = await createDatabase(data); + console.log('🚀 ~ onSubmit ~ ret:', ret); + if (ret.error_code === 0) { + router.refresh(); + hide(); + toast({ + title: 'Create Success', + description: '' + }); + } + } + + return ( +
+ + ( + + Name + + + + + + )} + /> + ( + + Create option + + + + )} + /> + + + ); +} diff --git a/gui/app/(dashboard)/page.tsx b/gui/app/(dashboard)/page.tsx index 67dc9e9bda..b96d335fae 100644 --- a/gui/app/(dashboard)/page.tsx +++ b/gui/app/(dashboard)/page.tsx @@ -40,7 +40,6 @@ export default async function HomePage({ searchParams: { q: string; offset: string }; }) { const ret = await listDatabase(); - console.log('🚀 ~ x:', ret); const search = searchParams.q ?? ''; const offset = searchParams.offset ?? 0; diff --git a/gui/app/(dashboard)/products/product.tsx b/gui/app/(dashboard)/products/product.tsx index 9b49b6a9c0..a4981c4782 100644 --- a/gui/app/(dashboard)/products/product.tsx +++ b/gui/app/(dashboard)/products/product.tsx @@ -10,7 +10,6 @@ import { import { TableCell, TableRow } from '@/components/ui/table'; import { MoreHorizontal } from 'lucide-react'; // import { SelectProduct } from '@/lib/db'; -import { deleteProduct } from '../actions'; export function Product({ product }: { product: any }) { return ( @@ -38,7 +37,7 @@ export function Product({ product }: { product: any }) { Actions Edit -
+
diff --git a/gui/components/ui/collapsible.tsx b/gui/components/ui/collapsible.tsx new file mode 100644 index 0000000000..9fa48946af --- /dev/null +++ b/gui/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/gui/components/ui/context-menu.tsx b/gui/components/ui/context-menu.tsx new file mode 100644 index 0000000000..93ef37ba98 --- /dev/null +++ b/gui/components/ui/context-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/gui/components/ui/side-menu.tsx b/gui/components/ui/side-menu.tsx new file mode 100644 index 0000000000..395cf35d55 --- /dev/null +++ b/gui/components/ui/side-menu.tsx @@ -0,0 +1,143 @@ +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible'; +import Link from 'next/link'; +import { ReactNode } from 'react'; +import { ContextMenu, ContextMenuTrigger } from './context-menu'; + +function ChevronRightIcon(props: any) { + return ( + + + + ); +} + +export interface SubItem { + key: string; + label: string; +} + +export interface MenuItem { + key: string; + label: string; + icon?: ReactNode; + children: SubItem[]; +} + +interface IProps { + items: MenuItem[]; + contextMenuContent?: ((key: string) => ReactNode) | ReactNode; +} + +export default function SideMenu({ items, contextMenuContent }: IProps) { + return ( +
+ + + Home + + + +
+ +
+ Dashboard +
+ + +
+ Profile +
+ +
+
+
+ + + Products + + + +
+ +
+ View Products +
+ + +
+ Add Product +
+ +
+
+
+ + {items.map((x) => { + return ( + + + + + {x.label} + + + {typeof contextMenuContent === 'function' + ? contextMenuContent(x.key) + : contextMenuContent} + + + +
+ {x.children.map((y) => { + return ( + +
+ {y.label} +
+ + ); + })} +
+
+
+ ); + })} +
+ ); +} diff --git a/gui/lib/constant/api.ts b/gui/lib/constant/api.ts index e25b17212f..8da4155377 100644 --- a/gui/lib/constant/api.ts +++ b/gui/lib/constant/api.ts @@ -1,4 +1,5 @@ export const ApiUrl = { databases: 'databases', - database: 'database' + database: 'database', + tables: 'tables' }; diff --git a/gui/lib/hooks.ts b/gui/lib/hooks.ts new file mode 100644 index 0000000000..8dc1723355 --- /dev/null +++ b/gui/lib/hooks.ts @@ -0,0 +1,18 @@ +import { useCallback, useState } from 'react'; + +export const useSeDialogState = () => { + const [visible, setVisible] = useState(false); + + const showDialog = useCallback(() => { + setVisible(true); + }, []); + const hideDialog = useCallback(() => { + setVisible(false); + }, []); + + const switchVisible = useCallback(() => { + setVisible(!visible); + }, [visible]); + + return { visible, showDialog, hideDialog, switchVisible }; +}; diff --git a/gui/lib/interfaces.ts b/gui/lib/interfaces.ts new file mode 100644 index 0000000000..3f649723f6 --- /dev/null +++ b/gui/lib/interfaces.ts @@ -0,0 +1,8 @@ +export interface IDialogProps { + showDialog?(): void; + hideDialog(): void; + switchVisible(): void; + visible?: boolean; + loading?: boolean; + onOk?(payload?: T): Promise | void; +} diff --git a/gui/lib/request.ts b/gui/lib/request.ts index fc4cfdbca1..351a06eafc 100644 --- a/gui/lib/request.ts +++ b/gui/lib/request.ts @@ -32,7 +32,7 @@ export const request = async ( export const get = (url: string, params?: Record) => request(url, params, 'GET'); -export const post = (url: string, params: Record) => +export const post = (url: string, params: Record) => request(url, params, 'POST'); export const drop = (url: string, params: Record) => diff --git a/gui/package.json b/gui/package.json index 99d7320eb0..71c004e06d 100644 --- a/gui/package.json +++ b/gui/package.json @@ -9,6 +9,8 @@ "dependencies": { "@hookform/resolvers": "^3.9.0", "@neondatabase/serverless": "^0.9.4", + "@radix-ui/react-collapsible": "^1.1.0", + "@radix-ui/react-context-menu": "^2.2.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", diff --git a/gui/pnpm-lock.yaml b/gui/pnpm-lock.yaml index 4cd07eceb9..9394675a0b 100644 --- a/gui/pnpm-lock.yaml +++ b/gui/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@neondatabase/serverless': specifier: ^0.9.4 version: 0.9.4 + '@radix-ui/react-collapsible': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) + '@radix-ui/react-context-menu': + specifier: ^2.2.1 + version: 2.2.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) '@radix-ui/react-dialog': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) @@ -1328,6 +1334,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.0': + resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1350,6 +1369,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-context-menu@2.2.1': + resolution: {integrity: sha512-wvMKKIeb3eOrkJ96s722vcidZ+2ZNfcYZWBPRHIB1VWrF+fiF851Io6LX0kmK5wTDQFKdulCCKJk2c3SBaQHvA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-context@1.1.0': resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} peerDependencies: @@ -4180,6 +4212,22 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-collapsible@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + react: 19.0.0-rc.0 + react-dom: 19.0.0-rc.0(react@19.0.0-rc.0) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) @@ -4198,6 +4246,20 @@ snapshots: optionalDependencies: '@types/react': 18.3.3 + '@radix-ui/react-context-menu@2.2.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + '@radix-ui/react-menu': 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@19.0.0-rc.0(react@19.0.0-rc.0))(react@19.0.0-rc.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0) + react: 19.0.0-rc.0 + react-dom: 19.0.0-rc.0(react@19.0.0-rc.0) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@19.0.0-rc.0)': dependencies: react: 19.0.0-rc.0