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 -

-
- - By{' '} - Vercel Logo - -
-
- -
- Next.js Logo -
- -
- -

- Docs{' '} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{' '} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{' '} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{' '} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
-
- ) -} 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 ( + + +
+
+ + Comfy Deploy + + + {/*
*/} +
+
+ {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 ( + + ); + }, + cell: ({ row }) => { + return ( + // + row.getValue("name") + // + ); + }, + }, + { + accessorKey: "endpoint", + header: () =>
Endpoint
, + cell: ({ row }) => { + return ( +
{row.original.endpoint}
+ ); + }, + }, + { + accessorKey: "date", + sortingFn: "datetime", + enableSorting: true, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
{getRelativeTime(row.original.date)}
+ ), + }, + + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const workflow = row.original; + + return ( + + + + + + Actions + { + 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" + /> +
+ + {/* + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + */} +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+
+ ); +} + +const formSchema = z.object({ + name: z.string().min(1), + endpoint: z.string().min(1), +}); + +function AddMachinesDialog() { + const [open, setOpen] = React.useState(false); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + endpoint: "", + }, + }); + + return ( + + + + + +
+ { + await addMachine(data.name, data.endpoint); + // await new Promise(resolve => setTimeout(resolve, 3000)); + setOpen(false); + })} + > + + Add Machines + + Add Comfyui machines to your account. + + +
+ {/*
*/} + ( + + Name + + + + {/* + This is your public display name. + */} + + + )} + /> + + ( + + Endpoint + + + + {/* + This is your public display name. + */} + + + )} + /> +
+ + + + + + +
+ ); +} + +function AddWorkflowButton({ pending }: { pending: boolean }) { + // const { pending } = useFormStatus(); + return ( + + ); +} diff --git a/web/src/components/NavbarRight.tsx b/web/src/components/NavbarRight.tsx new file mode 100644 index 0000000..91127e9 --- /dev/null +++ b/web/src/components/NavbarRight.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { usePathname } from "next/navigation"; +import { useRouter } from "next/navigation"; + +export function NavbarRight() { + const pathname = usePathname(); + const router = useRouter(); + + return ( + { + if (value === "machines") { + router.push("/machines"); + } else { + router.push("/"); + } + }} + > + + Workflow + Machines + + + ); +} diff --git a/web/src/components/VersionSelect.tsx b/web/src/components/VersionSelect.tsx new file mode 100644 index 0000000..4684e30 --- /dev/null +++ b/web/src/components/VersionSelect.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { findFirstTableWithVersion } from "@/app/[workflow_id]/page"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { getMachines } from "@/server/curdMachine"; + +export function VersionSelect({ + workflow, +}: { + workflow: Awaited>; +}) { + return ( + + ); +} + + +export function MachineSelect({ + machines, + }: { + machines: Awaited>; + }) { + return ( + + ); + } + \ No newline at end of file diff --git a/web/src/components/WorkflowList.tsx b/web/src/components/WorkflowList.tsx new file mode 100644 index 0000000..552c10d --- /dev/null +++ b/web/src/components/WorkflowList.tsx @@ -0,0 +1,309 @@ +"use client"; + +import { getRelativeTime } from "../lib/getRelativeTime"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { deleteWorkflow } from "@/server/deleteWorkflow"; +import type { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, +} from "@tanstack/react-table"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react"; +import * as React from "react"; + +export type Payment = { + id: string; + amount: number; + date: Date; + email: string; +}; + +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: "email", + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => { + return ( + + {row.getValue("email")} + + ); + }, + }, + + { + accessorKey: "amount", + header: () =>
Version
, + cell: ({ row }) => { + const amount = parseFloat(row.getValue("amount")); + + // Format the amount as a dollar amount + // const formatted = new Intl.NumberFormat("en-US", { + // style: "currency", + // currency: "USD", + // }).format(amount); + + return
{amount}
; + }, + }, + { + accessorKey: "date", + sortingFn: "datetime", + enableSorting: true, + header: ({ column }) => { + return ( + + ); + }, + cell: ({ row }) => ( +
+ {getRelativeTime(row.original.date)} +
+ ), + }, + { + id: "actions", + enableHiding: false, + cell: ({ row }) => { + const workflow = row.original; + + return ( + + + + + + Actions + { + deleteWorkflow(workflow.id); + // navigator.clipboard.writeText(payment.id) + }} + > + Delete Workflow + + {/* + View customer + View payment details */} + + + ); + }, + }, +]; + +export function WorkflowList({ data }: { data: Payment[] }) { + 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("email")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter((column) => column.getCanHide()) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ); + })} + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + +
+
+
+ ); +} diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx new file mode 100644 index 0000000..78fcae9 --- /dev/null +++ b/web/src/components/ui/button.tsx @@ -0,0 +1,55 @@ +import { cn } from "@/lib/utils"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import * as React from "react"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/web/src/components/ui/card.tsx b/web/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/web/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/web/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..cad6f58 --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..f69a0d6 --- /dev/null +++ b/web/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/web/src/components/ui/form.tsx b/web/src/components/ui/form.tsx new file mode 100644 index 0000000..4603f8b --- /dev/null +++ b/web/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +