diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index bffb357..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "extends": "next/core-web-vitals"
-}
diff --git a/.gitignore b/.gitignore
index fd3dbb5..ed8ebf5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,36 +1 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-.yarn/install-state.gz
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# local env files
-.env*.local
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
+__pycache__
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
new file mode 100644
index 0000000..74cae7d
--- /dev/null
+++ b/.vscode/extensions.json
@@ -0,0 +1,14 @@
+{
+ "recommendations": [
+ "DavidAnson.vscode-markdownlint", // markdown linting
+ "yzhang.markdown-all-in-one", // nicer markdown support
+ "esbenp.prettier-vscode", // prettier plugin
+ "dbaeumer.vscode-eslint", // eslint plugin
+ "bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
+ "ban.spellright", // Spell check for docs
+ "stripe.vscode-stripe", // stripe VSCode extension
+ "Prisma.prisma", // syntax|format|completion for prisma
+ "rebornix.project-snippets", // Share useful snippets between collaborators
+ "inlang.vs-code-extension" // improved i18n DX
+ ]
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..fb52ab5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,17 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "editor.formatOnSave": false,
+ "editor.codeActionsOnSave": {
+ "source.fixAll.eslint": true
+ },
+ "typescript.preferences.importModuleSpecifier": "non-relative",
+ "spellright.language": ["en"],
+ "spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
+ "tailwindCSS.experimental.classRegex": [
+ [
+ "cva\\(([^)]*)\\)",
+ "[\"'`]([^\"'`]*).*?[\"'`]"
+ ]
+ ],
+ "eslint.workingDirectories": ["web"]
+}
diff --git a/__init__.py b/__init__.py
new file mode 100644
index 0000000..a4d13f9
--- /dev/null
+++ b/__init__.py
@@ -0,0 +1,52 @@
+"""
+@author: BennyKok
+@title: comfy-deploy
+@nickname: Comfy Deploy
+@description:
+"""
+import os
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__)))
+
+import routes
+import inspect
+import sys
+import importlib
+import subprocess
+import requests
+import folder_paths
+from folder_paths import add_model_folder_path, get_filename_list, get_folder_paths
+from tqdm import tqdm
+
+ag_path = os.path.join(os.path.dirname(__file__))
+
+def get_python_files(path):
+ return [f[:-3] for f in os.listdir(path) if f.endswith(".py")]
+
+def append_to_sys_path(path):
+ if path not in sys.path:
+ sys.path.append(path)
+
+paths = ["comfy-nodes"]
+files = []
+
+for path in paths:
+ full_path = os.path.join(ag_path, path)
+ append_to_sys_path(full_path)
+ files.extend(get_python_files(full_path))
+
+NODE_CLASS_MAPPINGS = {}
+NODE_DISPLAY_NAME_MAPPINGS = {}
+
+# Import all the modules and append their mappings
+for file in files:
+ module = importlib.import_module(file)
+
+ if hasattr(module, "NODE_CLASS_MAPPINGS"):
+ NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
+ if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS"):
+ NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
+
+WEB_DIRECTORY = "web-plugin"
+__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
diff --git a/bun.lockb b/bun.lockb
deleted file mode 100755
index 30bf970..0000000
Binary files a/bun.lockb and /dev/null differ
diff --git a/package.json b/package.json
deleted file mode 100644
index 9d52629..0000000
--- a/package.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "name": "comfy-deploy",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "react": "^18",
- "react-dom": "^18",
- "next": "14.0.3"
- },
- "devDependencies": {
- "typescript": "^5",
- "@types/node": "^20",
- "@types/react": "^18",
- "@types/react-dom": "^18",
- "autoprefixer": "^10.0.1",
- "postcss": "^8",
- "tailwindcss": "^3.3.0",
- "eslint": "^8",
- "eslint-config-next": "14.0.3"
- }
-}
diff --git a/routes.py b/routes.py
new file mode 100644
index 0000000..942a809
--- /dev/null
+++ b/routes.py
@@ -0,0 +1,22 @@
+from aiohttp import web
+from dotenv import load_dotenv
+import os
+import requests
+import folder_paths
+import json
+import numpy as np
+import server
+import re
+import base64
+from PIL import Image
+import io
+import time
+import execution
+import random
+
+load_dotenv()
+
+@server.PromptServer.instance.routes.get("/comfy-deploy/run")
+async def get_web_styles(request):
+ filename = os.path.join(os.path.dirname(__file__), "js/tw-styles.css")
+ return web.FileResponse(filename)
\ No newline at end of file
diff --git a/src/app/globals.css b/src/app/globals.css
deleted file mode 100644
index fd81e88..0000000
--- a/src/app/globals.css
+++ /dev/null
@@ -1,27 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
- }
-}
-
-body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
-}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
deleted file mode 100644
index 40e027f..0000000
--- a/src/app/layout.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { Metadata } from 'next'
-import { Inter } from 'next/font/google'
-import './globals.css'
-
-const inter = Inter({ subsets: ['latin'] })
-
-export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
-}
-
-export default function RootLayout({
- children,
-}: {
- children: React.ReactNode
-}) {
- return (
-
-
{children}
-
- )
-}
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index b973266..0000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import Image from 'next/image'
-
-export default function Home() {
- return (
-
-
-
- Get started by editing
- src/app/page.tsx
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/tailwind.config.ts b/tailwind.config.ts
deleted file mode 100644
index 1af3b8f..0000000
--- a/tailwind.config.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Config } from 'tailwindcss'
-
-const config: Config = {
- content: [
- './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
- './src/components/**/*.{js,ts,jsx,tsx,mdx}',
- './src/app/**/*.{js,ts,jsx,tsx,mdx}',
- ],
- theme: {
- extend: {
- backgroundImage: {
- 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
- 'gradient-conic':
- 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
- },
- },
- },
- plugins: [],
-}
-export default config
diff --git a/web-plugin/api.js b/web-plugin/api.js
new file mode 100644
index 0000000..f152d28
--- /dev/null
+++ b/web-plugin/api.js
@@ -0,0 +1,4 @@
+/** @typedef {import('../../../web/scripts/api.js').api} API*/
+import { api as _api } from '../../scripts/api.js';
+/** @type {API} */
+export const api = _api;
diff --git a/web-plugin/app.js b/web-plugin/app.js
new file mode 100644
index 0000000..53ccc05
--- /dev/null
+++ b/web-plugin/app.js
@@ -0,0 +1,4 @@
+/** @typedef {import('../../../web/scripts/app.js').ComfyApp} ComfyApp*/
+import { app as _app } from '../../scripts/app.js';
+/** @type {ComfyApp} */
+export const app = _app;
diff --git a/web-plugin/index.js b/web-plugin/index.js
new file mode 100644
index 0000000..7bca115
--- /dev/null
+++ b/web-plugin/index.js
@@ -0,0 +1,214 @@
+import { app } from "./app.js";
+import { api } from "./api.js";
+import { ComfyWidgets, LGraphNode } from "./widgets.js";
+
+/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
+/** @type {ComfyExtension} */
+const ext = {
+ name: "BennyKok.ComfyDeploy",
+
+ init(app) {
+ addButton();
+ },
+
+ registerCustomNodes() {
+ /** @type {LGraphNode}*/
+ class ComfyDeploy {
+ color = LGraphCanvas.node_colors.yellow.color;
+ bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
+ groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
+ constructor() {
+ if (!this.properties) {
+ this.properties = {};
+ this.properties.workflow_name = "";
+ this.properties.workflow_id = "";
+ }
+
+ ComfyWidgets.STRING(
+ this,
+ "workflow_name",
+ ["", { default: this.properties.workflow_name, multiline: false }],
+ app,
+ );
+
+ ComfyWidgets.STRING(
+ this,
+ "workflow_id",
+ ["", { default: this.properties.workflow_id, multiline: false }],
+ app,
+ );
+
+ // this.widgets.forEach((w) => {
+ // // w.computeSize = () => [200,10]
+ // w.computedHeight = 2;
+ // })
+
+ this.widgets_start_y = 10;
+ this.setSize(this.computeSize());
+
+ // const config = { };
+
+ // console.log(this);
+ this.serialize_widgets = true;
+ this.isVirtualNode = true;
+ }
+ }
+
+ // Load default visibility
+
+ LiteGraph.registerNodeType(
+ "Comfy Deploy",
+ Object.assign(ComfyDeploy, {
+ title_mode: LiteGraph.NORMAL_TITLE,
+ title: "Comfy Deploy",
+ collapsable: true,
+ }),
+ );
+
+ ComfyDeploy.category = "deploy";
+ },
+
+ async setup() {
+ // const graphCanvas = document.getElementById("graph-canvas");
+
+ window.addEventListener("message", (event) => {
+ if (!event.data.flow || Object.entries(event.data.flow).length <= 0)
+ return;
+ // updateBlendshapesPrompts(event.data.flow);
+ });
+
+ api.addEventListener("executed", (evt) => {
+ const images = evt.detail?.output.images;
+ // if (images?.length > 0 && images[0].type === "output") {
+ // generatedImages[evt.detail.node] = images[0].filename;
+ // }
+ // if (evt.detail?.output.gltfFilename) {
+
+ // }
+ });
+ },
+};
+
+/**
+ * @typedef {import('../../../web/types/litegraph.js').LGraph} LGraph
+ * @typedef {import('../../../web/types/litegraph.js').LGraphNode} LGraphNode
+ */
+
+function addButton() {
+ const menu = document.querySelector(".comfy-menu");
+
+ const deploy = document.createElement("button");
+ deploy.textContent = "Deploy";
+ deploy.className = "sharebtn";
+ deploy.onclick = async () => {
+ /** @type {LGraph} */
+ const graph = app.graph;
+
+ const deployMeta = graph.findNodesByType("Comfy Deploy");
+ const deployMetaNode = deployMeta[0];
+
+ console.log(deployMetaNode);
+
+ const workflow_name = deployMetaNode.widgets[0].value;
+ const workflow_id = deployMetaNode.widgets[1].value;
+
+ console.log(workflow_name, workflow_id);
+
+ const prompt = await app.graphToPrompt();
+ console.log(graph);
+ console.log(prompt);
+
+ deploy.textContent = "Deploying...";
+ deploy.style.color = "orange";
+
+ const apiRoute = "http://localhost:3001/api/upload";
+ const userId = "user_2ZA6vuKD3IJXju16oJVQGLBcWwg";
+ try {
+ let data = await fetch(apiRoute, {
+ method: "POST",
+ body: JSON.stringify({
+ user_id: userId,
+ workflow_name,
+ workflow_id,
+ workflow: prompt.workflow,
+ workflow_api: prompt.output,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ console.log(data);
+
+ if (data.status !== 200) {
+ throw new Error(await data.text());
+ } else {
+ data = await data.json();
+ }
+
+ deploy.textContent = "Done";
+ deploy.style.color = "green";
+
+ deployMetaNode.widgets[1].value = data.workflow_id;
+ graph.change();
+
+ infoDialog.show(
+ `Deployed successfully! Workflow ID: ${data.workflow_id} Workflow Name: ${workflow_name} Workflow Version: ${data.version}`,
+ );
+
+ setTimeout(() => {
+ deploy.textContent = "Deploy";
+ deploy.style.color = "white";
+ }, 1000);
+ } catch (e) {
+ app.ui.dialog.show(e);
+ console.error(e);
+ deploy.textContent = "Error";
+ deploy.style.color = "red";
+ setTimeout(() => {
+ deploy.textContent = "Deploy";
+ deploy.style.color = "white";
+ }, 1000);
+ }
+ };
+
+ menu.append(deploy);
+}
+
+app.registerExtension(ext);
+
+
+import { ComfyDialog, $el } from '../../scripts/ui.js';
+
+export class InfoDialog extends ComfyDialog {
+ constructor() {
+ super();
+ this.element.classList.add("comfy-normal-modal");
+ }
+ createButtons() {
+ return [
+ $el("button", {
+ type: "button",
+ textContent: "Close",
+ onclick: () => this.close(),
+ }),
+ ];
+ }
+
+ close() {
+ this.element.style.display = "none";
+ }
+
+ show(html) {
+ this.textElement.style.color = "white";
+ if (typeof html === "string") {
+ this.textElement.innerHTML = html;
+ } else {
+ this.textElement.replaceChildren(html);
+ }
+ this.element.style.display = "flex";
+ this.element.style.zIndex = 1001;
+ }
+}
+
+export const infoDialog = new InfoDialog()
\ No newline at end of file
diff --git a/web-plugin/widgets.js b/web-plugin/widgets.js
new file mode 100644
index 0000000..a5bd3cf
--- /dev/null
+++ b/web-plugin/widgets.js
@@ -0,0 +1,18 @@
+// /** @typedef {import('../../../web/scripts/api.js').api} API*/
+// import { api as _api } from "../../scripts/api.js";
+// /** @type {API} */
+// export const api = _api;
+
+/** @typedef {typeof import('../../../web/scripts/widgets.js').ComfyWidgets} Widgets*/
+import { ComfyWidgets as _ComfyWidgets } from "../../scripts/widgets.js";
+
+/**
+ * @type {Widgets}
+ */
+export const ComfyWidgets = _ComfyWidgets;
+
+// import { LGraphNode as _LGraphNode } from "../../types/litegraph.js";
+
+/** @typedef {typeof import('../../../web/types/litegraph.js').LGraphNode} LGraphNode*/
+/** @type {LGraphNode}*/
+export const LGraphNode = LiteGraph.LGraphNode;
diff --git a/web/.eslintignore b/web/.eslintignore
new file mode 100644
index 0000000..8fe11da
--- /dev/null
+++ b/web/.eslintignore
@@ -0,0 +1,6 @@
+node_modules
+**/node_modules
+**/.next
+**/public
+packages/prisma/zod
+apps/web/public/embed
diff --git a/web/.eslintrc.js b/web/.eslintrc.js
new file mode 100644
index 0000000..706f9e8
--- /dev/null
+++ b/web/.eslintrc.js
@@ -0,0 +1,94 @@
+/** @type {import("eslint").Linter.Config} */
+module.exports = {
+ root: true,
+ extends: [
+ // "plugin:playwright/playwright-test",
+ "next",
+ "plugin:prettier/recommended",
+ // "turbo",
+ // "plugin:you-dont-need-lodash-underscore/compatible-warn",
+ ],
+ plugins: ["unused-imports"],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: ["./tsconfig.json"],
+ // project: ["./apps/*/tsconfig.json", "./packages/*/tsconfig.json"],
+ },
+ settings: {
+ next: {
+ // rootDir: ["apps/*/", "packages/*/"],
+ rootDir: ["src"],
+ },
+ },
+ rules: {
+ "@next/next/no-img-element": "off",
+ "@next/next/no-html-link-for-pages": "off",
+ "jsx-a11y/role-supports-aria-props": "off", // @see https://github.com/vercel/next.js/issues/27989#issuecomment-897638654
+ // "playwright/no-page-pause": "error",
+ "react/jsx-curly-brace-presence": [
+ "error",
+ { props: "never", children: "never" },
+ ],
+ "react/self-closing-comp": ["error", { component: true, html: true }],
+ "@typescript-eslint/no-unused-vars": [
+ "warn",
+ {
+ vars: "all",
+ varsIgnorePattern: "^_",
+ args: "after-used",
+ argsIgnorePattern: "^_",
+ destructuredArrayIgnorePattern: "^_",
+ },
+ ],
+ "unused-imports/no-unused-imports": "error",
+ "no-restricted-imports": [
+ "error",
+ {
+ patterns: ["lodash"],
+ },
+ ],
+ "prefer-template": "error",
+ },
+ overrides: [
+ {
+ files: ["*.ts", "*.tsx"],
+ extends: [
+ "plugin:@typescript-eslint/recommended",
+ // "plugin:@calcom/eslint/recommended",
+ ],
+ plugins: [
+ "@typescript-eslint",
+ // "@calcom/eslint"
+ ],
+ parser: "@typescript-eslint/parser",
+ rules: {
+ "@typescript-eslint/consistent-type-imports": [
+ "error",
+ {
+ prefer: "type-imports",
+ // TODO: enable this once prettier supports it
+ // fixStyle: "inline-type-imports",
+ fixStyle: "separate-type-imports",
+ disallowTypeAnnotations: false,
+ },
+ ],
+ },
+ // overrides: [
+ // {
+ // files: ["**/playwright/**/*.{tsx,ts}"],
+ // rules: {
+ // "@typescript-eslint/no-unused-vars": "off",
+ // "no-undef": "off",
+ // },
+ // },
+ // ],
+ },
+ // {
+ // files: ["**/playwright/**/*.{js,jsx}"],
+ // rules: {
+ // "@typescript-eslint/no-unused-vars": "off",
+ // "no-undef": "off",
+ // },
+ // },
+ ],
+};
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..fd3dbb5
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,36 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+.yarn/install-state.gz
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/README.md b/web/README.md
similarity index 100%
rename from README.md
rename to web/README.md
diff --git a/web/bun.lockb b/web/bun.lockb
new file mode 100755
index 0000000..6e02745
Binary files /dev/null and b/web/bun.lockb differ
diff --git a/web/components.json b/web/components.json
new file mode 100644
index 0000000..e4e8e1a
--- /dev/null
+++ b/web/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "src/app/globals.css",
+ "baseColor": "slate",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils"
+ }
+}
\ No newline at end of file
diff --git a/web/docker-compose.yml b/web/docker-compose.yml
new file mode 100644
index 0000000..8cce5eb
--- /dev/null
+++ b/web/docker-compose.yml
@@ -0,0 +1,22 @@
+# this file is a helper to run Cal.com locally
+# starts a postgres instance on port 5460 to use as a local db
+version: "3.6"
+services:
+ postgres:
+ image: "postgres:15.2-alpine"
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: verceldb
+ ports:
+ - "5480:5432"
+ pg_proxy:
+ image: ghcr.io/neondatabase/wsproxy:latest
+ environment:
+ APPEND_PORT: "postgres:5432"
+ ALLOW_ADDR_REGEX: ".*"
+ LOG_TRAFFIC: "true"
+ ports:
+ - "5481:80"
+ depends_on:
+ - postgres
\ No newline at end of file
diff --git a/web/drizzle.config.ts b/web/drizzle.config.ts
new file mode 100644
index 0000000..9e8a158
--- /dev/null
+++ b/web/drizzle.config.ts
@@ -0,0 +1,15 @@
+import type { Config } from 'drizzle-kit';
+import { config } from 'dotenv';
+
+config({
+ path: `.env.local`,
+});
+
+export default {
+ schema: './src/db/schema.ts',
+ driver: 'pg',
+ out: './drizzle',
+ dbCredentials: {
+ connectionString: (process.env.POSTGRES_URL as string) + ( process.env.POSTGRES_SSL !== "false" ? '?ssl=true' : ""),
+ },
+} satisfies Config;
diff --git a/web/drizzle/0000_quiet_ulik.sql b/web/drizzle/0000_quiet_ulik.sql
new file mode 100644
index 0000000..04f02cb
--- /dev/null
+++ b/web/drizzle/0000_quiet_ulik.sql
@@ -0,0 +1,75 @@
+CREATE SCHEMA "comfy_deploy";
+--> statement-breakpoint
+DO $$ BEGIN
+ CREATE TYPE "workflow_run_status" AS ENUM('not-started', 'running', 'success', 'failed');
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "comfy_deploy"."machines" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" uuid,
+ "name" text NOT NULL,
+ "endpoint" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "comfy_deploy"."users" (
+ "id" text PRIMARY KEY NOT NULL,
+ "username" text NOT NULL,
+ "name" text NOT NULL,
+ "created_at" timestamp DEFAULT now(),
+ "updated_at" timestamp DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "comfy_deploy"."workflow_runs" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "workflow_version_id" uuid NOT NULL,
+ "machine_id" uuid NOT NULL,
+ "status" "workflow_run_status" DEFAULT 'not-started' NOT NULL,
+ "ended_at" timestamp,
+ "created_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "comfy_deploy"."workflows" (
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "user_id" text NOT NULL,
+ "name" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "comfy_deploy"."workflow_versions" (
+ "workflow_id" uuid NOT NULL,
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
+ "workflow" jsonb,
+ "workflow_api" jsonb,
+ "version" integer NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> 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 no action 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_machine_id_machines_id_fk" FOREIGN KEY ("machine_id") REFERENCES "comfy_deploy"."machines"("id") ON DELETE no action ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "comfy_deploy"."workflows" ADD CONSTRAINT "workflows_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_versions" ADD CONSTRAINT "workflow_versions_workflow_id_workflows_id_fk" FOREIGN KEY ("workflow_id") REFERENCES "comfy_deploy"."workflows"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
diff --git a/web/drizzle/0001_silly_arachne.sql b/web/drizzle/0001_silly_arachne.sql
new file mode 100644
index 0000000..781fa51
--- /dev/null
+++ b/web/drizzle/0001_silly_arachne.sql
@@ -0,0 +1,6 @@
+ALTER TABLE "comfy_deploy"."machines" ALTER COLUMN "user_id" SET DATA TYPE text;--> 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 no action ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;
diff --git a/web/drizzle/0002_clean_khan.sql b/web/drizzle/0002_clean_khan.sql
new file mode 100644
index 0000000..6a145bb
--- /dev/null
+++ b/web/drizzle/0002_clean_khan.sql
@@ -0,0 +1 @@
+ALTER TABLE "comfy_deploy"."machines" ALTER COLUMN "user_id" SET NOT NULL;
\ No newline at end of file
diff --git a/web/drizzle/meta/0000_snapshot.json b/web/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..d3f8153
--- /dev/null
+++ b/web/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,320 @@
+{
+ "id": "c9cde314-1f93-4e4c-a010-e31a1edd4cee",
+ "prevId": "00000000-0000-0000-0000-000000000000",
+ "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": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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": {},
+ "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_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": true
+ },
+ "machine_id": {
+ "name": "machine_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "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": "no action",
+ "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": "no action",
+ "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/0001_snapshot.json b/web/drizzle/meta/0001_snapshot.json
new file mode 100644
index 0000000..6816c77
--- /dev/null
+++ b/web/drizzle/meta/0001_snapshot.json
@@ -0,0 +1,334 @@
+{
+ "id": "3f100d2d-1d9c-46fb-8914-6e8c5a228142",
+ "prevId": "c9cde314-1f93-4e4c-a010-e31a1edd4cee",
+ "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": false
+ },
+ "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": "no action",
+ "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_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": true
+ },
+ "machine_id": {
+ "name": "machine_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "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": "no action",
+ "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": "no action",
+ "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/0002_snapshot.json b/web/drizzle/meta/0002_snapshot.json
new file mode 100644
index 0000000..e9a4e4f
--- /dev/null
+++ b/web/drizzle/meta/0002_snapshot.json
@@ -0,0 +1,334 @@
+{
+ "id": "a5fcd4b7-d6f7-4fcc-a826-0d4ed8d3c7dc",
+ "prevId": "3f100d2d-1d9c-46fb-8914-6e8c5a228142",
+ "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": "no action",
+ "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_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": true
+ },
+ "machine_id": {
+ "name": "machine_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "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": "no action",
+ "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": "no action",
+ "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
new file mode 100644
index 0000000..59e0c91
--- /dev/null
+++ b/web/drizzle/meta/_journal.json
@@ -0,0 +1,27 @@
+{
+ "version": "5",
+ "dialect": "pg",
+ "entries": [
+ {
+ "idx": 0,
+ "version": "5",
+ "when": 1702033790765,
+ "tag": "0000_quiet_ulik",
+ "breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "5",
+ "when": 1702034015507,
+ "tag": "0001_silly_arachne",
+ "breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "5",
+ "when": 1702044546478,
+ "tag": "0002_clean_khan",
+ "breakpoints": true
+ }
+ ]
+}
\ No newline at end of file
diff --git a/web/migrate.mts b/web/migrate.mts
new file mode 100644
index 0000000..d989afe
--- /dev/null
+++ b/web/migrate.mts
@@ -0,0 +1,41 @@
+const { drizzle } = await import("drizzle-orm/postgres-js");
+const { migrate } = await import("drizzle-orm/postgres-js/migrator");
+const { default: postgres } = await import("postgres");
+
+import { config } from "dotenv";
+config({
+ path: ".local.env",
+});
+
+const migrationsFolderName = process.env.MIGRATIONS_FOLDER || "drizzle";
+let sslMode: string | boolean = process.env.SSL || "require";
+
+if (sslMode === "false") sslMode = false;
+
+console.log(migrationsFolderName, sslMode);
+
+const connectionString = process.env.POSTGRES_URL!;
+console.log(connectionString);
+const sql = postgres(connectionString, { max: 1, ssl: sslMode as any });
+const db = drizzle(sql, {
+ logger: true,
+});
+
+let retries = 5;
+while(retries) {
+ try {
+ await sql`SELECT NOW()`;
+ console.log('Database is live');
+ break;
+ } catch (error) {
+ console.error('Database is not live yet', error);
+ retries -= 1;
+ console.log(`Retries left: ${retries}`);
+ await new Promise(res => setTimeout(res, 1000));
+ }
+}
+
+console.log("Migrating...");
+await migrate(db, { migrationsFolder: migrationsFolderName });
+console.log("Done!");
+process.exit();
diff --git a/next.config.js b/web/next.config.js
similarity index 100%
rename from next.config.js
rename to web/next.config.js
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..9ff1693
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "comfy-deploy",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "dev:all": "concurrently \"bun run db-up\" \"bun run migrate-local\" \"next dev\"",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint",
+ "generate": "bunx drizzle-kit generate:pg",
+ "migrate-production": "cp .env.local .env.local.bak && vercel env pull --environment=production && bun run migrate.mts && mv .env.local.bak .env.local",
+ "migrate-local": "SSL=false LOCAL=true bun run migrate.mts",
+ "db-up": "docker-compose up",
+ "db-dev": "bun run db-up && bun run migrate-local"
+ },
+ "dependencies": {
+ "@clerk/nextjs": "^4.27.4",
+ "@hookform/resolvers": "^3.3.2",
+ "@neondatabase/serverless": "^0.6.0",
+ "@radix-ui/react-checkbox": "^1.0.4",
+ "@radix-ui/react-dialog": "^1.0.5",
+ "@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-select": "^2.0.0",
+ "@radix-ui/react-slot": "^1.0.2",
+ "@radix-ui/react-tabs": "^1.0.4",
+ "@tanstack/react-table": "^8.10.7",
+ "class-variance-authority": "^0.7.0",
+ "clsx": "^2.0.0",
+ "dayjs": "^1.11.10",
+ "drizzle-orm": "^0.29.1",
+ "eslint-config-next": "14.0.3",
+ "eslint-config-prettier": "^8.6.0",
+ "eslint-config-turbo": "latest",
+ "eslint-plugin-prettier": "4.2.1",
+ "lucide-react": "^0.294.0",
+ "next": "14.0.3",
+ "prettier-plugin-tailwindcss": "0.2.5",
+ "react": "^18",
+ "react-dom": "^18",
+ "react-hook-form": "^7.48.2",
+ "tailwind-merge": "^2.1.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.22.4"
+ },
+ "devDependencies": {
+ "@trivago/prettier-plugin-sort-imports": "4.1.1",
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "@typescript-eslint/eslint-plugin": "^6.13.2",
+ "@typescript-eslint/parser": "^6.13.2",
+ "autoprefixer": "^10.0.1",
+ "concurrently": "^8.2.2",
+ "dotenv": "^16.3.1",
+ "drizzle-kit": "^0.20.6",
+ "eslint": "8.34.0",
+ "eslint-plugin-unused-imports": "^3.0.0",
+ "postcss": "^8",
+ "postgres": "^3.4.3",
+ "prettier": "2.8.6",
+ "tailwindcss": "^3.3.0",
+ "typescript": "^5"
+ }
+}
\ No newline at end of file
diff --git a/postcss.config.js b/web/postcss.config.js
similarity index 96%
rename from postcss.config.js
rename to web/postcss.config.js
index 33ad091..12a703d 100644
--- a/postcss.config.js
+++ b/web/postcss.config.js
@@ -3,4 +3,4 @@ module.exports = {
tailwindcss: {},
autoprefixer: {},
},
-}
+};
diff --git a/web/prettier-preset.js b/web/prettier-preset.js
new file mode 100644
index 0000000..91fa034
--- /dev/null
+++ b/web/prettier-preset.js
@@ -0,0 +1,39 @@
+module.exports = {
+ bracketSpacing: true,
+ bracketSameLine: true,
+ singleQuote: false,
+ jsxSingleQuote: false,
+ trailingComma: "es5",
+ semi: true,
+ printWidth: 110,
+ arrowParens: "always",
+ endOfLine: "auto",
+ importOrder: [
+ // Mocks must be at the top as they contain vi.mock calls
+ "(.*)/__mocks__/(.*)",
+ "",
+ "^@(calcom|ee)/(.*)$",
+ "^@lib/(.*)$",
+ "^@components/(.*)$",
+ "^@(server|trpc)/(.*)$",
+ "^~/(.*)$",
+ "^[./]",
+ ],
+ importOrderSeparation: true,
+ plugins: [
+ "@trivago/prettier-plugin-sort-imports",
+ /**
+ * **NOTE** tailwind plugin must come last!
+ * @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
+ */
+ "prettier-plugin-tailwindcss",
+ ],
+ // overrides: [
+ // {
+ // files: ["apps/website/lib/utils/wordlist/wordlist.ts"],
+ // options: {
+ // quoteProps: "consistent",
+ // },
+ // },
+ // ],
+};
diff --git a/public/next.svg b/web/public/next.svg
similarity index 100%
rename from public/next.svg
rename to web/public/next.svg
diff --git a/public/vercel.svg b/web/public/vercel.svg
similarity index 100%
rename from public/vercel.svg
rename to web/public/vercel.svg
diff --git a/web/src/app/[workflow_id]/page.tsx b/web/src/app/[workflow_id]/page.tsx
new file mode 100644
index 0000000..0b3d669
--- /dev/null
+++ b/web/src/app/[workflow_id]/page.tsx
@@ -0,0 +1,64 @@
+import { MachineSelect, VersionSelect } from "@/components/VersionSelect";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { db } from "@/db/db";
+import { workflowTable, workflowVersionTable } from "@/db/schema";
+import { getRelativeTime } from "@/lib/getRelativeTime";
+import { getMachines } from "@/server/curdMachine";
+import { desc, eq, sql } from "drizzle-orm";
+import { Play } from "lucide-react";
+
+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 default async function Page({
+ params,
+}: {
+ params: { workflow_id: string };
+}) {
+ const workflow_id = params.workflow_id;
+
+ const workflow = await findFirstTableWithVersion(workflow_id);
+ const machines = await getMachines();
+
+ return (
+
+
+
+ {workflow?.name}
+
+ {getRelativeTime(workflow?.updated_at)}
+
+
+
+
+
+
+
+
+
+
+ Run
+
+
+
+
+
+ );
+}
diff --git a/web/src/app/api/create-run/route.ts b/web/src/app/api/create-run/route.ts
new file mode 100644
index 0000000..0ea8c3b
--- /dev/null
+++ b/web/src/app/api/create-run/route.ts
@@ -0,0 +1,56 @@
+import { parseDataSafe } from "../../../lib/parseDataSafe";
+import { db } from "@/db/db";
+import {
+ workflowRunStatus,
+ workflowRunsTable,
+ workflowTable,
+ workflowVersionTable,
+} from "@/db/schema";
+import { eq, sql } from "drizzle-orm";
+import { NextResponse } from "next/server";
+import { ZodFormattedError, z } from "zod";
+
+const Request = z.object({
+ workflow_version_id: z.string(),
+ machine_id: z.string(),
+});
+
+export async function OPTIONS(request: Request) {
+ return new Response(null, {
+ status: 204,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ },
+ });
+}
+
+export async function POST(request: Request) {
+ const [data, error] = await parseDataSafe(Request, request);
+ if (!data || error) return error;
+
+ let { workflow_version_id, machine_id } = data;
+
+ const workflow_run = await db
+ .insert(workflowRunsTable)
+ .values({
+ workflow_version_id,
+ machine_id,
+ })
+ .returning();
+
+ return NextResponse.json(
+ {
+ workflow_run_id: workflow_run[0].id,
+ },
+ {
+ status: 200,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ },
+ },
+ );
+}
diff --git a/web/src/app/api/update-run/route.ts b/web/src/app/api/update-run/route.ts
new file mode 100644
index 0000000..b5c003a
--- /dev/null
+++ b/web/src/app/api/update-run/route.ts
@@ -0,0 +1,55 @@
+import { parseDataSafe } from "../../../lib/parseDataSafe";
+import { db } from "@/db/db";
+import {
+ workflowRunStatus,
+ workflowRunsTable,
+ workflowTable,
+ workflowVersionTable,
+} from "@/db/schema";
+import { eq, sql } from "drizzle-orm";
+import { NextResponse } from "next/server";
+import { ZodFormattedError, z } from "zod";
+
+const Request = z.object({
+ run_id: z.string(),
+ status: z.enum(["not-started", "running", "success", "failed"]),
+});
+
+export async function OPTIONS(request: Request) {
+ return new Response(null, {
+ status: 204,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ },
+ });
+}
+
+export async function POST(request: Request) {
+ const [data, error] = await parseDataSafe(Request, request);
+ if (!data || error) return error;
+
+ let { run_id, status } = data;
+
+ const workflow_run = await db
+ .update(workflowRunsTable)
+ .set({
+ status: status,
+ })
+ .where(eq(workflowRunsTable.id, run_id));
+
+ return NextResponse.json(
+ {
+ message: "success",
+ },
+ {
+ status: 200,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ },
+ },
+ );
+}
diff --git a/web/src/app/api/upload/route.ts b/web/src/app/api/upload/route.ts
new file mode 100644
index 0000000..c27be52
--- /dev/null
+++ b/web/src/app/api/upload/route.ts
@@ -0,0 +1,125 @@
+import { parseDataSafe } from "../../../lib/parseDataSafe";
+import { db } from "@/db/db";
+import { workflowTable, workflowVersionTable } from "@/db/schema";
+import { eq, sql } from "drizzle-orm";
+import { NextResponse } from "next/server";
+import { ZodFormattedError, z } from "zod";
+
+const corsHeaders = {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+};
+
+const UploadRequest = z.object({
+ user_id: z.string(),
+ workflow_id: z.string().optional(),
+ workflow_name: z.string().optional(),
+ workflow: z.any(),
+ workflow_api: z.any(),
+});
+
+export async function OPTIONS(request: Request) {
+ return new Response(null, {
+ status: 204,
+ headers: {
+ "Access-Control-Allow-Origin": "*",
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
+ },
+ });
+}
+
+export async function POST(request: Request) {
+ console.log("hi");
+
+ const [data, error] = await parseDataSafe(
+ UploadRequest,
+ request,
+ corsHeaders,
+ );
+
+ if (!data || error) return error;
+
+ let { user_id, workflow, workflow_api, workflow_id, workflow_name } = data;
+
+ let version = -1;
+
+ // Case 1 new workflow
+ try {
+ if ((!workflow_id || workflow_id.length == 0) && workflow_name) {
+ // Create a new parent workflow
+ const workflow = await db
+ .insert(workflowTable)
+ .values({
+ user_id,
+ name: workflow_name,
+ })
+ .returning();
+
+ workflow_id = workflow[0].id;
+
+ // Create a new version
+ const data = await db
+ .insert(workflowVersionTable)
+ .values({
+ workflow_id: workflow_id,
+ workflow,
+ workflow_api,
+ version: 1,
+ })
+ .returning();
+ version = data[0].version;
+ } else if (workflow_id) {
+ // Case 2 update workflow
+ const data = await db
+ .insert(workflowVersionTable)
+ .values({
+ workflow_id,
+ workflow,
+ workflow_api,
+ // version: sql`${workflowVersionTable.version} + 1`,
+ version: sql`(
+ SELECT COALESCE(MAX(version), 0) + 1
+ FROM ${workflowVersionTable}
+ WHERE workflow_id = ${workflow_id}
+ )`,
+ })
+ .returning();
+ version = data[0].version;
+ } else {
+ return NextResponse.json(
+ {
+ error: "Invalid request, missing either workflow_id or name",
+ },
+ {
+ status: 500,
+ statusText: "Invalid request",
+ headers: corsHeaders,
+ },
+ );
+ }
+ } catch (error: any) {
+ return NextResponse.json(
+ {
+ error: error.toString(),
+ },
+ {
+ status: 500,
+ statusText: "Invalid request",
+ headers: corsHeaders,
+ },
+ );
+ }
+
+ return NextResponse.json(
+ {
+ workflow_id: workflow_id,
+ version: version,
+ },
+ {
+ status: 200,
+ headers: corsHeaders,
+ },
+ );
+}
diff --git a/src/app/favicon.ico b/web/src/app/favicon.ico
similarity index 100%
rename from src/app/favicon.ico
rename to web/src/app/favicon.ico
diff --git a/web/src/app/globals.css b/web/src/app/globals.css
new file mode 100644
index 0000000..6a75725
--- /dev/null
+++ b/web/src/app/globals.css
@@ -0,0 +1,76 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
\ No newline at end of file
diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx
new file mode 100644
index 0000000..f48e631
--- /dev/null
+++ b/web/src/app/layout.tsx
@@ -0,0 +1,36 @@
+import "./globals.css";
+import { NavbarRight } from "@/components/NavbarRight";
+import type { Metadata } from "next";
+import { Inter } from "next/font/google";
+
+const inter = Inter({ subsets: ["latin"] });
+
+export const metadata: Metadata = {
+ title: "Comfy Deploy",
+ description: "Generated by create next app",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/web/src/app/machines/page.tsx b/web/src/app/machines/page.tsx
new file mode 100644
index 0000000..126b2af
--- /dev/null
+++ b/web/src/app/machines/page.tsx
@@ -0,0 +1,67 @@
+import { MachineList } from "@/components/MachineList";
+import { WorkflowList } from "@/components/WorkflowList";
+import { db } from "@/db/db";
+import {
+ machinesTable,
+ usersTable,
+ workflowTable,
+ workflowVersionTable,
+} from "@/db/schema";
+import { auth, clerkClient } from "@clerk/nextjs";
+import { desc, eq, sql } from "drizzle-orm";
+
+export default function Page() {
+ return ;
+}
+
+async function MachineListServer() {
+ const { userId } = await auth();
+
+ if (!userId) {
+ return No auth
;
+ }
+
+ const workflow = await db.query.machinesTable.findMany({
+ orderBy: desc(machinesTable.updated_at),
+ where: eq(machinesTable.user_id, userId),
+ });
+
+ return (
+
+ {/*
Machines
*/}
+
{
+ return {
+ id: x.id,
+ name: x.name,
+ date: x.updated_at,
+ endpoint: x.endpoint,
+ };
+ })}
+ />
+
+ );
+}
+
+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/app/page.tsx b/web/src/app/page.tsx
new file mode 100644
index 0000000..1ceb9a4
--- /dev/null
+++ b/web/src/app/page.tsx
@@ -0,0 +1,77 @@
+import { WorkflowList } from "@/components/WorkflowList";
+import { db } from "@/db/db";
+import { usersTable, workflowTable, workflowVersionTable } from "@/db/schema";
+import { auth, clerkClient } from "@clerk/nextjs";
+import { desc, eq, sql } from "drizzle-orm";
+
+export default function Home() {
+ return ;
+}
+
+async function WorkflowServer() {
+ const { userId } = await auth();
+
+ if (!userId) {
+ return No auth
;
+ }
+
+ const user = await db.query.usersTable.findFirst({
+ where: eq(usersTable.id, userId),
+ });
+
+ if (!user) {
+ await setInitialUserData(userId);
+ }
+
+ const workflow = await db.query.workflowTable.findMany({
+ // extras: {
+ // count: sql`(select count(*) from ${workflowVersionTable})`.as(
+ // "count",
+ // ),
+ // },
+ with: {
+ versions: {
+ limit: 1,
+ orderBy: desc(workflowVersionTable.version),
+ },
+ },
+ orderBy: desc(workflowTable.updated_at),
+ where: eq(workflowTable.user_id, userId),
+ });
+
+ return (
+ {
+ return {
+ id: x.id,
+ email: x.name,
+ amount: x.versions[0]?.version ?? 0,
+ date: x.updated_at,
+ };
+ })}
+ />
+ );
+}
+
+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/MachineList.tsx b/web/src/components/MachineList.tsx
new file mode 100644
index 0000000..67b3556
--- /dev/null
+++ b/web/src/components/MachineList.tsx
@@ -0,0 +1,425 @@
+"use client";
+
+import { getRelativeTime } from "../lib/getRelativeTime";
+import {
+ FormControl,
+ FormDescription,
+ 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,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { addMachine, deleteMachine } from "@/server/curdMachine";
+import { deleteWorkflow } from "@/server/deleteWorkflow";
+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,
+ ChevronDown,
+ LoaderIcon,
+ MoreHorizontal,
+} from "lucide-react";
+import * as React from "react";
+import { useFormStatus } from "react-dom";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+export type Machine = {
+ 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 (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ >
+ Name
+
+
+ );
+ },
+ 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 (
+ column.toggleSorting(column.getIsSorted() === "asc")}
+ >
+ Update Date
+
+
+ );
+ },
+ cell: ({ row }) => (
+ {getRelativeTime(row.original.date)}
+ ),
+ },
+
+ {
+ id: "actions",
+ enableHiding: false,
+ cell: ({ row }) => {
+ const workflow = row.original;
+
+ return (
+
+
+
+ Open menu
+
+
+
+
+ Actions
+ {
+ deleteMachine(workflow.id);
+ // navigator.clipboard.writeText(payment.id)
+ }}
+ >
+ Delete Machine
+
+ {/*
+ View customer
+ View payment details */}
+
+
+ );
+ },
+ },
+];
+
+export function MachineList({ data }: { data: Machine[] }) {
+ 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"
+ />
+
+
+ {/*
+
+
+ Columns
+
+
+
+ {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.
+
+
+ table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+ Previous
+
+ table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+ Next
+
+
+
+
+ );
+}
+
+const formSchema = z.object({
+ name: z.string().min(1),
+ endpoint: z.string().min(1),
+});
+
+function AddMachinesDialog() {
+ const [open, setOpen] = React.useState(false);
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: "",
+ endpoint: "",
+ },
+ });
+
+ return (
+
+
+
+ Add Machines
+
+
+
+