From 0835d966f1fa098f1effafe071ef6ee55a0a0c6b Mon Sep 17 00:00:00 2001 From: BennyKok Date: Wed, 13 Dec 2023 17:14:55 +0800 Subject: [PATCH] feat: add s3 localstack, upload api --- requirements.txt | 1 + routes.py | 78 +++- web/aws/buckets.sh | 2 + web/bun.lockb | Bin 275443 -> 326854 bytes web/docker-compose.yml | 10 +- web/drizzle/0004_zippy_freak.sql | 31 ++ web/drizzle/meta/0004_snapshot.json | 410 +++++++++++++++++++ web/drizzle/meta/_journal.json | 7 + web/package.json | 5 + web/src/app/[workflow_id]/page.tsx | 91 +--- web/src/app/api/create-run/route.ts | 33 +- web/src/app/api/file-upload/route.ts | 44 ++ web/src/app/api/update-run/route.ts | 42 +- web/src/app/api/upload/route.ts | 12 +- web/src/app/api/view/route.ts | 9 + web/src/app/layout.tsx | 2 + web/src/app/machines/page.tsx | 10 +- web/src/components/MachineList.tsx | 21 +- web/src/components/MachinesWS.tsx | 3 +- web/src/components/RunDisplay.tsx | 122 +++++- web/src/components/RunsTable.tsx | 35 ++ web/src/components/StatusBadge.tsx | 23 ++ web/src/components/VersionSelect.tsx | 5 +- web/src/components/ui/table.tsx | 53 ++- web/src/db/schema.ts | 39 +- web/src/lib/parseDataSafe.ts | 12 +- web/src/server/createRun.ts | 61 +-- web/src/server/curdMachine.ts | 16 +- web/src/server/findAllRuns.tsx | 23 ++ web/src/server/findFirstTableWithVersion.tsx | 10 + web/src/server/getRunsOutput.tsx | 12 + web/src/server/resource.ts | 106 +++++ web/src/types/ComfyAPI_Run.ts | 7 + web/tailwind.config.ts | 4 +- 34 files changed, 1112 insertions(+), 227 deletions(-) create mode 100644 requirements.txt create mode 100755 web/aws/buckets.sh create mode 100644 web/drizzle/0004_zippy_freak.sql create mode 100644 web/drizzle/meta/0004_snapshot.json create mode 100644 web/src/app/api/file-upload/route.ts create mode 100644 web/src/app/api/view/route.ts create mode 100644 web/src/components/RunsTable.tsx create mode 100644 web/src/components/StatusBadge.tsx create mode 100644 web/src/server/findAllRuns.tsx create mode 100644 web/src/server/findFirstTableWithVersion.tsx create mode 100644 web/src/server/getRunsOutput.tsx create mode 100644 web/src/server/resource.ts create mode 100644 web/src/types/ComfyAPI_Run.ts diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1db657b --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +boto3 \ No newline at end of file diff --git a/routes.py b/routes.py index 6db1603..6ce05db 100644 --- a/routes.py +++ b/routes.py @@ -22,6 +22,7 @@ from enum import Enum import aiohttp from aiohttp import web +import boto3 api = None api_task = None @@ -96,6 +97,7 @@ async def comfy_deploy_run(request): prompt_metadata[res['prompt_id']] = { 'status_endpoint': data.get('status_endpoint'), + 'file_upload_endpoint': data.get('file_upload_endpoint'), } status = 200 @@ -151,17 +153,18 @@ async def send_json_override(self, event, data, sid=None): # now we send everything await send(event, data) + await self.send_json_original(event, data, sid) if event == 'execution_start': update_run(prompt_id, Status.RUNNING) - # if event == 'executing': - # update_run(prompt_id, Status.RUNNING) - - if event == 'executed': + # the last executing event is none, then the workflow is finished + if event == 'executing' and data.get('node') is None: update_run(prompt_id, Status.SUCCESS) - await self.send_json_original(event, data, sid) + if event == 'executed' and 'node' in data and 'output' in data: + asyncio.create_task(update_run_with_output(prompt_id, data.get('output'))) + # update_run_with_output(prompt_id, data.get('output')) class Status(Enum): @@ -171,7 +174,10 @@ class Status(Enum): FAILED = "failed" def update_run(prompt_id, status: Status): - if prompt_id in prompt_metadata and ('status' not in prompt_metadata[prompt_id] or prompt_metadata[prompt_id]['status'] != status): + if prompt_id not in prompt_metadata: + return + + if ('status' not in prompt_metadata[prompt_id] or prompt_metadata[prompt_id]['status'] != status): status_endpoint = prompt_metadata[prompt_id]['status_endpoint'] body = { "run_id": prompt_id, @@ -180,5 +186,65 @@ def update_run(prompt_id, status: Status): prompt_metadata[prompt_id]['status'] = status requests.post(status_endpoint, json=body) + +async def upload_file(prompt_id, filename, subfolder=None): + """ + Uploads file to S3 bucket using S3 client object + :return: None + """ + filename,output_dir = folder_paths.annotated_filepath(filename) + + # validation for security: prevent accessing arbitrary path + if filename[0] == '/' or '..' in filename: + return + + if output_dir is None: + output_dir = folder_paths.get_directory_by_type("output") + + if output_dir is None: + return + + if subfolder != None: + full_output_dir = os.path.join(output_dir, subfolder) + if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir: + return + output_dir = full_output_dir + + filename = os.path.basename(filename) + file = os.path.join(output_dir, filename) + + print("uploading file", file) + + file_upload_endpoint = prompt_metadata[prompt_id]['file_upload_endpoint'] + + content_type = "image/png" + + result = requests.get(f"{file_upload_endpoint}?file_name={filename}&run_id={prompt_id}&type={content_type}") + ok = result.json() + + with open(file, 'rb') as f: + data = f.read() + headers = { + "x-amz-acl": "public-read", + "Content-Type": content_type, + "Content-Length": str(len(data)), + } + response = requests.put(ok.get("url"), headers=headers, data=data) + print("upload file response", response.status_code) + +async def update_run_with_output(prompt_id, data): + if prompt_id in prompt_metadata: + status_endpoint = prompt_metadata[prompt_id]['status_endpoint'] + + images = data.get('images', []) + for image in images: + await upload_file(prompt_id, image.get("filename"), subfolder=image.get("subfolder")) + + body = { + "run_id": prompt_id, + "output_data": data + } + requests.post(status_endpoint, json=body) + prompt_server.send_json_original = prompt_server.send_json prompt_server.send_json = send_json_override.__get__(prompt_server, server.PromptServer) \ No newline at end of file diff --git a/web/aws/buckets.sh b/web/aws/buckets.sh new file mode 100755 index 0000000..384bb6c --- /dev/null +++ b/web/aws/buckets.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +awslocal s3 mb s3://comfyui-deploy \ No newline at end of file diff --git a/web/bun.lockb b/web/bun.lockb index 58976711facafb1e3371c3250b45b091dca0f8b7..6229656203770127e890ca1e143455089ea2f978 100755 GIT binary patch delta 88994 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

cgNE+U@yUH(a`Z)^3f= z?Uz1FYOq`lPc(1Yxd=O^gQezxxdk}j-~oYW+i&m9N{8=kK0U2r)Z(TESjL8r`6SPl zuL&id`#RbdXB~E?abh*xeB-2P-;GU=u7?zh*<8*-0G!_ir!=aoD(Ue(++sF^Pbr$d zbsy1!!|z0(VPuebe4d4b3W+jAnG2F-!C#g6l}?cQ@Ldf^^dl^X&r zNkD~O*odDq(P6uxlRIAwz)lo}$q>K|;Rl*c@8OHcR|3?P02-hNGTgaTq%%FP1Ur9Ff T14TXq1Qod5zqY=bzaRet`VBCw delta 54301 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 NzqHHJ3DhlE_CNZ-x()yU diff --git a/web/docker-compose.yml b/web/docker-compose.yml index 8cce5eb..7bf20d1 100644 --- a/web/docker-compose.yml +++ b/web/docker-compose.yml @@ -19,4 +19,12 @@ services: ports: - "5481:80" depends_on: - - postgres \ No newline at end of file + - postgres + localstack: + image: localstack/localstack:latest + environment: + SERVICES: s3 + ports: + - 4566:4566 + volumes: + - ./aws:/etc/localstack/init/ready.d \ No newline at end of file diff --git a/web/drizzle/0004_zippy_freak.sql b/web/drizzle/0004_zippy_freak.sql new file mode 100644 index 0000000..43b6cd4 --- /dev/null +++ b/web/drizzle/0004_zippy_freak.sql @@ -0,0 +1,31 @@ +CREATE TABLE IF NOT EXISTS "comfy_deploy"."workflow_run_outputs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "run_id" uuid NOT NULL, + "data" jsonb, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "comfy_deploy"."machines" DROP CONSTRAINT "machines_user_id_users_id_fk"; +--> statement-breakpoint +ALTER TABLE "comfy_deploy"."workflow_runs" DROP CONSTRAINT "workflow_runs_workflow_version_id_workflow_versions_id_fk"; +--> statement-breakpoint +ALTER TABLE "comfy_deploy"."workflow_runs" ALTER COLUMN "workflow_version_id" DROP NOT NULL;--> statement-breakpoint +ALTER TABLE "comfy_deploy"."workflow_runs" ALTER COLUMN "machine_id" DROP NOT NULL;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "comfy_deploy"."machines" ADD CONSTRAINT "machines_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfy_deploy"."users"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "comfy_deploy"."workflow_runs" ADD CONSTRAINT "workflow_runs_workflow_version_id_workflow_versions_id_fk" FOREIGN KEY ("workflow_version_id") REFERENCES "comfy_deploy"."workflow_versions"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "comfy_deploy"."workflow_run_outputs" ADD CONSTRAINT "workflow_run_outputs_run_id_workflow_runs_id_fk" FOREIGN KEY ("run_id") REFERENCES "comfy_deploy"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/web/drizzle/meta/0004_snapshot.json b/web/drizzle/meta/0004_snapshot.json new file mode 100644 index 0000000..91ae114 --- /dev/null +++ b/web/drizzle/meta/0004_snapshot.json @@ -0,0 +1,410 @@ +{ + "id": "07a389e2-3713-4047-93e7-bf1da2333b16", + "prevId": "4e03f61d-b976-41b4-bbad-7655f73bf0fc", + "version": "5", + "dialect": "pg", + "tables": { + "machines": { + "name": "machines", + "schema": "comfy_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": "comfy_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": "comfy_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": "comfy_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_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": "comfy_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": "comfy_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": { + "workflow_run_status": { + "name": "workflow_run_status", + "values": { + "not-started": "not-started", + "running": "running", + "success": "success", + "failed": "failed" + } + } + }, + "schemas": { + "comfy_deploy": "comfy_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 970601c..c5c9eea 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1702212969930, "tag": "0003_oval_mockingbird", "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1702357291227, + "tag": "0004_zippy_freak", + "breakpoints": true } ] } \ No newline at end of file diff --git a/web/package.json b/web/package.json index 9b8fb55..0d25ba4 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,8 @@ "db-dev": "bun run db-up && bun run migrate-local" }, "dependencies": { + "@aws-sdk/client-s3": "^3.472.0", + "@aws-sdk/s3-request-presigner": "^3.472.0", "@clerk/nextjs": "^4.27.4", "@hookform/resolvers": "^3.3.2", "@neondatabase/serverless": "^0.6.0", @@ -26,6 +28,7 @@ "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-tabs": "^1.0.4", "@tanstack/react-table": "^8.10.7", + "@types/uuid": "^9.0.7", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "dayjs": "^1.11.10", @@ -36,8 +39,10 @@ "react-dom": "^18", "react-hook-form": "^7.48.2", "react-use-websocket": "^4.5.0", + "sonner": "^1.2.4", "tailwind-merge": "^2.1.0", "tailwindcss-animate": "^1.0.7", + "uuid": "^9.0.1", "zod": "^3.22.4", "zustand": "^4.4.7" }, diff --git a/web/src/app/[workflow_id]/page.tsx b/web/src/app/[workflow_id]/page.tsx index b4aea3e..2e651ed 100644 --- a/web/src/app/[workflow_id]/page.tsx +++ b/web/src/app/[workflow_id]/page.tsx @@ -1,12 +1,11 @@ -import { RunDisplay } from "../../components/RunDisplay"; -import { LoadingIcon } from "@/components/LoadingIcon"; +import { RunsTable } from "../../components/RunsTable"; +import { findFirstTableWithVersion } from "../../server/findFirstTableWithVersion"; import { MachinesWSMain } from "@/components/MachinesWS"; import { MachineSelect, RunWorkflowButton, VersionSelect, } from "@/components/VersionSelect"; -import { Badge } from "@/components/ui/badge"; import { Card, CardContent, @@ -14,49 +13,8 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { - Table, - TableBody, - TableCaption, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { db } from "@/db/db"; -import { - workflowRunsTable, - workflowTable, - workflowVersionTable, -} from "@/db/schema"; import { getRelativeTime } from "@/lib/getRelativeTime"; import { getMachines } from "@/server/curdMachine"; -import { desc, eq } from "drizzle-orm"; - -export async function findFirstTableWithVersion(workflow_id: string) { - return await db.query.workflowTable.findFirst({ - with: { versions: { orderBy: desc(workflowVersionTable.version) } }, - where: eq(workflowTable.id, workflow_id), - }); -} - -export async function findAllRuns(workflow_id: string) { - return await db.query.workflowRunsTable.findMany({ - where: eq(workflowRunsTable.workflow_id, workflow_id), - orderBy: desc(workflowRunsTable.created_at), - with: { - machine: { - columns: { - name: true, - }, - }, - version: { - columns: { - version: true, - }, - }, - }, - }); -} export default async function Page({ params, @@ -69,7 +27,7 @@ export default async function Page({ const machines = await getMachines(); return ( -

+
{workflow?.name} @@ -101,46 +59,3 @@ export default async function Page({
); } - -async function RunsTable(props: { workflow_id: string }) { - const allRuns = await findAllRuns(props.workflow_id); - return ( - - A list of your recent runs. - - - Version - Machine - Time - Live Status - Status - - - - {allRuns.map((run) => ( - - ))} - -
- ); -} - -export function StatusBadge({ - run, -}: { - run: Awaited>[0]; -}) { - switch (run.status) { - case "running": - return ( - - {run.status} - - ); - case "success": - return {run.status}; - case "failed": - return {run.status}; - } - return {run.status}; -} diff --git a/web/src/app/api/create-run/route.ts b/web/src/app/api/create-run/route.ts index aab477e..5e62f60 100644 --- a/web/src/app/api/create-run/route.ts +++ b/web/src/app/api/create-run/route.ts @@ -1,5 +1,6 @@ import { parseDataSafe } from "../../../lib/parseDataSafe"; import { createRun } from "../../../server/createRun"; +import { NextResponse } from "next/server"; import { z } from "zod"; const Request = z.object({ @@ -8,12 +9,6 @@ const Request = z.object({ machine_id: z.string(), }); -export const ComfyAPI_Run = z.object({ - prompt_id: z.string(), - number: z.number(), - node_errors: z.any(), -}); - export async function POST(request: Request) { const [data, error] = await parseDataSafe(Request, request); if (!data || error) return error; @@ -22,5 +17,29 @@ export async function POST(request: Request) { const { workflow_version_id, machine_id } = data; - return await createRun(origin, workflow_version_id, machine_id); + try { + const workflow_run_id = await createRun( + origin, + workflow_version_id, + machine_id + ); + + return NextResponse.json( + { + workflow_run_id: workflow_run_id, + }, + { + status: 200, + } + ); + } catch (error: any) { + return NextResponse.json( + { + error: error.message, + }, + { + status: 500, + } + ); + } } diff --git a/web/src/app/api/file-upload/route.ts b/web/src/app/api/file-upload/route.ts new file mode 100644 index 0000000..0c65efb --- /dev/null +++ b/web/src/app/api/file-upload/route.ts @@ -0,0 +1,44 @@ +import { parseDataSafe } from "../../../lib/parseDataSafe"; +import { handleResourceUpload } from "@/server/resource"; +import { NextResponse } from "next/server"; +import { z } from "zod"; + +const Request = z.object({ + file_name: z.string(), + run_id: z.string(), + type: z.enum(["image/png", "image/jpeg"]), +}); + +export const dynamic = "force-dynamic"; + +export async function GET(request: Request) { + const [data, error] = await parseDataSafe(Request, request); + if (!data || error) return error; + + const { file_name, run_id, type } = data; + + try { + const uploadUrl = await handleResourceUpload({ + resourceBucket: "comfyui-deploy", + resourceId: `outputs/runs/${run_id}/${file_name}`, + resourceType: type, + isPublic: true, + }); + + return NextResponse.json( + { + url: uploadUrl, + }, + { status: 200 } + ); + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return NextResponse.json( + { + error: errorMessage, + }, + { status: 500 } + ); + } +} diff --git a/web/src/app/api/update-run/route.ts b/web/src/app/api/update-run/route.ts index 3418680..d928bcb 100644 --- a/web/src/app/api/update-run/route.ts +++ b/web/src/app/api/update-run/route.ts @@ -1,35 +1,47 @@ import { parseDataSafe } from "../../../lib/parseDataSafe"; import { db } from "@/db/db"; -import { workflowRunsTable } from "@/db/schema"; +import { workflowRunOutputs, workflowRunsTable } from "@/db/schema"; import { eq } from "drizzle-orm"; -import { revalidatePath } from "next/cache"; import { NextResponse } from "next/server"; import { z } from "zod"; const Request = z.object({ run_id: z.string(), - status: z.enum(["not-started", "running", "success", "failed"]), + status: z.enum(["not-started", "running", "success", "failed"]).optional(), + output_data: z.any().optional(), }); export async function POST(request: Request) { const [data, error] = await parseDataSafe(Request, request); if (!data || error) return error; - const { run_id, status } = data; + const { run_id, status, output_data } = data; - const workflow_run = await db - .update(workflowRunsTable) - .set({ - status: status, - }) - .where(eq(workflowRunsTable.id, run_id)) - .returning(); + console.log(run_id, status, output_data); - const workflow_version = await db.query.workflowVersionTable.findFirst({ - where: eq(workflowRunsTable.id, workflow_run[0].workflow_version_id), - }); + if (output_data) { + const workflow_run_output = await db.insert(workflowRunOutputs).values({ + run_id: run_id, + data: output_data, + }); + } else if (status) { + console.log("status", status); + const workflow_run = await db + .update(workflowRunsTable) + .set({ + status: status, + ended_at: + status === "success" || status === "failed" ? new Date() : null, + }) + .where(eq(workflowRunsTable.id, run_id)) + .returning(); + } - revalidatePath(`./${workflow_version?.workflow_id}`); + // const workflow_version = await db.query.workflowVersionTable.findFirst({ + // where: eq(workflowRunsTable.id, workflow_run[0].workflow_version_id), + // }); + + // revalidatePath(`./${workflow_version?.workflow_id}`); return NextResponse.json( { diff --git a/web/src/app/api/upload/route.ts b/web/src/app/api/upload/route.ts index c27be52..5ec0065 100644 --- a/web/src/app/api/upload/route.ts +++ b/web/src/app/api/upload/route.ts @@ -1,9 +1,9 @@ import { parseDataSafe } from "../../../lib/parseDataSafe"; import { db } from "@/db/db"; import { workflowTable, workflowVersionTable } from "@/db/schema"; -import { eq, sql } from "drizzle-orm"; +import { sql } from "drizzle-orm"; import { NextResponse } from "next/server"; -import { ZodFormattedError, z } from "zod"; +import { z } from "zod"; const corsHeaders = { "Access-Control-Allow-Origin": "*", @@ -36,7 +36,7 @@ export async function POST(request: Request) { const [data, error] = await parseDataSafe( UploadRequest, request, - corsHeaders, + corsHeaders ); if (!data || error) return error; @@ -96,7 +96,7 @@ export async function POST(request: Request) { status: 500, statusText: "Invalid request", headers: corsHeaders, - }, + } ); } } catch (error: any) { @@ -108,7 +108,7 @@ export async function POST(request: Request) { status: 500, statusText: "Invalid request", headers: corsHeaders, - }, + } ); } @@ -120,6 +120,6 @@ export async function POST(request: Request) { { status: 200, headers: corsHeaders, - }, + } ); } diff --git a/web/src/app/api/view/route.ts b/web/src/app/api/view/route.ts new file mode 100644 index 0000000..c944d86 --- /dev/null +++ b/web/src/app/api/view/route.ts @@ -0,0 +1,9 @@ +import { NextResponse, type NextRequest } from "next/server"; + +export async function GET(request: NextRequest) { + const file = new URL(request.url).searchParams.get("file"); + console.log(file); + return NextResponse.redirect( + `${process.env.SPACES_ENDPOINT}/comfyui-deploy/${file}` + ); +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index f48e631..0c0184b 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -2,6 +2,7 @@ import "./globals.css"; import { NavbarRight } from "@/components/NavbarRight"; import type { Metadata } from "next"; import { Inter } from "next/font/google"; +import { Toaster } from "sonner"; const inter = Inter({ subsets: ["latin"] }); @@ -29,6 +30,7 @@ export default function RootLayout({
{children}
+ diff --git a/web/src/app/machines/page.tsx b/web/src/app/machines/page.tsx index 126b2af..6cc64a3 100644 --- a/web/src/app/machines/page.tsx +++ b/web/src/app/machines/page.tsx @@ -1,14 +1,8 @@ import { MachineList } from "@/components/MachineList"; -import { WorkflowList } from "@/components/WorkflowList"; import { db } from "@/db/db"; -import { - machinesTable, - usersTable, - workflowTable, - workflowVersionTable, -} from "@/db/schema"; +import { machinesTable, usersTable } from "@/db/schema"; import { auth, clerkClient } from "@clerk/nextjs"; -import { desc, eq, sql } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; export default function Page() { return ; diff --git a/web/src/components/MachineList.tsx b/web/src/components/MachineList.tsx index b7caff4..858becd 100644 --- a/web/src/components/MachineList.tsx +++ b/web/src/components/MachineList.tsx @@ -56,6 +56,7 @@ 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 = { @@ -159,9 +160,8 @@ export const columns: ColumnDef[] = [ Actions { - deleteMachine(workflow.id); - // navigator.clipboard.writeText(payment.id) + onClick={async () => { + callServerWithToast(await deleteMachine(workflow.id)); }} > Delete Machine @@ -176,6 +176,17 @@ export const columns: ColumnDef[] = [ }, ]; +async function callServerWithToast(result: { + message: string; + error?: boolean; +}) { + if (result.error) { + toast.error(result.message); + } else { + toast.success(result.message); + } +} + export function MachineList({ data }: { data: Machine[] }) { const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState( @@ -333,8 +344,8 @@ function AddMachinesDialog() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - name: "", - endpoint: "", + name: "My Local Machine", + endpoint: "http://127.0.0.1:8188", }, }); diff --git a/web/src/components/MachinesWS.tsx b/web/src/components/MachinesWS.tsx index 9d2ea04..bbdd5c3 100644 --- a/web/src/components/MachinesWS.tsx +++ b/web/src/components/MachinesWS.tsx @@ -8,6 +8,7 @@ import { create } from "zustand"; type State = { data: { id: string; + timestamp: number; json: { event: string; data: any; @@ -27,7 +28,7 @@ export const useStore = create((set) => ({ addData: (id, json) => set((state) => ({ ...state, - data: [...state.data, { id, json }], + data: [...state.data, { id, json, timestamp: Date.now() }], })), })); diff --git a/web/src/components/RunDisplay.tsx b/web/src/components/RunDisplay.tsx index 4f47465..ce25594 100644 --- a/web/src/components/RunDisplay.tsx +++ b/web/src/components/RunDisplay.tsx @@ -1,27 +1,123 @@ "use client"; -import type { findAllRuns } from "../app/[workflow_id]/page"; -import { StatusBadge } from "../app/[workflow_id]/page"; import { useStore } from "@/components/MachinesWS"; -import { TableCell, TableRow } from "@/components/ui/table"; +import { StatusBadge } from "@/components/StatusBadge"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; import { getRelativeTime } from "@/lib/getRelativeTime"; +import { type findAllRuns } from "@/server/findAllRuns"; +import { getRunsOutput } from "@/server/getRunsOutput"; +import { useEffect, useState } from "react"; export function RunDisplay({ run, }: { run: Awaited>[0]; }) { - const data = useStore((state) => state.data.find((x) => x.id === run.id)); + const data = useStore( + (state) => + state.data + .filter((x) => x.id === run.id) + .sort((a, b) => b.timestamp - a.timestamp)?.[0] + ); + + let status = run.status; + + if (data?.json.event == "executing" && data.json.data.node == undefined) { + status = "success"; + } else if (data?.json.event == "executing") { + status = "running"; + } return ( - - {run.version.version} - {run.machine.name} - {getRelativeTime(run.created_at)} - {data ? data.json.event : "-"} - - - - + + + + {run.version?.version} + {run.machine?.name} + {getRelativeTime(run.created_at)} + + {data && status != "success" + ? `${data.json.event} - ${data.json.data.node}` + : "-"} + + + + + + + + + Run outputs + + You can view your run's outputs here + + + + + ); } + +export function RunOutputs({ run_id }: { run_id: string }) { + const [outputs, setOutputs] = useState< + Awaited> + >([]); + + useEffect(() => { + getRunsOutput(run_id).then((x) => setOutputs(x)); + }, [run_id]); + + return ( + + {/* A list of your recent runs. */} + + + File + Output + + + + {outputs?.map((run) => { + const fileName = run.data.images[0].filename; + // const filePath + return ( + + {fileName} + + + + + ); + })} + +
+ ); +} + +export function OutputRender(props: { run_id: string; filename: string }) { + if (props.filename.endsWith(".png")) { + return ( + {props.filename} + ); + } +} diff --git a/web/src/components/RunsTable.tsx b/web/src/components/RunsTable.tsx new file mode 100644 index 0000000..f96a72b --- /dev/null +++ b/web/src/components/RunsTable.tsx @@ -0,0 +1,35 @@ +import { findAllRuns } from "../server/findAllRuns"; +import { RunDisplay } from "./RunDisplay"; +import { + Table, + TableBody, + TableCaption, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +export async function RunsTable(props: { workflow_id: string }) { + const allRuns = await findAllRuns(props.workflow_id); + return ( +
+ + A list of your recent runs. + + + Version + Machine + Time + Live Status + Status + + + + {allRuns.map((run) => ( + + ))} + +
+
+ ); +} diff --git a/web/src/components/StatusBadge.tsx b/web/src/components/StatusBadge.tsx new file mode 100644 index 0000000..cacbe75 --- /dev/null +++ b/web/src/components/StatusBadge.tsx @@ -0,0 +1,23 @@ +import type { findAllRuns } from "../server/findAllRuns"; +import { LoadingIcon } from "@/components/LoadingIcon"; +import { Badge } from "@/components/ui/badge"; + +export function StatusBadge({ + status, +}: { + status: Awaited>[0]["status"]; +}) { + switch (status) { + case "running": + return ( + + {status} + + ); + case "success": + return {status}; + case "failed": + return {status}; + } + return {status}; +} diff --git a/web/src/components/VersionSelect.tsx b/web/src/components/VersionSelect.tsx index f2cb7d2..c02b05f 100644 --- a/web/src/components/VersionSelect.tsx +++ b/web/src/components/VersionSelect.tsx @@ -1,6 +1,5 @@ "use client"; -import type { findFirstTableWithVersion } from "@/app/[workflow_id]/page"; import { LoadingIcon } from "@/components/LoadingIcon"; import { Button } from "@/components/ui/button"; import { @@ -14,6 +13,7 @@ import { } from "@/components/ui/select"; import { createRun } from "@/server/createRun"; import type { getMachines } from "@/server/curdMachine"; +import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion"; import { Play } from "lucide-react"; import { parseAsInteger, useQueryState } from "next-usequerystate"; import { useState } from "react"; @@ -56,7 +56,7 @@ export function MachineSelect({ machines: Awaited>; }) { const [machine, setMachine] = useQueryState("machine", { - defaultValue: machines[0].id ?? "", + defaultValue: machines?.[0].id ?? "", }); return (