diff --git a/web/bun.lockb b/web/bun.lockb index db03039..44a53d6 100755 Binary files a/web/bun.lockb and b/web/bun.lockb differ diff --git a/web/drizzle/0009_easy_banshee.sql b/web/drizzle/0009_easy_banshee.sql new file mode 100644 index 0000000..67d2eef --- /dev/null +++ b/web/drizzle/0009_easy_banshee.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS "comfyui_deploy"."api_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "key" text NOT NULL, + "name" text NOT NULL, + "user_id" text NOT NULL, + "org_id" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "api_keys_key_unique" UNIQUE("key") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "comfyui_deploy"."api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/web/drizzle/meta/0009_snapshot.json b/web/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..fc4084a --- /dev/null +++ b/web/drizzle/meta/0009_snapshot.json @@ -0,0 +1,614 @@ +{ + "id": "a5d542ea-484f-431a-b31f-170e789a03a7", + "prevId": "bba0fc3e-5729-44b8-bf45-d3f194cc79c0", + "version": "5", + "dialect": "pg", + "tables": { + "api_keys": { + "name": "api_keys", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "api_keys_user_id_users_id_fk": { + "name": "api_keys_user_id_users_id_fk", + "tableFrom": "api_keys", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_key_unique": { + "name": "api_keys_key_unique", + "nullsNotDistinct": false, + "columns": [ + "key" + ] + } + } + }, + "deployments": { + "name": "deployments", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_version_id": { + "name": "workflow_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment": { + "name": "environment", + "type": "deployment_environment", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "deployments_user_id_users_id_fk": { + "name": "deployments_user_id_users_id_fk", + "tableFrom": "deployments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "deployments_workflow_version_id_workflow_versions_id_fk": { + "name": "deployments_workflow_version_id_workflow_versions_id_fk", + "tableFrom": "deployments", + "tableTo": "workflow_versions", + "columnsFrom": [ + "workflow_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_workflow_id_workflows_id_fk": { + "name": "deployments_workflow_id_workflows_id_fk", + "tableFrom": "deployments", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "deployments_machine_id_machines_id_fk": { + "name": "deployments_machine_id_machines_id_fk", + "tableFrom": "deployments", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "machines": { + "name": "machines", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "machines_user_id_users_id_fk": { + "name": "machines_user_id_users_id_fk", + "tableFrom": "machines", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "users": { + "name": "users", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_run_outputs": { + "name": "workflow_run_outputs", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_run_outputs_run_id_workflow_runs_id_fk": { + "name": "workflow_run_outputs_run_id_workflow_runs_id_fk", + "tableFrom": "workflow_run_outputs", + "tableTo": "workflow_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_runs": { + "name": "workflow_runs", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow_version_id": { + "name": "workflow_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "workflow_inputs": { + "name": "workflow_inputs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "workflow_run_status", + "primaryKey": false, + "notNull": true, + "default": "'not-started'" + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_runs_workflow_version_id_workflow_versions_id_fk": { + "name": "workflow_runs_workflow_version_id_workflow_versions_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflow_versions", + "columnsFrom": [ + "workflow_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workflow_runs_workflow_id_workflows_id_fk": { + "name": "workflow_runs_workflow_id_workflows_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_runs_machine_id_machines_id_fk": { + "name": "workflow_runs_machine_id_machines_id_fk", + "tableFrom": "workflow_runs", + "tableTo": "machines", + "columnsFrom": [ + "machine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflows": { + "name": "workflows", + "schema": "comfyui_deploy", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflows_user_id_users_id_fk": { + "name": "workflows_user_id_users_id_fk", + "tableFrom": "workflows", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "workflow_versions": { + "name": "workflow_versions", + "schema": "comfyui_deploy", + "columns": { + "workflow_id": { + "name": "workflow_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workflow": { + "name": "workflow", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "workflow_api": { + "name": "workflow_api", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workflow_versions_workflow_id_workflows_id_fk": { + "name": "workflow_versions_workflow_id_workflows_id_fk", + "tableFrom": "workflow_versions", + "tableTo": "workflows", + "columnsFrom": [ + "workflow_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "deployment_environment": { + "name": "deployment_environment", + "values": { + "staging": "staging", + "production": "production" + } + }, + "workflow_run_status": { + "name": "workflow_run_status", + "values": { + "not-started": "not-started", + "running": "running", + "success": "success", + "failed": "failed" + } + } + }, + "schemas": { + "comfyui_deploy": "comfyui_deploy" + }, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/_journal.json b/web/drizzle/meta/_journal.json index 862d636..97faadb 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -64,6 +64,13 @@ "when": 1702615888467, "tag": "0008_futuristic_adam_destine", "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1702632471725, + "tag": "0009_easy_banshee", + "breakpoints": true } ] } \ No newline at end of file diff --git a/web/package.json b/web/package.json index 7823595..9f6f6d3 100644 --- a/web/package.json +++ b/web/package.json @@ -34,6 +34,7 @@ "dayjs": "^1.11.10", "drizzle-orm": "^0.29.1", "lucide-react": "^0.294.0", + "nanoid": "^5.0.4", "next": "14.0.3", "next-usequerystate": "^1.13.2", "react": "^18", diff --git a/web/src/app/api-keys/page.tsx b/web/src/app/api-keys/page.tsx new file mode 100644 index 0000000..13a43a0 --- /dev/null +++ b/web/src/app/api-keys/page.tsx @@ -0,0 +1,32 @@ +import { APIKeyList } from "@/components/APIKeyList"; +import { getAPIKeys } from "@/server/curdApiKeys"; +import { auth } from "@clerk/nextjs"; + +export default function Page() { + return ; +} + +async function Component() { + const { userId } = await auth(); + + if (!userId) { + return
No auth
; + } + + const workflow = await getAPIKeys(); + + return ( +
+ { + return { + id: x.id, + name: x.name, + date: x.updated_at, + endpoint: `****${x.key.slice(-4)}`, + }; + })} + /> +
+ ); +} diff --git a/web/src/app/machines/page.tsx b/web/src/app/machines/page.tsx index 6cc64a3..4d0bf7f 100644 --- a/web/src/app/machines/page.tsx +++ b/web/src/app/machines/page.tsx @@ -1,7 +1,7 @@ import { MachineList } from "@/components/MachineList"; import { db } from "@/db/db"; -import { machinesTable, usersTable } from "@/db/schema"; -import { auth, clerkClient } from "@clerk/nextjs"; +import { machinesTable } from "@/db/schema"; +import { auth } from "@clerk/nextjs"; import { desc, eq } from "drizzle-orm"; export default function Page() { @@ -36,26 +36,3 @@ async function MachineListServer() { ); } - -async function setInitialUserData(userId: string) { - const user = await clerkClient.users.getUser(userId); - - // incase we dont have username such as google login, fallback to first name + last name - const usernameFallback = - user.username ?? (user.firstName ?? "") + (user.lastName ?? ""); - - // For the display name, if it for some reason is empty, fallback to username - let nameFallback = (user.firstName ?? "") + (user.lastName ?? ""); - if (nameFallback === "") { - nameFallback = usernameFallback; - } - - const result = await db.insert(usersTable).values({ - id: userId, - // this is used for path, make sure this is unique - username: usernameFallback, - - // this is for display name, maybe different from username - name: nameFallback, - }); -} diff --git a/web/src/components/APIKeyList.tsx b/web/src/components/APIKeyList.tsx new file mode 100644 index 0000000..7cf7ccc --- /dev/null +++ b/web/src/components/APIKeyList.tsx @@ -0,0 +1,417 @@ +"use client"; + +import { getRelativeTime } from "../lib/getRelativeTime"; +import { LoadingIcon } from "./LoadingIcon"; +import { callServerPromise } from "./callServerPromise"; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + Form, +} from "./ui/form"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { addNewAPIKey, deleteAPIKey } from "@/server/curdApiKeys"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, +} from "@tanstack/react-table"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ArrowUpDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +export type APIKey = { + id: string; + name: string; + endpoint: string; + date: Date; +}; + +export const columns: ColumnDef[] = [ + { + accessorKey: "id", + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + // + row.getValue("name") + // + ); + }, + }, + { + accessorKey: "endpoint", + header: () =>
Endpoint
, + cell: ({ row }) => { + return ( +
{row.original.endpoint}
+ ); + }, + }, + { + accessorKey: "date", + sortingFn: "datetime", + enableSorting: true, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
+ {getRelativeTime(row.original.date)} +
+ ), + }, + + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const workflow = row.original; + + return ( + + + + + + Actions + { + callServerPromise(deleteAPIKey(workflow.id)); + }} + > + Delete API Key + + {/* + View customer + View payment details */} + + + ); + }, + }, +]; + +export function APIKeyList({ data }: { data: APIKey[] }) { + const [sorting, setSorting] = React.useState([]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + + const table = useReactTable({ + data, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }); + + return ( +
+
+ + table.getColumn("name")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> +
+ + {/* + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + */} +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+
+ ); +} + +const formSchema = z.object({ + name: z.string().min(1), +}); + +function AddMachinesDialog() { + const [open, setOpen] = React.useState(false); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "My API Key", + }, + }); + + const [apiKey, setAPIKey] = React.useState + > | null>(); + + return ( + { + setOpen(open); + if (!open) setAPIKey(null); + }} + > + + + + +
+ { + const apiKey = await callServerPromise(addNewAPIKey(data.name)); + if (apiKey) setAPIKey(apiKey); + // setOpen(false); + })} + > + + Create API Key + + Create API Key for workflow upload + + +
+ {/*
*/} + ( + + Name + + + + + + )} + /> + {apiKey && ( + + API Key (Copy the API key now) + + + + {/* */} + + )} +
+ + {apiKey ? ( + + ) : ( + + )} + + + + +
+ ); +} diff --git a/web/src/components/MachineList.tsx b/web/src/components/MachineList.tsx index 3783977..eedaf29 100644 --- a/web/src/components/MachineList.tsx +++ b/web/src/components/MachineList.tsx @@ -2,6 +2,7 @@ import { getRelativeTime } from "../lib/getRelativeTime"; import { LoadingIcon } from "./LoadingIcon"; +import { callServerPromise } from "./callServerPromise"; import { FormControl, FormField, @@ -56,7 +57,6 @@ import { import { ArrowUpDown, MoreHorizontal } from "lucide-react"; import * as React from "react"; import { useForm } from "react-hook-form"; -import { toast } from "sonner"; import { z } from "zod"; export type Machine = { @@ -176,20 +176,6 @@ export const columns: ColumnDef[] = [ }, ]; -export async function callServerPromise(result: Promise) { - return result - .then((x) => { - if ((x as { message: string })?.message !== undefined) { - toast.success((x as { message: string }).message); - } - return x; - }) - .catch((error) => { - toast.error(error.message); - return null; - }); -} - export function MachineList({ data }: { data: Machine[] }) { const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState( diff --git a/web/src/components/NavbarRight.tsx b/web/src/components/NavbarRight.tsx index 14364ca..f64a853 100644 --- a/web/src/components/NavbarRight.tsx +++ b/web/src/components/NavbarRight.tsx @@ -10,19 +10,28 @@ export function NavbarRight() { return ( { if (value === "machines") { router.push("/machines"); + } else if (value === "api-keys") { + router.push("/api-keys"); } else { router.push("/"); } }} > - + Workflow Machines + API Keys ); diff --git a/web/src/components/RunDisplay.tsx b/web/src/components/RunDisplay.tsx index 3273035..d9b035d 100644 --- a/web/src/components/RunDisplay.tsx +++ b/web/src/components/RunDisplay.tsx @@ -1,7 +1,7 @@ "use client"; import { LiveStatus } from "./LiveStatus"; -import { callServerPromise } from "@/components/MachineList"; +import { callServerPromise } from "./callServerPromise"; import { Dialog, DialogContent, diff --git a/web/src/components/VersionSelect.tsx b/web/src/components/VersionSelect.tsx index 9e45c0e..bca9b08 100644 --- a/web/src/components/VersionSelect.tsx +++ b/web/src/components/VersionSelect.tsx @@ -1,7 +1,7 @@ "use client"; +import { callServerPromise } from "./callServerPromise"; import { LoadingIcon } from "@/components/LoadingIcon"; -import { callServerPromise } from "@/components/MachineList"; import { Button } from "@/components/ui/button"; import { DropdownMenu, diff --git a/web/src/components/callServerPromise.tsx b/web/src/components/callServerPromise.tsx new file mode 100644 index 0000000..ac5d06a --- /dev/null +++ b/web/src/components/callServerPromise.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { toast } from "sonner"; + +export async function callServerPromise(result: Promise) { + return result + .then((x) => { + if ((x as { message: string })?.message !== undefined) { + toast.success((x as { message: string }).message); + } + return x; + }) + .catch((error) => { + toast.error(error.message); + return null; + }); +} diff --git a/web/src/db/schema.ts b/web/src/db/schema.ts index 1d999be..e832462 100644 --- a/web/src/db/schema.ts +++ b/web/src/db/schema.ts @@ -203,5 +203,19 @@ export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({ }), })); +export const apiKeyTable = dbSchema.table("api_keys", { + id: uuid("id").primaryKey().defaultRandom().notNull(), + key: text("key").notNull().unique(), + name: text("name").notNull(), + user_id: text("user_id") + .references(() => usersTable.id, { + onDelete: "cascade", + }) + .notNull(), + org_id: text("org_id"), + created_at: timestamp("created_at").defaultNow().notNull(), + updated_at: timestamp("updated_at").defaultNow().notNull(), +}); + export type UserType = InferSelectModel; export type WorkflowType = InferSelectModel; diff --git a/web/src/server/curdApiKeys.ts b/web/src/server/curdApiKeys.ts new file mode 100644 index 0000000..d55bc47 --- /dev/null +++ b/web/src/server/curdApiKeys.ts @@ -0,0 +1,78 @@ +"use server"; + +import { db } from "@/db/db"; +import { apiKeyTable } from "@/db/schema"; +import { auth } from "@clerk/nextjs"; +import { and, desc, eq } from "drizzle-orm"; +import { customAlphabet } from "nanoid"; +import { revalidatePath } from "next/cache"; + +export const nanoid = customAlphabet( + "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +); + +const prefixes = { + cd: "cd", +} as const; + +function newId(prefix: keyof typeof prefixes): string { + return [prefixes[prefix], nanoid(16)].join("_"); +} + +export async function addNewAPIKey(name: string) { + const { userId, orgId } = auth(); + + if (!userId) throw new Error("No user id"); + + const key = await db + .insert(apiKeyTable) + .values({ + name: name, + key: newId("cd"), + user_id: userId, + org_id: orgId, + }) + .returning(); + + revalidatePath("/api-keys"); + + return key[0]; +} + +export async function deleteAPIKey(id: string) { + const { userId, orgId } = auth(); + + if (!userId) throw new Error("No user id"); + + if (orgId) { + await db + .delete(apiKeyTable) + .where(and(eq(apiKeyTable.id, id), eq(apiKeyTable.org_id, orgId))) + .execute(); + } else { + await db + .delete(apiKeyTable) + .where(and(eq(apiKeyTable.id, id), eq(apiKeyTable.user_id, userId))) + .execute(); + } + + revalidatePath("/api-keys"); +} + +export async function getAPIKeys() { + const { userId, orgId } = auth(); + + if (!userId) throw new Error("No user id"); + + if (orgId) { + return await db.query.apiKeyTable.findMany({ + where: eq(apiKeyTable.org_id, orgId), + orderBy: desc(apiKeyTable.created_at), + }); + } else { + return await db.query.apiKeyTable.findMany({ + where: eq(apiKeyTable.user_id, userId), + orderBy: desc(apiKeyTable.created_at), + }); + } +}