diff --git a/.gitignore b/.gitignore index ed8ebf5..c34d28b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -__pycache__ \ No newline at end of file +__pycache__ +.DS_Store \ No newline at end of file diff --git a/web/.env.example b/web/.env.example index ded4e58..8eefbb5 100644 --- a/web/.env.example +++ b/web/.env.example @@ -13,5 +13,7 @@ SPACES_SECRET="aaa" SPACES_CDN_DONT_INCLUDE_BUCKET="false" SPACES_CDN_FORCE_PATH_STYLE="true" +MODAL_BUILDER_URL= + JWT_SECRET="openssl rand -hex 32" PLAUSIBLE_DOMAIN= \ No newline at end of file diff --git a/web/drizzle/0020_complete_black_tom.sql b/web/drizzle/0020_complete_black_tom.sql new file mode 100644 index 0000000..4928db9 --- /dev/null +++ b/web/drizzle/0020_complete_black_tom.sql @@ -0,0 +1,9 @@ +DO $$ BEGIN + CREATE TYPE "machine_status" AS ENUM('ready', 'building', 'error'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "status" "machine_status" DEFAULT 'ready' NOT NULL;--> statement-breakpoint +ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "snapshot" jsonb;--> statement-breakpoint +ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "build_log" text; \ No newline at end of file diff --git a/web/drizzle/meta/0020_snapshot.json b/web/drizzle/meta/0020_snapshot.json new file mode 100644 index 0000000..68cc8d2 --- /dev/null +++ b/web/drizzle/meta/0020_snapshot.json @@ -0,0 +1,703 @@ +{ + "id": "60fd6aaf-0bd4-4b77-b707-fea7c44e829d", + "prevId": "4c2423cd-420e-49fc-a6fe-2e6c56ce8c61", + "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 + }, + "revoked": { + "name": "revoked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": "cascade", + "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 + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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()" + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "machine_type", + "primaryKey": false, + "notNull": true, + "default": "'classic'" + }, + "status": { + "name": "status", + "type": "machine_status", + "primaryKey": false, + "notNull": true, + "default": "'ready'" + }, + "snapshot": { + "name": "snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "build_log": { + "name": "build_log", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "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 + }, + "origin": { + "name": "origin", + "type": "workflow_run_origin", + "primaryKey": false, + "notNull": true, + "default": "'api'" + }, + "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 + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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" + } + }, + "machine_status": { + "name": "machine_status", + "values": { + "ready": "ready", + "building": "building", + "error": "error" + } + }, + "machine_type": { + "name": "machine_type", + "values": { + "classic": "classic", + "runpod-serverless": "runpod-serverless", + "modal-serverless": "modal-serverless" + } + }, + "workflow_run_origin": { + "name": "workflow_run_origin", + "values": { + "manual": "manual", + "api": "api" + } + }, + "workflow_run_status": { + "name": "workflow_run_status", + "values": { + "not-started": "not-started", + "running": "running", + "uploading": "uploading", + "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 029c90f..3ae00ed 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -141,6 +141,13 @@ "when": 1704174903117, "tag": "0019_damp_stick", "breakpoints": true + }, + { + "idx": 20, + "version": "5", + "when": 1704350033885, + "tag": "0020_complete_black_tom", + "breakpoints": true } ] } \ No newline at end of file diff --git a/web/src/app/(app)/api/machine-built/route.ts b/web/src/app/(app)/api/machine-built/route.ts new file mode 100644 index 0000000..4dea5ef --- /dev/null +++ b/web/src/app/(app)/api/machine-built/route.ts @@ -0,0 +1,50 @@ +import { parseDataSafe } from "../../../../lib/parseDataSafe"; +import { db } from "@/db/db"; +import { machinesTable } from "@/db/schema"; +import { eq } from "drizzle-orm"; +import { NextResponse } from "next/server"; +import { z } from "zod"; + +const Request = z.object({ + machine_id: z.string(), + endpoint: z.string().optional(), + build_log: z.string().optional(), +}); + +export async function POST(request: Request) { + const [data, error] = await parseDataSafe(Request, request); + if (!data || error) return error; + + console.log(data); + + const { machine_id, endpoint, build_log } = data; + + if (endpoint) { + await db + .update(machinesTable) + .set({ + status: "ready", + endpoint: endpoint, + build_log: build_log, + }) + .where(eq(machinesTable.id, machine_id)); + } else { + // console.log(data); + await db + .update(machinesTable) + .set({ + status: "error", + build_log: build_log, + }) + .where(eq(machinesTable.id, machine_id)); + } + + return NextResponse.json( + { + message: "success", + }, + { + status: 200, + } + ); +} diff --git a/web/src/app/(app)/machines/[machine_id]/loading.tsx b/web/src/app/(app)/machines/[machine_id]/loading.tsx new file mode 100644 index 0000000..9ff4783 --- /dev/null +++ b/web/src/app/(app)/machines/[machine_id]/loading.tsx @@ -0,0 +1,9 @@ +"use client"; + +import { LoadingPageWrapper } from "@/components/LoadingWrapper"; +import { usePathname } from "next/navigation"; + +export default function Loading() { + const pathName = usePathname(); + return ; +} diff --git a/web/src/app/(app)/machines/[machine_id]/page.tsx b/web/src/app/(app)/machines/[machine_id]/page.tsx new file mode 100644 index 0000000..e2b97c3 --- /dev/null +++ b/web/src/app/(app)/machines/[machine_id]/page.tsx @@ -0,0 +1,43 @@ +import { MachineBuildLog } from "../../../../components/MachineBuildLog"; +import { LogsViewer } from "@/components/LogsViewer"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { getRelativeTime } from "@/lib/getRelativeTime"; +import { getMachineById } from "@/server/curdMachine"; + +export default async function Page({ + params, +}: { + params: { machine_id: string }; +}) { + const machine = await getMachineById(params.machine_id); + + return ( +
+ + + {machine.name} + + {getRelativeTime(machine?.updated_at)} + + + + {machine.status == "building" && ( + + )} + {machine.build_log && ( + + )} + + +
+ ); +} diff --git a/web/src/app/(app)/workflows/[workflow_id]/loading.tsx b/web/src/app/(app)/workflows/[workflow_id]/loading.tsx deleted file mode 100644 index 372234d..0000000 --- a/web/src/app/(app)/workflows/[workflow_id]/loading.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { LoadingPageWrapper } from "@/components/LoadingWrapper"; - -export default function Loading() { - // You can add any UI inside Loading, including a Skeleton. - return ; -} diff --git a/web/src/components/LogsViewer.tsx b/web/src/components/LogsViewer.tsx new file mode 100644 index 0000000..b1e13b9 --- /dev/null +++ b/web/src/components/LogsViewer.tsx @@ -0,0 +1,46 @@ +"use client"; + +import React, { useEffect, useRef } from "react"; + +export type LogsType = { + machine_id?: string; + logs: string; + timestamp: number; +}[]; + +export function LogsViewer({ logs }: { logs: LogsType }) { + const container = useRef(null); + + useEffect(() => { + // console.log(logs.length, container.current); + if (container.current) { + const scrollHeight = container.current.scrollHeight; + + container.current.scrollTo({ + top: scrollHeight, + behavior: "smooth", + }); + } + }, [logs.length]); + + return ( +
{ + if (!container.current && ref) { + const scrollHeight = ref.scrollHeight; + + ref.scrollTo({ + top: scrollHeight, + behavior: "instant", + }); + } + container.current = ref; + }} + className="flex flex-col text-xs p-2 overflow-y-scroll max-h-[400px] whitespace-break-spaces" + > + {logs.map((x, i) => ( +
{x.logs}
+ ))} +
+ ); +} diff --git a/web/src/components/MachineBuildLog.tsx b/web/src/components/MachineBuildLog.tsx new file mode 100644 index 0000000..13b25be --- /dev/null +++ b/web/src/components/MachineBuildLog.tsx @@ -0,0 +1,48 @@ +"use client"; + +import type { LogsType } from "@/components/LogsViewer"; +import { LogsViewer } from "@/components/LogsViewer"; +import { getConnectionStatus } from "@/components/getConnectionStatus"; +import { useEffect, useState } from "react"; +import useWebSocket from "react-use-websocket"; + +export function MachineBuildLog({ + machine_id, + endpoint, +}: { + machine_id: string; + endpoint: string; +}) { + const [logs, setLogs] = useState([]); + + const wsEndpoint = endpoint.replace(/^http/, "ws"); + const { lastMessage, readyState } = useWebSocket( + `${wsEndpoint}/ws/${machine_id}`, + { + shouldReconnect: () => true, + reconnectAttempts: 20, + reconnectInterval: 1000, + } + ); + + const connectionStatus = getConnectionStatus(readyState); + + useEffect(() => { + if (!lastMessage?.data) return; + + const message = JSON.parse(lastMessage.data); + + console.log(message); + + if (message?.event === "LOGS") { + setLogs((logs) => [...(logs ?? []), message.data]); + } + }, [lastMessage]); + + return ( +
+ {connectionStatus} + +
+ ); +} diff --git a/web/src/components/MachineList.tsx b/web/src/components/MachineList.tsx index fbb5bc6..b1c3f5c 100644 --- a/web/src/components/MachineList.tsx +++ b/web/src/components/MachineList.tsx @@ -23,8 +23,12 @@ import { TableRow, } from "@/components/ui/table"; import { type MachineType } from "@/db/schema"; -import { addMachineSchema } from "@/server/addMachineSchema"; import { + addCustomMachineSchema, + addMachineSchema, +} from "@/server/addMachineSchema"; +import { + addCustomMachine, addMachine, deleteMachine, disableMachine, @@ -92,10 +96,15 @@ export const columns: ColumnDef[] = [ return ( //
-
{row.getValue("name")}
+
+ {row.getValue("name")} + {row.original.disabled && ( Disabled )} + {!row.original.disabled && row.original.status && ( + {row.original.status} + )}
// ); @@ -254,6 +263,20 @@ export function MachineList({ data }: { data: Machine[] }) { serverAction={addMachine} formSchema={addMachineSchema} /> +
diff --git a/web/src/components/MachinesWS.tsx b/web/src/components/MachinesWS.tsx index c923040..6f543c6 100644 --- a/web/src/components/MachinesWS.tsx +++ b/web/src/components/MachinesWS.tsx @@ -1,5 +1,7 @@ "use client"; +import { LogsViewer } from "./LogsViewer"; +import { getConnectionStatus } from "./getConnectionStatus"; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -10,9 +12,8 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import type { getMachines } from "@/server/curdMachine"; -import { Check, CircleOff, SatelliteDish } from "lucide-react"; -import React, { useEffect, useRef, useState } from "react"; -import useWebSocket, { ReadyState } from "react-use-websocket"; +import React, { useEffect, useState } from "react"; +import useWebSocket from "react-use-websocket"; import { create } from "zustand"; type State = { @@ -98,17 +99,7 @@ function MachineWS({ } ); - const connectionStatus = { - [ReadyState.CONNECTING]: ( - - ), - [ReadyState.OPEN]: , - [ReadyState.CLOSING]: , - [ReadyState.CLOSED]: , - [ReadyState.UNINSTANTIATED]: "Uninstantiated", - }[readyState]; - - const container = useRef(null); + const connectionStatus = getConnectionStatus(readyState); useEffect(() => { if (!lastMessage?.data) return; @@ -130,18 +121,6 @@ function MachineWS({ } }, [lastMessage]); - useEffect(() => { - // console.log(logs.length, container.current); - if (container.current) { - const scrollHeight = container.current.scrollHeight; - - container.current.scrollTo({ - top: scrollHeight, - behavior: "smooth", - }); - } - }, [logs.length]); - return ( @@ -156,24 +135,7 @@ function MachineWS({ You can view your run's outputs here -
{ - if (!container.current && ref) { - const scrollHeight = ref.scrollHeight; - - ref.scrollTo({ - top: scrollHeight, - behavior: "instant", - }); - } - container.current = ref; - }} - className="flex flex-col text-xs p-2 overflow-y-scroll max-h-[400px] whitespace-break-spaces" - > - {logs.map((x, i) => ( -
{x.logs}
- ))} -
+
); diff --git a/web/src/components/getConnectionStatus.tsx b/web/src/components/getConnectionStatus.tsx new file mode 100644 index 0000000..5d454d6 --- /dev/null +++ b/web/src/components/getConnectionStatus.tsx @@ -0,0 +1,17 @@ +import { Check, CircleOff, SatelliteDish } from "lucide-react"; +import React from "react"; +import { ReadyState } from "react-use-websocket"; + +export function getConnectionStatus(readyState: ReadyState) { + const connectionStatus = { + [ReadyState.CONNECTING]: ( + + ), + [ReadyState.OPEN]: , + [ReadyState.CLOSING]: , + [ReadyState.CLOSED]: , + [ReadyState.UNINSTANTIATED]: "Uninstantiated", + }[readyState]; + + return connectionStatus; +} diff --git a/web/src/db/schema.ts b/web/src/db/schema.ts index 7ce9490..a04bba0 100644 --- a/web/src/db/schema.ts +++ b/web/src/db/schema.ts @@ -108,6 +108,12 @@ export const machinesType = pgEnum("machine_type", [ "modal-serverless", ]); +export const machinesStatus = pgEnum("machine_status", [ + "ready", + "building", + "error", +]); + // We still want to keep the workflow run record. export const workflowRunsTable = dbSchema.table("workflow_runs", { id: uuid("id").primaryKey().defaultRandom().notNull(), @@ -193,6 +199,9 @@ export const machinesTable = dbSchema.table("machines", { disabled: boolean("disabled").default(false).notNull(), auth_token: text("auth_token"), type: machinesType("type").notNull().default("classic"), + status: machinesStatus("status").notNull().default("ready"), + snapshot: jsonb("snapshot").$type(), + build_log: text("build_log"), }); export const insertMachineSchema = createInsertSchema(machinesTable, { diff --git a/web/src/server/addMachineSchema.ts b/web/src/server/addMachineSchema.ts index 1119930..9f5bc5c 100644 --- a/web/src/server/addMachineSchema.ts +++ b/web/src/server/addMachineSchema.ts @@ -1,4 +1,5 @@ -import { insertMachineSchema } from "@/db/schema"; +import { insertMachineSchema, machinesTable } from "@/db/schema"; +import { createInsertSchema } from "drizzle-zod"; export const addMachineSchema = insertMachineSchema.pick({ name: true, @@ -6,3 +7,14 @@ export const addMachineSchema = insertMachineSchema.pick({ type: true, auth_token: true, }); + +export const insertCustomMachineSchema = createInsertSchema(machinesTable, { + name: (schema) => schema.name.default("My Machine"), + type: (schema) => schema.type.default("modal-serverless"), +}); + +export const addCustomMachineSchema = insertCustomMachineSchema.pick({ + name: true, + type: true, + snapshot: true, +}); diff --git a/web/src/server/curdMachine.ts b/web/src/server/curdMachine.ts index 91af5e1..48e886a 100644 --- a/web/src/server/curdMachine.ts +++ b/web/src/server/curdMachine.ts @@ -1,12 +1,17 @@ "use server"; -import type { addMachineSchema } from "./addMachineSchema"; +import type { + addCustomMachineSchema, + addMachineSchema, +} from "./addMachineSchema"; import { withServerPromise } from "./withServerPromise"; import { db } from "@/db/db"; import { machinesTable } from "@/db/schema"; import { auth } from "@clerk/nextjs"; -import { and, eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import { revalidatePath } from "next/cache"; +import { headers } from "next/headers"; +import { redirect } from "next/navigation"; import "server-only"; import type { z } from "zod"; @@ -27,6 +32,26 @@ export async function getMachines() { return machines; } +export async function getMachineById(id: string) { + const { userId, orgId } = auth(); + if (!userId) throw new Error("No user id"); + const machines = await db + .select() + .from(machinesTable) + .where( + and( + orgId + ? eq(machinesTable.org_id, orgId) + : and( + eq(machinesTable.user_id, userId), + isNull(machinesTable.org_id) + ), + eq(machinesTable.id, id) + ) + ); + return machines[0]; +} + export const addMachine = withServerPromise( async (data: z.infer) => { const { userId, orgId } = auth(); @@ -42,6 +67,72 @@ export const addMachine = withServerPromise( } ); +export const addCustomMachine = withServerPromise( + async (data: z.infer) => { + const { userId, orgId } = auth(); + const headersList = headers(); + + if (!userId) return { error: "No user id" }; + + // Insert to our db + const a = await db + .insert(machinesTable) + .values({ + ...data, + org_id: orgId, + user_id: userId, + status: "building", + endpoint: "not-ready", + }) + .returning(); + + const b = a[0]; + + // const origin = new URL(request.url).origin; + const domain = headersList.get("x-forwarded-host") || ""; + const protocol = headersList.get("x-forwarded-proto") || ""; + // console.log("domain", domain); + // console.log("domain", `${protocol}://${domain}/api/machine-built`); + // return { message: "Machine Building" }; + + if (domain === "") { + throw new Error("No domain"); + } + + // Call remote builder + const result = await fetch(`${process.env.MODAL_BUILDER_URL!}/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + machine_id: b.id, + name: b.id, + snapshot: JSON.parse(data.snapshot as string), + callback_url: `${protocol}://${domain}/api/machine-built`, + }), + }); + + if (!result.ok) { + const error_log = await result.text(); + await db + .update(machinesTable) + .set({ + ...data, + status: "error", + build_log: error_log, + }) + .where(eq(machinesTable.id, b.id)); + throw new Error(`Error: ${result.statusText} ${error_log}`); + } + + redirect(`/machines/${b.id}`); + + // revalidatePath("/machines"); + return { message: "Machine Building" }; + } +); + export const updateMachine = withServerPromise( async ({ id, diff --git a/web/src/server/withServerPromise.ts b/web/src/server/withServerPromise.ts index dc5e126..94f8540 100644 --- a/web/src/server/withServerPromise.ts +++ b/web/src/server/withServerPromise.ts @@ -1,5 +1,8 @@ +import { isRedirectError } from "next/dist/client/components/redirect"; + export async function wrapServerPromise(result: Promise) { return result.catch((error) => { + if (isRedirectError(error)) throw error; return { error: error.message, };