diff --git a/app/(deploy)/[workspace]/databases/database-column.tsx b/app/(deploy)/[workspace]/databases/database-column.tsx new file mode 100644 index 0000000..dc51413 --- /dev/null +++ b/app/(deploy)/[workspace]/databases/database-column.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { Database } from '@prisma/client'; +import { ColumnDef } from '@tanstack/react-table'; +import Image from 'next/image'; +import { databaseProviders } from './database-new-form'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { EllipsisVertical } from 'lucide-react'; + +interface IDatabase extends Pick {} + +export const columns: ColumnDef[] = [ + { + accessorKey: 'provider', + header: 'Provider', + cell(props) { + return ( +
+ pro.id.toUpperCase() === props.getValue())[0]?.image?.src} alt={databaseProviders.filter((pro) => pro.id.toUpperCase() === props.getValue())[0]?.image?.alt} width={32} height={32} /> +
+ ); + }, + }, + { + accessorKey: 'name', + header: 'Name', + }, + { + accessorKey: 'createdAt', + header: 'Created', + cell(props) { + return {new Date(props.getValue() as string).toLocaleDateString('en-US', { hour: '2-digit', minute: '2-digit' })}; + }, + }, +]; diff --git a/app/(deploy)/[workspace]/databases/database-new-form.tsx b/app/(deploy)/[workspace]/databases/database-new-form.tsx index bd27c8b..685fa94 100644 --- a/app/(deploy)/[workspace]/databases/database-new-form.tsx +++ b/app/(deploy)/[workspace]/databases/database-new-form.tsx @@ -52,7 +52,7 @@ const steps: DatabaseNewSteps[] = [ }, ]; -const databaseProviders: IDatabaseProvider[] = [ +export const databaseProviders: IDatabaseProvider[] = [ { id: 'vitess', display: 'Vitess', @@ -69,6 +69,14 @@ const databaseProviders: IDatabaseProvider[] = [ src: '/redis.svg', }, }, + { + id: 'postgres', + display: 'Postgres', + image: { + alt: 'Postgres', + src: '/postgres.png', + }, + }, ]; export default function DatabaseNewForm() { @@ -122,6 +130,7 @@ export default function DatabaseNewForm() { { setDatabaseConfig((prev) => ({ ...prev, provider: databaseProviders.filter((provider) => provider.id == id)[0] })); }} diff --git a/app/(deploy)/[workspace]/databases/database-table.tsx b/app/(deploy)/[workspace]/databases/database-table.tsx new file mode 100644 index 0000000..60e50e7 --- /dev/null +++ b/app/(deploy)/[workspace]/databases/database-table.tsx @@ -0,0 +1,126 @@ +'use client'; + +import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; + +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { useWorkspace } from '@/hooks/useWorkspace'; +import { useRouter } from 'next/navigation'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Button } from '@/components/ui/button'; +import { EllipsisVertical, X } from 'lucide-react'; +import { Dialog, DialogClose, DialogContent, DialogTrigger } from '@/components/ui/dialog'; +import { useState } from 'react'; +import { Database } from '@prisma/client'; + +interface DataTableProps { + columns: ColumnDef[]; + data: (TData & Database)[]; +} + +export function DatabaseTable({ columns, data }: DataTableProps) { + const [open, setOpen] = useState(false); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + + const router = useRouter(); + const { slug } = useWorkspace(); + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + Actions + + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row, index) => ( + + {row.getVisibleCells().map((cell) => ( + { + router.push(`/${slug}/databases/${(row.original as any)?.id}`); + }} + className="cursor-pointer" + > + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + + + + + + + {/* Edit Database + */} + + + { + e.preventDefault(); + setOpen(true); + }} + > + Connection details + + + +
+
+

Connection details

+ + + +
+

Details to connect to your new database

+
+ +
+

Host: {data[index].host}

+

Port: {data[index].port}

+

Username: {data[index].username}

+

Password: {data[index].password}

+
+
+
+ Destroy +
+
+
+
+ )) + ) : ( + + + No results. + + + )} +
+
+
+ ); +} diff --git a/app/(deploy)/[workspace]/databases/page.tsx b/app/(deploy)/[workspace]/databases/page.tsx index 76bde48..f55bb9a 100644 --- a/app/(deploy)/[workspace]/databases/page.tsx +++ b/app/(deploy)/[workspace]/databases/page.tsx @@ -1,6 +1,8 @@ import { Database } from 'lucide-react'; import DatabaseNewForm from './database-new-form'; import prisma from '@/lib/prisma'; +import { DatabaseTable } from './database-table'; +import { columns } from './database-column'; export default async function Databases({ params }: { params: { workspace: string } }) { const workspaceSlug = params.workspace; @@ -23,10 +25,6 @@ export default async function Databases({ params }: { params: { workspace: strin

No database

Get started by creating a new database.

- {/* */} @@ -35,9 +33,12 @@ export default async function Databases({ params }: { params: { workspace: strin return (
-

Databases

-
- {workspace.Database.map((database) => ( +
+

Databases

+ +
+
+ {/* {workspace.Database.map((database) => (
@@ -45,8 +46,8 @@ export default async function Databases({ params }: { params: { workspace: strin

{database.provider}

- ))} - + ))} */} +
); diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 01ff19c..8292e20 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,122 +1,46 @@ -"use client" +'use client'; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils'; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +const DialogOverlay = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +const DialogContent = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, children, ...props }, ref) => ( + + + + {children} + {/* + + Close + */} + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) =>
; +DialogHeader.displayName = 'DialogHeader'; -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) =>
; +DialogFooter.displayName = 'DialogFooter'; -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +const DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ); +DialogTitle.displayName = DialogPrimitive.Title.displayName; -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +const DialogDescription = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ); +DialogDescription.displayName = DialogPrimitive.Description.displayName; -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} +export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/lib/deploy/database.ts b/lib/deploy/database.ts index f8fa980..52637c4 100644 --- a/lib/deploy/database.ts +++ b/lib/deploy/database.ts @@ -10,7 +10,7 @@ export async function deployDatabase(config: IDatabaseConfig) { const database = await prisma.database.create({ data: { name: config.name, - provider: DatabaseProvider.REDIS, + provider: config.provider.id.toUpperCase() as DatabaseProvider, password: config.user.password, username: config.user.username, workspaceId: config.workspaceId, diff --git a/package-lock.json b/package-lock.json index 08bfdc4..61be1ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,14 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@tanstack/react-table": "^8.16.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", @@ -856,6 +858,35 @@ } } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz", + "integrity": "sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-menu": "2.0.6", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", @@ -939,6 +970,46 @@ } } }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", + "integrity": "sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-focus-guards": "1.0.1", + "@radix-ui/react-focus-scope": "1.0.4", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.7.tgz", @@ -1394,6 +1465,37 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/react-table": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.16.0.tgz", + "integrity": "sha512-rKRjnt8ostqN2fercRVOIH/dq7MAmOENCMvVlKx6P9Iokhh6woBGnIZEkqsY/vEJf1jN3TqLOb34xQGLVRuhAg==", + "dependencies": { + "@tanstack/table-core": "8.16.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.16.0.tgz", + "integrity": "sha512-dCG8vQGk4js5v88/k83tTedWOwjGnIyONrKpHpfmSJB8jwFHl8GSu1sBBxbtACVAPtAQgwNxl0rw1d3RqRM1Tg==", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", diff --git a/package.json b/package.json index ea01fd5..283b607 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,14 @@ "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", + "@tanstack/react-table": "^8.16.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b942fff..768dd78 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -96,6 +96,7 @@ model Database { enum DatabaseProvider { VITESS REDIS + POSTGRES } model Application { diff --git a/public/postgres.png b/public/postgres.png new file mode 100644 index 0000000..761595b Binary files /dev/null and b/public/postgres.png differ