feat: add preview image builder

This commit is contained in:
BennyKok 2024-01-04 22:29:22 +08:00
parent 314eb9fd16
commit 9937252777
18 changed files with 1085 additions and 56 deletions

3
.gitignore vendored
View File

@ -1 +1,2 @@
__pycache__
__pycache__
.DS_Store

View File

@ -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=

View File

@ -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;

View File

@ -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": {}
}
}

View File

@ -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
}
]
}

View File

@ -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,
}
);
}

View File

@ -0,0 +1,9 @@
"use client";
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
import { usePathname } from "next/navigation";
export default function Loading() {
const pathName = usePathname();
return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
}

View File

@ -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 (
<div>
<Card className="w-full h-fit mt-4">
<CardHeader>
<CardTitle>{machine.name}</CardTitle>
<CardDescription suppressHydrationWarning={true}>
{getRelativeTime(machine?.updated_at)}
</CardDescription>
</CardHeader>
<CardContent>
{machine.status == "building" && (
<MachineBuildLog
machine_id={params.machine_id}
endpoint={process.env.MODAL_BUILDER_URL!}
/>
)}
{machine.build_log && (
<LogsViewer logs={JSON.parse(machine.build_log)} />
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -1,6 +0,0 @@
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
export default function Loading() {
// You can add any UI inside Loading, including a Skeleton.
return <LoadingPageWrapper tag="workflow" />;
}

View File

@ -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<HTMLDivElement | null>(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 (
<div
ref={(ref) => {
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) => (
<div key={i}>{x.logs}</div>
))}
</div>
);
}

View File

@ -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<LogsType>([]);
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 (
<div>
{connectionStatus}
<LogsViewer logs={logs} />
</div>
);
}

View File

@ -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<Machine>[] = [
return (
// <a className="hover:underline" href={`/${row.original.id}`}>
<div className="flex flex-row gap-2 items-center">
<div>{row.getValue("name")}</div>
<a href={`/machines/${row.original.id}`} className="hover:underline">
{row.getValue("name")}
</a>
{row.original.disabled && (
<Badge variant="destructive">Disabled</Badge>
)}
{!row.original.disabled && row.original.status && (
<Badge variant="outline">{row.original.status}</Badge>
)}
</div>
// </a>
);
@ -254,6 +263,20 @@ export function MachineList({ data }: { data: Machine[] }) {
serverAction={addMachine}
formSchema={addMachineSchema}
/>
<InsertModal
title="Custom Machine"
description="Add custom Comfyui machines to your account."
serverAction={addCustomMachine}
formSchema={addCustomMachineSchema}
fieldConfig={{
type: {
fieldType: "fallback",
inputProps: {
disabled: true,
},
},
}}
/>
</div>
</div>
<div className="rounded-md border overflow-x-auto w-full">

View File

@ -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]: (
<SatelliteDish size={14} className="text-orange-500" />
),
[ReadyState.OPEN]: <Check size={14} className="text-green-500" />,
[ReadyState.CLOSING]: <CircleOff size={14} className="text-orange-500" />,
[ReadyState.CLOSED]: <CircleOff size={14} className="text-red-500" />,
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
}[readyState];
const container = useRef<HTMLDivElement | null>(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 (
<Dialog>
<DialogTrigger asChild className="">
@ -156,24 +135,7 @@ function MachineWS({
You can view your run&apos;s outputs here
</DialogDescription>
</DialogHeader>
<div
ref={(ref) => {
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) => (
<div key={i}>{x.logs}</div>
))}
</div>
<LogsViewer logs={logs} />
</DialogContent>
</Dialog>
);

View File

@ -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]: (
<SatelliteDish size={14} className="text-orange-500" />
),
[ReadyState.OPEN]: <Check size={14} className="text-green-500" />,
[ReadyState.CLOSING]: <CircleOff size={14} className="text-orange-500" />,
[ReadyState.CLOSED]: <CircleOff size={14} className="text-red-500" />,
[ReadyState.UNINSTANTIATED]: "Uninstantiated",
}[readyState];
return connectionStatus;
}

View File

@ -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<any>(),
build_log: text("build_log"),
});
export const insertMachineSchema = createInsertSchema(machinesTable, {

View File

@ -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,
});

View File

@ -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<typeof addMachineSchema>) => {
const { userId, orgId } = auth();
@ -42,6 +67,72 @@ export const addMachine = withServerPromise(
}
);
export const addCustomMachine = withServerPromise(
async (data: z.infer<typeof addCustomMachineSchema>) => {
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,

View File

@ -1,5 +1,8 @@
import { isRedirectError } from "next/dist/client/components/redirect";
export async function wrapServerPromise<T>(result: Promise<T>) {
return result.catch((error) => {
if (isRedirectError(error)) throw error;
return {
error: error.message,
};