Compare commits

..
Author SHA1 Message Date
EmmanuelMr18 0cb2af3dc3 feat(share): display muliple images 2024-02-16 22:52:32 -06:00
EmmanuelMr18 9d999f92a9 fix(workflows): user can click multiple times on run even while loading 2024-02-14 19:00:18 -06:00
bennykok 01e8668d1a chore(plugin): update comfyui json 2024-02-14 14:56:11 +08:00
Emmanuel Morales 8e58d962a7 feat(example page): add inpaint, controlnet and sdxl turbo workflows (#24) 2024-02-14 12:49:32 +08:00
bennykok c59c308d32 chore(plugin): update comfyui-json version, fixed some custom nodes detection 2024-02-12 16:01:05 +08:00
bennykok 4560f2cca9 chore(plugin): update comfyui-json version 2024-02-11 21:53:48 +08:00
Nick Kao 872752b820 Merge pull request #22 from BennyKok/fix/createRun-overwritring-ComfyUIDeployExternalText-inputs
fix(create run): ComfyUIDeployExternalText default value was overwritten
2024-02-10 11:08:03 -08:00
EmmanuelMr18 97bb2b69c5 fix(create run): ComfyUIDeployExternalText default value overwriting 2024-02-10 10:57:54 -06:00
bennykok 08fe87c8af fix(plugin): cached execution progress update 2024-02-09 22:13:43 +08:00
bennykok d43e5fcefc fix: live status not sending, add progress 2024-02-09 21:52:48 +08:00
Emmanuel Morales 65492a108c feat(example page): create initial 4 cards (#21)
* feat(example page): add new examples section in the navbar

* feat(example page): initial layout of the new page

* feat(example page): create initial 4 workflows

* style(example page): center title

* style(example page): remove comment

* fix(example page): add hyperlink for txt2img card
2024-02-09 11:15:30 +08:00
San45600 1939ff4153 fix: mobile sheet ui fix (#20) 2024-02-09 11:11:58 +08:00
bennykok 4c32248d86 fix: External Text Input default value not working 2024-02-08 23:09:38 +08:00
bennykok 5ddbfdf44b feat(plugin): update comfyui json version, add missing nodes display 2024-02-08 22:55:04 +08:00
bennykok 7e86c20383 fix(plugin): ensure the status of run is set to fail if the prompt validation failed 2024-02-07 23:51:29 +08:00
bennykok 3adf77617b fix(plugin): async file upload to make sure it is not blocking 2024-02-07 23:51:29 +08:00
bennykok 1bc62a5fb4 fix: deps layout and comfyui-json 2024-02-07 18:17:22 +08:00
bennykok d473a211d0 fix: running locally cause a crash in after adding live status 2024-02-07 14:37:49 +08:00
bennykok 3aa239e58d fix: workflow upload issues 2024-02-06 16:18:54 +08:00
BennyKok 223aa5e70b Merge pull request #18 from EmmanuelMr18/docs/update-readme-development-section 2024-02-06 08:46:43 +08:00
Emmanuel Morales 5eef60a4eb docs(development): update step 9 text 2024-02-05 18:24:09 -06:00
Emmanuel Morales de750995cb docs(Development): add step to run local migration 2024-02-05 18:20:32 -06:00
bennykok 0f58fbcebd chore: update add live status 2024-02-05 23:28:18 +08:00
bennykok 6dc964c425 fix: update comfyui json in plugin 2024-02-05 17:43:34 +08:00
bennykok 4171c08413 fix(plugin): deps layout 2024-02-05 16:58:30 +08:00
bennykok 4348ab45dc feat: introduce dependencies upload 2024-02-05 16:47:21 +08:00
bennykok df46e3a0e5 chore: add CD_ENABLE_RUN_LOG flag 2024-02-01 23:38:01 +08:00
bennykok 2772101bbf chore: print out the log for debugging 2024-02-01 22:36:43 +08:00
bennykok 72fee51d32 fix: making sure the log was sent before setting the status 2024-02-01 21:04:24 +08:00
bennykok ffe0f98360 chore: log out data for debugging 2024-02-01 20:56:28 +08:00
bennykok 68377a84bc feat(plugin): add run log in comfydeploy plugin level 2024-02-01 18:31:49 +08:00
bennykok 50d4c399e9 feat: output render support mp4 and webm 2024-01-31 21:24:35 +08:00
bennykok 5a3955dfcb refactor(plugin): upload files logic in custom routes 2024-01-31 21:23:34 +08:00
bennykok 03227b52c0 chore(plugin): add a 2 secs delay before loading the incoming workflow 2024-01-31 21:23:21 +08:00
bennykok 8a8fbccfaa fix: gif file not returning file url. 2024-01-31 16:45:32 +08:00
bennykok 018d9a7b8d fix: civitai download url
For self hosting instance, this quick fix enable you to have the civitai token appended to the download url.

Add CIVITAI_TOKEN in Fly io
2024-01-31 16:33:16 +08:00
bennykok 774fd566d1 fix: selection issues on workflow page 2024-01-31 15:22:52 +08:00
BennyKok b81fcae6fb fix: timeout issues with nextjs by changing run remote to run spawn 2024-01-31 15:22:50 +08:00
bennykok b6b34c9062 fix: ensure logs are disabled by default 2024-01-30 15:26:12 +08:00
bennykok f73baa091a fix: disable the console log wrapper by default to preview spamming log issues
Running with export CD_ENABLE_LOG=true; python main.py to enable log
2024-01-29 13:39:37 +08:00
bennykok a838cb7ad4 fix: when there is auth token, replace it locally 2024-01-27 11:40:06 +08:00
bennykok 2afcade4f2 fix: add fly io to dev container 2024-01-26 16:07:54 +08:00
BennyKok d70333baa6 fix: comment typo 2024-01-26 07:47:41 +00:00
BennyKok 43cfebd97a fix: only create share slug when public-share deployment 2024-01-26 07:33:30 +00:00
BennyKok 763d2f77ce fix: comparison operator in CreateShareButton 2024-01-24 10:31:24 +00:00
BennyKok f1b68164c6 fix: Update comfyui snapshot hash 2024-01-24 06:17:28 +00:00
BennyKok 6437de4def fix: Add createSelectSchema for workflowVersionTable 2024-01-24 02:11:56 +00:00
BennyKok b24bdb070e Merge pull request #14 from j-jiseophan/fix-coroutine
fix coroutine error
2024-01-23 22:58:35 +08:00
BennyKok 33b57aea34 fix: display download progress for external checkpoint 2024-01-23 11:27:54 +08:00
BennyKok 71dcbe0d11 bump 2024-01-23 11:01:15 +08:00
BennyKok 8e3ee6468f fix client 2024-01-23 11:00:42 +08:00
BennyKok b8f39239ba attempt fix 2024-01-23 10:55:19 +08:00
BennyKok 9296f8f703 fix edit endpoint 2024-01-23 10:49:07 +08:00
BennyKok 6ed7807081 bump default machine comfy deploy version 2024-01-23 10:43:53 +08:00
BennyKok 43fe0a384a fix build 2024-01-23 10:33:45 +08:00
BennyKok dae6e46a8e fix: build 2024-01-23 09:37:57 +08:00
BennyKok f7f3d9708b chore: add dev github copilot 2024-01-23 09:34:16 +08:00
BennyKok 52d6e07eeb feat: add opening directly from comfy deploy -> machines 2024-01-23 09:33:53 +08:00
Jiseop Han f64c242ee5 fix coroutine error 2024-01-23 00:01:01 +09:00
BennyKok c0450b58d5 fix: format 2024-01-22 18:31:56 +08:00
BennyKok 47168930dc feat: add new auth_request flow for logging in with comfy deploy 2024-01-22 14:11:14 +08:00
BennyKok 3043093d22 chore: update dev container json 2024-01-22 12:56:14 +08:00
BennyKok 271552b453 chore: update biome settings 2024-01-21 23:55:18 +08:00
BennyKok 6de7bf3f20 feat: share page slug 2024-01-21 11:40:48 +08:00
BennyKok ca1b05fff5 feat: add cold start duration display 2024-01-21 11:40:06 +08:00
BennyKok 1d2497116d chore: update custom node inputs list 2024-01-20 22:58:08 +08:00
BennyKok fb020f9f3c feat(plugin): add external checkpoitns node 2024-01-20 22:49:35 +08:00
BennyKok e344c3e6a4 chore: add dev containers 2024-01-20 22:38:31 +08:00
BennyKok e400966117 chore: migrate to biomejs 2024-01-20 20:09:28 +08:00
105 changed files with 4360 additions and 2711 deletions
+18
View File
@@ -0,0 +1,18 @@
ARG VARIANT=18-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:${VARIANT}
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
WORKDIR "/"
# Install fly
RUN curl -L https://fly.io/install.sh | sh
ENV FLYCTL_INSTALL="/root/.fly"
ENV PATH="$FLYCTL_INSTALL/bin:$PATH"
# RUN echo 'export FLYCTL_INSTALL="/home/node/.fly"' >> ~/.bashrc
# RUN echo 'export PATH="$FLYCTL_INSTALL/bin:$PATH"' >> ~/.bashrc
RUN npm install -g bun
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Comfy Deploy Dev",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"postCreateCommand": "cd web && bun install && bun run migrate-local",
"remoteUser": "root",
"customizations": {
"vscode": {
"extensions": [
"biomejs.biome",
"formulahendry.auto-rename-tag",
"bradlc.vscode-tailwindcss",
"stivo.tailwind-fold",
"streetsidesoftware.code-spell-checker",
"GitHub.copilot",
"ms-azuretools.vscode-docker"
]
}
}
}
+48
View File
@@ -0,0 +1,48 @@
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
environment:
VSCODE_DEV_CONTAINER: true
volumes:
# Forwards the local Docker socket to the container.
- /var/run/docker.sock:/var/run/docker-host.sock
# Update this to wherever you want VS Code to mount the folder of your project
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
# entrypoint: /usr/local/share/docker-init.sh
command: sleep infinity
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
localstack:
image: localstack/localstack:latest
environment:
SERVICES: s3
ports:
- 4566:4566
volumes:
- ../web/aws:/etc/localstack/init/ready.d
- ../web/aws:/app/web/aws
+1 -8
View File
@@ -1,14 +1,7 @@
{ {
"recommendations": [ "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 "bradlc.vscode-tailwindcss", // hinting / autocompletion for tailwind
"ban.spellright", // Spell check for docs "ban.spellright", // Spell check for docs
"stripe.vscode-stripe", // stripe VSCode extension "biomejs.biome" // improved i18n DX
"Prisma.prisma", // syntax|format|completion for prisma
"rebornix.project-snippets", // Share useful snippets between collaborators
"inlang.vs-code-extension" // improved i18n DX
] ]
} }
+5 -7
View File
@@ -1,18 +1,16 @@
{ {
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"editor.formatOnSave": false, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll.eslint": true "quickfix.biome": "always"
// "source.organizeImports.biome": "explicit"
}, },
"typescript.preferences.importModuleSpecifier": "non-relative", "typescript.preferences.importModuleSpecifier": "non-relative",
"spellright.language": ["en"], "spellright.language": ["en"],
"spellright.documentTypes": ["markdown", "typescript", "typescriptreact"], "spellright.documentTypes": ["markdown", "typescript", "typescriptreact"],
"tailwindCSS.experimental.classRegex": [ "tailwindCSS.experimental.classRegex": [
[ ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
"cva\\(([^)]*)\\)", ]
"[\"'`]([^\"'`]*).*?[\"'`]"
]
],
// "eslint.workingDirectories": [ // "eslint.workingDirectories": [
// { "mode": "auto" } // { "mode": "auto" }
// ], // ],
+3 -2
View File
@@ -75,10 +75,11 @@ Major areas
3. `bun i` 3. `bun i`
4. Start docker 4. Start docker
5. `cp .env.example .env.local` 5. `cp .env.example .env.local`
6. Repace `JWT_SECRET` with `openssl rand -hex 32` 6. Replace `JWT_SECRET` with `openssl rand -hex 32`
7. Get a local clerk dev key for `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` 7. Get a local clerk dev key for `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`
8. Keep a terminal live for `bun run db-dev` 8. Keep a terminal live for `bun run db-dev`
9. Finally start the next server with `bun dev` 9. Execute the local migration to create the initial data `bun run migrate-local`
10. Finally start the next server with `bun dev`
**Schema Changes** **Schema Changes**
+28
View File
@@ -0,0 +1,28 @@
{
"$schema": "./web/node_modules/@biomejs/biome/configuration_schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"enabled": true
},
"linter": {
"enabled": false,
"rules": {
"recommended": true
}
},
"json": {
"parser": {
"allowComments": true
}
},
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
}
}
+1 -50
View File
@@ -8,7 +8,6 @@ from enum import Enum
import json import json
import subprocess import subprocess
import time import time
from uuid import uuid4
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import asyncio import asyncio
import threading import threading
@@ -20,7 +19,6 @@ from urllib.parse import parse_qs
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.types import ASGIApp, Scope, Receive, Send from starlette.types import ASGIApp, Scope, Receive, Send
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
# executor = ThreadPoolExecutor(max_workers=5) # executor = ThreadPoolExecutor(max_workers=5)
@@ -226,52 +224,6 @@ async def websocket_endpoint(websocket: WebSocket, machine_id: str):
# return {"Hello": "World"} # return {"Hello": "World"}
class UploadBody(BaseModel):
download_url: str
volume_name: str
volume_id: str
# callback_url: str
@app.post("/upload_volume")
async def upload_checkpoint(body: UploadBody):
global last_activity_time
last_activity_time = time.time()
logger.info(f"Extended inactivity time to {global_timeout}")
download_url = body.download_url
volume_name = body.volume_name
# callback_url = body.callback_url
folder_path = f"/app/builds/{body.volume_id}"
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume-builder", folder_path)
await cp_process.wait()
# Write the config file
config = {
"volume_names": {
volume_name: download_url
},
"paths": {
volume_name: f'/volumes/{uuid4()}'
},
}
await asyncio.subprocess.create_subprocess_shell(
f"modal run app.py",
# stdout=asyncio.subprocess.PIPE,
# stderr=asyncio.subprocess.PIPE,
cwd=folder_path,
env={**os.environ, "COLUMNS": "10000"}
)
with open(f"{folder_path}/config.py", "w") as f:
f.write("config = " + json.dumps(config))
# check that thi
return JSONResponse(status_code=200, content={"message": "Volume uploading", "build_machine_instance_id": fly_instance_id})
@app.post("/create") @app.post("/create")
async def create_machine(item: Item): async def create_machine(item: Item):
global last_activity_time global last_activity_time
@@ -361,8 +313,7 @@ async def build_logic(item: Item):
"name": item.name, "name": item.name,
"deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"), "deploy_test": os.environ.get("DEPLOY_TEST_FLAG", "False"),
"gpu": item.gpu, "gpu": item.gpu,
"public_checkpoint_volume": "model-store", "civitai_token": os.environ.get("CIVITAI_TOKEN", "")
"private_checkpoint_volume": "private-model-store"
} }
with open(f"{folder_path}/config.py", "w") as f: with open(f"{folder_path}/config.py", "w") as f:
f.write("config = " + json.dumps(config)) f.write("config = " + json.dumps(config))
+12 -13
View File
@@ -1,13 +1,12 @@
from config import config from config import config
import modal import modal
from modal import Image, Mount, web_endpoint, Stub, asgi_app, Volume from modal import Image, Mount, web_endpoint, Stub, asgi_app
import json import json
import urllib.request import urllib.request
import urllib.parse import urllib.parse
from pydantic import BaseModel from pydantic import BaseModel
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from volume import volumes
# deploy_test = False # deploy_test = False
@@ -29,6 +28,7 @@ web_app = FastAPI()
print(config) print(config)
print("deploy_test ", deploy_test) print("deploy_test ", deploy_test)
stub = Stub(name=config["name"]) stub = Stub(name=config["name"])
# print(stub.app_id)
if not deploy_test: if not deploy_test:
# dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data")) # dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data"))
@@ -36,6 +36,9 @@ if not deploy_test:
dockerfile_image = ( dockerfile_image = (
modal.Image.debian_slim() modal.Image.debian_slim()
.env({
"CIVITAI_TOKEN": config["civitai_token"],
})
.apt_install("git", "wget") .apt_install("git", "wget")
.pip_install( .pip_install(
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm" "git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
@@ -56,7 +59,7 @@ if not deploy_test:
# # Install comfy deploy # # Install comfy deploy
# "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git", # "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git",
# ) # )
.copy_local_file(f"{current_directory}/data/extra_model_paths.yaml", "/comfyui") # .copy_local_file(f"{current_directory}/data/extra_model_paths.yaml", "/comfyui")
.copy_local_file(f"{current_directory}/data/start.sh", "/start.sh") .copy_local_file(f"{current_directory}/data/start.sh", "/start.sh")
.run_commands("chmod +x /start.sh") .run_commands("chmod +x /start.sh")
@@ -153,9 +156,8 @@ image = Image.debian_slim()
target_image = image if deploy_test else dockerfile_image target_image = image if deploy_test else dockerfile_image
@stub.function(image=target_image, gpu=config["gpu"]
,volumes=volumes @stub.function(image=target_image, gpu=config["gpu"])
)
def run(input: Input): def run(input: Input):
import subprocess import subprocess
import time import time
@@ -164,7 +166,6 @@ def run(input: Input):
command = ["python", "main.py", command = ["python", "main.py",
"--disable-auto-launch", "--disable-metadata"] "--disable-auto-launch", "--disable-metadata"]
server_process = subprocess.Popen(command, cwd="/comfyui") server_process = subprocess.Popen(command, cwd="/comfyui")
check_server( check_server(
@@ -233,13 +234,12 @@ def run(input: Input):
async def bar(request_input: RequestInput): async def bar(request_input: RequestInput):
# print(request_input) # print(request_input)
if not deploy_test: if not deploy_test:
return run.remote(request_input.input) run.spawn(request_input.input)
return {"status": "success"}
# pass # pass
@stub.function(image=image @stub.function(image=image)
,volumes=volumes
)
@asgi_app() @asgi_app()
def comfyui_api(): def comfyui_api():
return web_app return web_app
@@ -289,7 +289,6 @@ def spawn_comfyui_in_background():
# to be on a single container. # to be on a single container.
concurrency_limit=1, concurrency_limit=1,
timeout=10 * 60, timeout=10 * 60,
volumes=volumes,
) )
@asgi_app() @asgi_app()
def comfyui_app(): def comfyui_app():
@@ -308,4 +307,4 @@ def comfyui_app():
}, },
)() )()
return make_simple_proxy_app(ProxyContext(config)) return make_simple_proxy_app(ProxyContext(config))
+1 -7
View File
@@ -1,7 +1 @@
config = { config = {"name": "my-app", "deploy_test": "True", "gpu": "T4"}
"name": "my-app",
"deploy_test": "True",
"gpu": "T4",
"public_checkpoint_volume": "model-store",
"private_checkpoint_volume": "private-model-store"
}
@@ -1,30 +1,11 @@
comfyui: comfyui:
base_path: /extra_models/ base_path: /runpod-volume/ComfyUI/
checkpoints: | checkpoints: models/checkpoints/
checkpoints clip: models/clip/
private_checkpoints clip_vision: models/clip_vision/
clip: | configs: models/configs/
clip controlnet: models/controlnet/
private_clip embeddings: models/embeddings/
clip_vision: | loras: models/loras/
clip_vision upscale_models: models/upscale_models/
private_clip_vision vae: models/vae/
configs: |
configs
private_configs
controlnet: |
controlnet
private_controlnet
embeddings: |
embeddings
private_embeddings
loras: |
loras
private_loras
upscale_models: |
upscale_models
private_upscale_models
vae: |
vae
private_vae
@@ -1,101 +0,0 @@
"""
This is a standalone script to download models into a modal Volume using civitai
Example Usage
`modal run insert_models::insert_model --civitai-url https://civitai.com/models/36520/ghostmix`
This inserts an individual model from a civitai url
`modal run insert_models::insert_models_civitai_api`
This inserts a bunch of models based on the models retrieved by civitai
civitai's API reference https://github.com/civitai/civitai/wiki/REST-API-Reference
"""
import modal
import subprocess
import requests
import json
stub = modal.Stub()
# NOTE: volume name can be variable
volume = modal.Volume.persisted("rah")
model_store_path = "/vol/models"
MODEL_ROUTE = "models"
image = (
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
)
@stub.function(volumes={model_store_path: volume}, image=image, timeout=50000, gpu=None)
def download_model(download_url):
print(download_url)
subprocess.run(["wget", download_url, "--content-disposition", "-P", model_store_path])
subprocess.run(["ls", "-la", model_store_path])
volume.commit()
# file is raw output from Civitai API https://github.com/civitai/civitai/wiki/REST-API-Reference
@stub.function()
def get_civitai_models(model_type: str, sort: str = "Highest Rated", page: int = 1):
"""Fetch models from CivitAI API based on type."""
try:
response = requests.get(f"https://civitai.com/api/v1/models", params={"types": model_type, "page": page, "sort": sort})
response.raise_for_status()
return response.json()
except requests.RequestException as e:
print(f"Error fetching models: {e}")
return None
@stub.function()
def get_civitai_model_url(civitai_url: str):
# Validate the URL
if civitai_url.startswith("https://civitai.com/api/"):
api_url = civitai_url
elif civitai_url.startswith("https://civitai.com/models/"):
try:
model_id = civitai_url.split("/")[4]
int(model_id)
except (IndexError, ValueError):
return None
api_url = f"https://civitai.com/api/v1/models/{model_id}"
else:
return "Error: URL must be from civitai.com and contain /models/"
response = requests.get(api_url)
# Check for successful response
if response.status_code != 200:
return f"Error: Unable to fetch data from {api_url}"
# Return the response data
return response.json()
@stub.local_entrypoint()
def insert_models_civitai_api(type: str = "Checkpoint", sort = "Highest Rated", page: int = 1):
civitai_models = get_civitai_models.local(type, sort, page)
if civitai_models:
for _ in download_model.map(map(lambda model: model['modelVersions'][0]['downloadUrl'], civitai_models['items'])):
pass
else:
print("Failed to retrieve models.")
@stub.local_entrypoint()
def insert_model(civitai_url: str):
if civitai_url.startswith("'https://civitai.com/api/download/models/"):
download_url = civitai_url
else:
civitai_model = get_civitai_model_url.local(civitai_url)
if civitai_model:
download_url = civitai_model['modelVersions'][0]['downloadUrl']
else:
return "invalid URL"
download_model.remote(download_url)
@stub.local_entrypoint()
def simple_download():
download_urls = ['https://civitai.com/api/download/models/119057', 'https://civitai.com/api/download/models/130090', 'https://civitai.com/api/download/models/31859', 'https://civitai.com/api/download/models/128713', 'https://civitai.com/api/download/models/179657', 'https://civitai.com/api/download/models/143906', 'https://civitai.com/api/download/models/9208', 'https://civitai.com/api/download/models/136078', 'https://civitai.com/api/download/models/134065', 'https://civitai.com/api/download/models/288775', 'https://civitai.com/api/download/models/95263', 'https://civitai.com/api/download/models/288982', 'https://civitai.com/api/download/models/87153', 'https://civitai.com/api/download/models/10638', 'https://civitai.com/api/download/models/263809', 'https://civitai.com/api/download/models/130072', 'https://civitai.com/api/download/models/117019', 'https://civitai.com/api/download/models/95256', 'https://civitai.com/api/download/models/197181', 'https://civitai.com/api/download/models/256915', 'https://civitai.com/api/download/models/118945', 'https://civitai.com/api/download/models/125843', 'https://civitai.com/api/download/models/179015', 'https://civitai.com/api/download/models/245598', 'https://civitai.com/api/download/models/223670', 'https://civitai.com/api/download/models/90072', 'https://civitai.com/api/download/models/290817', 'https://civitai.com/api/download/models/154097', 'https://civitai.com/api/download/models/143497', 'https://civitai.com/api/download/models/5637']
for _ in download_model.map(download_urls):
pass
@@ -45,13 +45,19 @@ for package in packages:
response = requests.request("POST", f"{root_url}/customnode/install", json=package, headers=headers) response = requests.request("POST", f"{root_url}/customnode/install", json=package, headers=headers)
print(response.text) print(response.text)
# with open('models.json') as f: with open('models.json') as f:
# models = json.load(f) models = json.load(f)
#
# for model in models: for model in models:
# response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers) import os
# print(response.text) if "civitai.com/api" in model['url'] and not "token=" in model['url']:
if "?" in model['url']:
model['url'] += "&token=" + os.environ.get('CIVITAI_TOKEN', '')
else:
model['url'] += "?token=" + os.environ.get('CIVITAI_TOKEN', '')
response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers)
print(response.text)
# Close the server # Close the server
server_process.terminate() server_process.terminate()
print("Finished installing dependencies.") print("Finished installing dependencies.")
@@ -1,10 +0,0 @@
import modal
from config import config
public_model_volume = modal.Volume.persisted(config["public_checkpoint_volume"])
private_volume = modal.Volume.persisted(config["private_checkpoint_volume"])
BASEMODEL_DIR = "/extra_models/"
MODEL_DIR = BASEMODEL_DIR + "checkpoints"
PRIVATE_MODEL_DIR = BASEMODEL_DIR + "private_checkpoints"
volumes = {MODEL_DIR: public_model_volume, PRIVATE_MODEL_DIR: private_volume}
@@ -1,45 +0,0 @@
import modal
from config import config
import os
import subprocess
stub = modal.Stub()
# Volume names may only contain alphanumeric characters, dashes, periods, and underscores, and must be less than 64 characters in length.
def is_valid_name(name: str) -> bool:
allowed_characters = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._")
return 0 < len(name) <= 64 and all(char in allowed_characters for char in name)
def create_volumes(volume_names, paths):
path_to_vol = {}
for volume_name in volume_names.keys():
if not is_valid_name(volume_name):
pass
modal_volume = modal.Volume.persisted(volume_name)
path_to_vol[paths[volume_name]] = modal_volume
return path_to_vol
vol_name_to_links = config["volume_names"]
vol_name_to_path = config["paths"]
volumes = create_volumes(vol_name_to_links, vol_name_to_path)
image = (
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
)
print(vol_name_to_links)
print(vol_name_to_path)
print(volumes)
@stub.function(volumes=volumes, image=image, timeout=5000, gpu=None)
def download_model(volume_name, download_url):
model_store_path = vol_name_to_path[volume_name]
subprocess.run(["wget", download_url, "--content-disposition", "-P", model_store_path])
subprocess.run(["ls", "-la", model_store_path])
volumes[model_store_path].commit()
@stub.local_entrypoint()
def simple_download():
print(vol_name_to_links)
print([(vol_name, link) for vol_name,link in vol_name_to_links.items()])
list(download_model.starmap([(vol_name, link) for vol_name,link in vol_name_to_links.items()]))
@@ -1,8 +0,0 @@
config = {
"volume_names": {
"test": "https://pub-6230db03dc3a4861a9c3e55145ceda44.r2.dev/openpose-pose (1).png"
},
"paths": {
"test": "/volumes/something"
}
}
+68
View File
@@ -0,0 +1,68 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import folder_paths
from tqdm import tqdm
class ComfyUIDeployExternalCheckpoint:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_checkpoint"},
),
},
"optional": {
"default_checkpoint_name": (folder_paths.get_filename_list("checkpoints"), ),
}
}
RETURN_TYPES = (folder_paths.get_filename_list("checkpoints"),)
RETURN_NAMES = ("path",)
FUNCTION = "run"
CATEGORY = "deploy"
def run(self, input_id, default_checkpoint_name=None):
import requests
import os
import uuid
if input_id and input_id.startswith('http'):
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
print(folder_paths.folder_names_and_paths["checkpoints"][0][0])
destination_path = os.path.join(
folder_paths.folder_names_and_paths["checkpoints"][0][0], unique_filename)
print(destination_path)
print("Downloading external checkpoint - " +
input_id + " to " + destination_path)
response = requests.get(
input_id, headers={'User-Agent': 'Mozilla/5.0'}, allow_redirects=True, stream=True)
file_size = int(response.headers.get('Content-Length', 0))
chunk = 1
chunk_size = 1024
num_bars = int(file_size / chunk_size)
with open(destination_path, 'wb') as out_file:
for chunk in tqdm(
response.iter_content(chunk_size=chunk_size),
total=num_bars,
unit='KB',
desc="Downloading",
leave=True # leave=True to keep progress bars
):
out_file.write(chunk)
return (unique_filename,)
else:
return (default_checkpoints_name,)
NODE_CLASS_MAPPINGS = {
"ComfyUIDeployExternalCheckpoint": ComfyUIDeployExternalCheckpoint}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalCheckpoint": "External Checkpoint (ComfyUI Deploy)"}
+1 -3
View File
@@ -29,9 +29,7 @@ class ComfyUIDeployExternalText:
CATEGORY = "text" CATEGORY = "text"
def run(self, input_id, default_value=None): def run(self, input_id, default_value=None):
if not input_id or len(input_id.strip()) == 0: return [default_value]
return [default_value]
return [input_id]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalText": ComfyUIDeployExternalText} NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalText": ComfyUIDeployExternalText}
+255 -17
View File
@@ -22,10 +22,14 @@ from logging.handlers import RotatingFileHandler
from enum import Enum from enum import Enum
from urllib.parse import quote from urllib.parse import quote
import threading import threading
import hashlib
import aiohttp
api = None api = None
api_task = None api_task = None
prompt_metadata = {} prompt_metadata = {}
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true'
def post_prompt(json_data): def post_prompt(json_data):
prompt_server = server.PromptServer.instance prompt_server = server.PromptServer.instance
@@ -97,6 +101,7 @@ async def comfy_deploy_run(request):
prompt_metadata[prompt_id] = { prompt_metadata[prompt_id] = {
'status_endpoint': data.get('status_endpoint'), 'status_endpoint': data.get('status_endpoint'),
'file_upload_endpoint': data.get('file_upload_endpoint'), 'file_upload_endpoint': data.get('file_upload_endpoint'),
'workflow_api': workflow_api
} }
try: try:
@@ -113,6 +118,8 @@ async def comfy_deploy_run(request):
"stack_trace": stack_trace "stack_trace": stack_trace
} }
}) })
# When there are critical errors, the prompt is actually not run
await update_run(prompt_id, Status.FAILED)
return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}") return web.Response(status=500, reason=f"{error_type}: {e}, {stack_trace_short}")
status = 200 status = 200
@@ -141,6 +148,138 @@ async def comfy_deploy_run(request):
sockets = dict() sockets = dict()
def get_comfyui_path_from_file_path(file_path):
file_path_parts = file_path.split("\\")
if file_path_parts[0] == "input":
print("matching input")
file_path = os.path.join(folder_paths.get_directory_by_type("input"), *file_path_parts[1:])
elif file_path_parts[0] == "models":
print("matching models")
file_path = folder_paths.get_full_path(file_path_parts[1], os.path.join(*file_path_parts[2:]))
print(file_path)
return file_path
# Form ComfyUI Manager
def compute_sha256_checksum(filepath):
filepath = get_comfyui_path_from_file_path(filepath)
"""Compute the SHA256 checksum of a file, in chunks"""
sha256 = hashlib.sha256()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
sha256.update(chunk)
return sha256.hexdigest()
# This is start uploading the files to Comfy Deploy
@server.PromptServer.instance.routes.post('/comfyui-deploy/upload-file')
async def upload_file(request):
data = await request.json()
file_path = data.get("file_path")
print("Original file path", file_path)
file_path = get_comfyui_path_from_file_path(file_path)
# return web.json_response({
# "error": f"File not uploaded"
# }, status=500)
token = data.get("token")
get_url = data.get("url")
try:
base = folder_paths.base_path
file_path = os.path.join(base, file_path)
if os.path.exists(file_path):
file_size = os.path.getsize(file_path)
file_extension = os.path.splitext(file_path)[1]
if file_extension in ['.jpg', '.jpeg']:
file_type = 'image/jpeg'
elif file_extension == '.png':
file_type = 'image/png'
elif file_extension == '.webp':
file_type = 'image/webp'
else:
file_type = 'application/octet-stream' # Default to binary file type if unknown
else:
return web.json_response({
"error": f"File not found: {file_path}"
}, status=404)
except Exception as e:
return web.json_response({
"error": str(e)
}, status=500)
if get_url:
try:
async with aiohttp.ClientSession() as session:
headers = {'Authorization': f'Bearer {token}'}
params = {'file_size': file_size, 'type': file_type}
async with session.get(get_url, params=params, headers=headers) as response:
if response.status == 200:
content = await response.json()
upload_url = content["upload_url"]
with open(file_path, 'rb') as f:
headers = {
"Content-Type": file_type,
"x-amz-acl": "public-read",
"Content-Length": str(file_size)
}
async with session.put(upload_url, data=f, headers=headers) as upload_response:
if upload_response.status == 200:
return web.json_response({
"message": "File uploaded successfully",
"download_url": content["download_url"]
})
else:
return web.json_response({
"error": f"Failed to upload file to {upload_url}. Status code: {upload_response.status}"
}, status=upload_response.status)
else:
return web.json_response({
"error": f"Failed to fetch data from {get_url}. Status code: {response.status}"
}, status=response.status)
except Exception as e:
return web.json_response({
"error": f"An error occurred while fetching data from {get_url}: {str(e)}"
}, status=500)
return web.json_response({
"error": f"File not uploaded"
}, status=500)
@server.PromptServer.instance.routes.get('/comfyui-deploy/get-file-hash')
async def get_file_hash(request):
file_path = request.rel_url.query.get('file_path', '')
if file_path is None:
return web.json_response({
"error": "file_path is required"
}, status=400)
try:
base = folder_paths.base_path
file_path = os.path.join(base, file_path)
# print("file_path", file_path)
file_hash = compute_sha256_checksum(
file_path
)
return web.json_response({
"file_hash": file_hash
})
except Exception as e:
return web.json_response({
"error": str(e)
}, status=500)
@server.PromptServer.instance.routes.get('/comfyui-deploy/ws') @server.PromptServer.instance.routes.get('/comfyui-deploy/ws')
async def websocket_handler(request): async def websocket_handler(request):
ws = web.WebSocketResponse() ws = web.WebSocketResponse()
@@ -157,7 +296,9 @@ async def websocket_handler(request):
try: try:
# Send initial state to the new client # Send initial state to the new client
await send("status", { 'sid': sid }, sid) await send("status", { 'sid': sid }, sid)
await send_first_time_log(sid)
if cd_enable_log:
await send_first_time_log(sid)
async for msg in ws: async for msg in ws:
if msg.type == aiohttp.WSMsgType.ERROR: if msg.type == aiohttp.WSMsgType.ERROR:
@@ -203,7 +344,10 @@ async def send_json_override(self, event, data, sid=None):
prompt_id = data.get('prompt_id') prompt_id = data.get('prompt_id')
# now we send everything # now we send everything
await asyncio.wait([send(event, data), self.send_json_original(event, data, sid)]) await asyncio.wait([
asyncio.create_task(send(event, data)),
asyncio.create_task(self.send_json_original(event, data, sid))
])
if event == 'execution_start': if event == 'execution_start':
update_run(prompt_id, Status.RUNNING) update_run(prompt_id, Status.RUNNING)
@@ -214,6 +358,34 @@ async def send_json_override(self, event, data, sid=None):
if not have_pending_upload(prompt_id): if not have_pending_upload(prompt_id):
update_run(prompt_id, Status.SUCCESS) update_run(prompt_id, Status.SUCCESS)
if event == 'executing' and data.get('node') is not None:
node = data.get('node')
if prompt_id in prompt_metadata:
if 'progress' not in prompt_metadata[prompt_id]:
prompt_metadata[prompt_id]["progress"] = set()
prompt_metadata[prompt_id]["progress"].add(node)
calculated_progress = len(prompt_metadata[prompt_id]["progress"]) / len(prompt_metadata[prompt_id]['workflow_api'])
# print("calculated_progress", calculated_progress)
if 'last_updated_node' in prompt_metadata[prompt_id] and prompt_metadata[prompt_id]['last_updated_node'] == node:
return
prompt_metadata[prompt_id]['last_updated_node'] = node
class_type = prompt_metadata[prompt_id]['workflow_api'][node]['class_type']
print("updating run live status", class_type)
await update_run_live_status(prompt_id, "Executing " + class_type, calculated_progress)
if event == 'execution_cached' and data.get('nodes') is not None:
if prompt_id in prompt_metadata:
if 'progress' not in prompt_metadata[prompt_id]:
prompt_metadata[prompt_id]["progress"] = set()
if 'nodes' in data:
for node in data.get('nodes', []):
prompt_metadata[prompt_id]["progress"].add(node)
# prompt_metadata[prompt_id]["progress"].update(data.get('nodes'))
if event == 'execution_error': if event == 'execution_error':
# Careful this might not be fully awaited. # Careful this might not be fully awaited.
await update_run_with_output(prompt_id, data) await update_run_with_output(prompt_id, data)
@@ -233,7 +405,30 @@ class Status(Enum):
FAILED = "failed" FAILED = "failed"
UPLOADING = "uploading" UPLOADING = "uploading"
# Global variable to keep track of the last read line number
last_read_line_number = 0
async def update_run_live_status(prompt_id, live_status, calculated_progress: float):
if prompt_id not in prompt_metadata:
return
print("progress", calculated_progress)
status_endpoint = prompt_metadata[prompt_id]['status_endpoint']
body = {
"run_id": prompt_id,
"live_status": live_status,
"progress": calculated_progress
}
# requests.post(status_endpoint, json=body)
async with aiohttp.ClientSession() as session:
async with session.post(status_endpoint, json=body) as response:
pass
def update_run(prompt_id, status: Status): def update_run(prompt_id, status: Status):
global last_read_line_number
if prompt_id not in prompt_metadata: if prompt_id not in prompt_metadata:
return return
@@ -248,16 +443,50 @@ def update_run(prompt_id, status: Status):
"run_id": prompt_id, "run_id": prompt_id,
"status": status.value, "status": status.value,
} }
prompt_metadata[prompt_id]['status'] = status
print(f"Status: {status.value}") print(f"Status: {status.value}")
try: try:
requests.post(status_endpoint, json=body) requests.post(status_endpoint, json=body)
if cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED):
try:
with open(comfyui_file_path, 'r') as log_file:
# log_data = log_file.read()
# Move to the last read line
all_log_data = log_file.read() # Read all log data
print("All log data before skipping:", all_log_data) # Log all data before skipping
log_file.seek(0) # Reset file pointer to the beginning
for _ in range(last_read_line_number):
next(log_file)
log_data = log_file.read()
# Update the last read line number
last_read_line_number += log_data.count('\n')
print("last_read_line_number", last_read_line_number)
print("log_data", log_data)
print("log_data.count(n)", log_data.count('\n'))
body = {
"run_id": prompt_id,
"log_data": [
{
"logs": log_data,
# "timestamp": time.time(),
}
]
}
requests.post(status_endpoint, json=body)
except Exception as log_error:
print(f"Error reading log file: {log_error}")
except Exception as e: except Exception as e:
error_type = type(e).__name__ error_type = type(e).__name__
stack_trace = traceback.format_exc().strip() stack_trace = traceback.format_exc().strip()
print(f"Error occurred while updating run: {e} {stack_trace}") print(f"Error occurred while updating run: {e} {stack_trace}")
finally:
prompt_metadata[prompt_id]['status'] = status
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"): async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
""" """
@@ -307,7 +536,9 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
"Content-Length": str(len(data)), "Content-Length": str(len(data)),
} }
response = requests.put(ok.get("url"), headers=headers, data=data) response = requests.put(ok.get("url"), headers=headers, data=data)
print("upload file response", response.status_code) async with aiohttp.ClientSession() as session:
async with session.put(ok.get("url"), headers=headers, data=data) as response:
print("upload file response", response.status)
def have_pending_upload(prompt_id): def have_pending_upload(prompt_id):
if 'prompt_id' in prompt_metadata and 'uploading_nodes' in prompt_metadata[prompt_id] and len(prompt_metadata[prompt_id]['uploading_nodes']) > 0: if 'prompt_id' in prompt_metadata and 'uploading_nodes' in prompt_metadata[prompt_id] and len(prompt_metadata[prompt_id]['uploading_nodes']) > 0:
@@ -384,20 +615,25 @@ async def update_file_status(prompt_id, data, uploading, have_error=False, node_
"prompt_id": prompt_id, "prompt_id": prompt_id,
}) })
async def handle_upload(prompt_id, data, key, content_type_key, default_content_type):
items = data.get(key, [])
for item in items:
await upload_file(
prompt_id,
item.get("filename"),
subfolder=item.get("subfolder"),
type=item.get("type"),
content_type=item.get(content_type_key, default_content_type)
)
# Upload files in the background # Upload files in the background
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True): async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
try: try:
images = data.get('images', []) await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
for image in images: await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
await upload_file(prompt_id, image.get("filename"), subfolder=image.get("subfolder"), type=image.get("type"), content_type=image.get("content_type", "image/png")) # This will also be mp4
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
files = data.get('files', [])
for file in files:
await upload_file(prompt_id, file.get("filename"), subfolder=file.get("subfolder"), type=file.get("type"), content_type=file.get("content_type", "image/png"))
gifs = data.get('gifs', [])
for gif in gifs:
await upload_file(prompt_id, gif.get("filename"), subfolder=gif.get("subfolder"), type=gif.get("type"), content_type=gif.get("format", "image/gif"))
if have_upload: if have_upload:
await update_file_status(prompt_id, data, False, node_id=node_id) await update_file_status(prompt_id, data, False, node_id=node_id)
@@ -438,6 +674,7 @@ prompt_server.send_json = send_json_override.__get__(prompt_server, server.Promp
root_path = os.path.dirname(os.path.abspath(__file__)) root_path = os.path.dirname(os.path.abspath(__file__))
two_dirs_up = os.path.dirname(os.path.dirname(root_path)) two_dirs_up = os.path.dirname(os.path.dirname(root_path))
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log') log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
comfyui_file_path = os.path.join(two_dirs_up, 'comfyui.log')
last_read_line = 0 last_read_line = 0
@@ -477,4 +714,5 @@ def run_in_new_thread(coroutine):
t.start() t.start()
asyncio.run_coroutine_threadsafe(coroutine, new_loop) asyncio.run_coroutine_threadsafe(coroutine, new_loop)
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket)) if cd_enable_log:
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
+43 -32
View File
@@ -7,45 +7,56 @@ import threading
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5) # Running with export CD_ENABLE_LOG=true; python main.py
original_stdout = sys.stdout # Check for 'cd-enable-log' flag in input arguments
original_stderr = sys.stderr # cd_enable_log = '--cd-enable-log' in sys.argv
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
class StreamToLogger(): def setup():
def __init__(self, log_level): handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
self.log_level = log_level
def write(self, buf): original_stdout = sys.stdout
if (self.log_level == logging.INFO): original_stderr = sys.stderr
original_stdout.write(buf)
original_stdout.flush()
elif (self.log_level == logging.ERROR):
original_stderr.write(buf)
original_stderr.flush()
for line in buf.rstrip().splitlines(): class StreamToLogger():
handler.handle( def __init__(self, log_level):
logging.LogRecord( self.log_level = log_level
name="comfy-deploy",
level=self.log_level, def write(self, buf):
pathname="prestartup_script.py", if (self.log_level == logging.INFO):
lineno=1, original_stdout.write(buf)
msg=line.rstrip(), original_stdout.flush()
args=None, elif (self.log_level == logging.ERROR):
exc_info=None original_stderr.write(buf)
original_stderr.flush()
for line in buf.rstrip().splitlines():
handler.handle(
logging.LogRecord(
name="comfy-deploy",
level=self.log_level,
pathname="prestartup_script.py",
lineno=1,
msg=line.rstrip(),
args=None,
exc_info=None
)
) )
)
def flush(self): def flush(self):
if (self.log_level == logging.INFO): if (self.log_level == logging.INFO):
original_stdout.flush() original_stdout.flush()
elif (self.log_level == logging.ERROR): elif (self.log_level == logging.ERROR):
original_stderr.flush() original_stderr.flush()
# Redirect stdout and stderr to the logger # Redirect stdout and stderr to the logger
sys.stdout = StreamToLogger(logging.INFO) sys.stdout = StreamToLogger(logging.INFO)
sys.stderr = StreamToLogger(logging.ERROR) sys.stderr = StreamToLogger(logging.ERROR)
if cd_enable_log:
print("** Comfy Deploy logging enabled")
setup()
try: try:
# Get the absolute path of the script's directory # Get the absolute path of the script's directory
+433 -49
View File
@@ -1,6 +1,7 @@
import { app } from "./app.js"; import { app } from "./app.js";
import { api } from "./api.js"; import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/[email protected]";
/** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/ /** @typedef {import('../../../web/types/comfy.js').ComfyExtension} ComfyExtension*/
/** @type {ComfyExtension} */ /** @type {ComfyExtension} */
@@ -9,6 +10,76 @@ const ext = {
init(app) { init(app) {
addButton(); addButton();
const queryParams = new URLSearchParams(window.location.search);
const workflow_version_id = queryParams.get("workflow_version_id");
const auth_token = queryParams.get("auth_token");
const org_display = queryParams.get("org_display");
const origin = queryParams.get("origin");
const data = getData();
let endpoint = data.endpoint;
let apiKey = data.apiKey;
// If there is auth token override it
if (auth_token) {
apiKey = auth_token;
endpoint = origin;
saveData({
displayName: org_display,
endpoint: origin,
apiKey: auth_token,
displayName: org_display,
environment: "cloud",
});
localStorage.setItem("comfy_deploy_env", "cloud");
}
if (!workflow_version_id) {
console.error("No workflow_version_id provided in query parameters.");
} else {
loadingDialog.showLoading(
"Loading workflow from " + org_display,
"Please wait...",
);
fetch(endpoint + "/api/workflow-version/" + workflow_version_id, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
})
.then(async (res) => {
const data = await res.json();
const { workflow, workflow_id, error } = data;
if (error) {
infoDialog.showMessage("Unable to load this workflow", error);
return;
}
// Adding a delay to wait for the intial graph to load
await new Promise((resolve) => setTimeout(resolve, 2000));
workflow?.nodes.forEach((x) => {
if (x?.type === "ComfyDeploy") {
x.widgets_values[1] = workflow_id;
// x.widgets_values[2] = workflow_version.version;
}
});
/** @type {LGraph} */
app.loadGraphData(workflow);
})
.catch((e) => infoDialog.showMessage("Error", e.message))
.finally(() => {
loadingDialog.close();
window.history.replaceState(
{},
document.title,
window.location.pathname,
);
});
}
}, },
registerCustomNodes() { registerCustomNodes() {
@@ -108,6 +179,92 @@ function showError(title, message) {
); );
} }
function createDynamicUIHtml(data) {
console.log(data);
let html =
'<div style="min-width: 600px; max-width: 1024px; margin: 14px auto; display: flex; flex-direction: column; gap: 24px;">';
const bgcolor = "var(--comfy-input-bg)";
const evenBg = "var(--border-color)";
const textColor = "var(--input-text)";
// Custom Nodes
html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
html +=
'<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Custom Nodes</h2>';
if (data.missing_nodes?.length > 0) {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding: 4px 12px; background-color: ${evenBg}">
<h3 style="font-size: 14px; font-weight: semibold; margin-bottom: 8px;">Missing Nodes</h3>
<p style="font-size: 12px;">These nodes are not found with any matching custom_nodes in the ComfyUI Manager Database</p>
${data.missing_nodes
.map((node) => {
return `<p style="font-size: 14px; color: #d69e2e;">${node}</p>`;
})
.join("")}
</div>
`;
}
Object.values(data.custom_nodes).forEach((node) => {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 16px;">
<a href="${
node.url
}" target="_blank" style="font-size: 18px; font-weight: semibold; color: white; text-decoration: none;">${
node.name
}</a>
<p style="font-size: 14px; color: #4b5563;">${node.hash}</p>
${
node.warning
? `<p style="font-size: 14px; color: #d69e2e;">${node.warning}</p>`
: ""
}
</div>
`;
});
html += "</div>";
// Models
html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
html +=
'<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Models</h2>';
Object.entries(data.models).forEach(([section, items]) => {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`;
items.forEach((item) => {
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
});
html += "</div>";
});
html += "</div>";
// Models
html += `<div style="background-color: ${bgcolor}; padding: 24px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);">`;
html +=
'<h2 style="margin-top: 0px; font-size: 24px; font-weight: bold; margin-bottom: 16px;">Files</h2>';
Object.entries(data.files).forEach(([section, items]) => {
html += `
<div style="border-bottom: 1px solid #e2e8f0; padding-top: 8px; padding-bottom: 8px;">
<h3 style="font-size: 18px; font-weight: semibold; margin-bottom: 8px;">${
section.charAt(0).toUpperCase() + section.slice(1)
}</h3>`;
items.forEach((item) => {
html += `<p style="font-size: 14px; color: ${textColor};">${item.name}</p>`;
});
html += "</div>";
});
html += "</div>";
html += "</div>";
return html;
}
function addButton() { function addButton() {
const menu = document.querySelector(".comfy-menu"); const menu = document.querySelector(".comfy-menu");
@@ -119,8 +276,33 @@ function addButton() {
/** @type {LGraph} */ /** @type {LGraph} */
const graph = app.graph; const graph = app.graph;
let { endpoint, apiKey, displayName } = getData();
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") {
configDialog.show();
return;
}
const ok = await confirmDialog.confirm(
"Confirm deployment -> " +
displayName +
"<br><br><small><div style='font-weight: normal;'>" +
endpoint +
"<div><br>",
`A new version will be deployed, are you confirm? <br><br><input id="include-deps" type="checkbox" checked>Include dependence</input>`,
);
if (!ok) return;
const includeDeps = document.getElementById("include-deps").checked;
if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1);
}
loadingDialog.showLoading("Generating snapshot", "Please wait...");
const snapshot = await fetch("/snapshot/get_current").then((x) => x.json()); const snapshot = await fetch("/snapshot/get_current").then((x) => x.json());
console.log(snapshot); // console.log(snapshot);
loadingDialog.close();
if (!snapshot) { if (!snapshot) {
showError( showError(
@@ -154,52 +336,122 @@ function addButton() {
const deployMetaNode = deployMeta[0]; const deployMetaNode = deployMeta[0];
console.log(deployMetaNode);
const workflow_name = deployMetaNode.widgets[0].value; const workflow_name = deployMetaNode.widgets[0].value;
const workflow_id = deployMetaNode.widgets[1].value; const workflow_id = deployMetaNode.widgets[1].value;
console.log(workflow_name, workflow_id);
const prompt = await app.graphToPrompt(); const prompt = await app.graphToPrompt();
console.log(graph); let deps = undefined;
console.log(prompt);
// const endpoint = localStorage.getItem("endpoint") ?? ""; if (includeDeps) {
// const apiKey = localStorage.getItem("apiKey"); loadingDialog.showLoading("Fetching existing version", "Please wait...");
const { endpoint, apiKey } = getData(); const existing_workflow = await fetch(
endpoint + "/api/workflow/" + workflow_id,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + apiKey,
},
},
)
.then((x) => x.json())
.catch(() => {
return {};
});
if (!endpoint || !apiKey || apiKey === "" || endpoint === "") { loadingDialog.close();
configDialog.show();
return; loadingDialog.showLoading(
"Generating dependency graph",
"Please wait...",
);
deps = await generateDependencyGraph({
workflow_api: prompt.output,
snapshot: snapshot,
computeFileHash: async (file) => {
console.log(file);
loadingDialog.showLoading("Generating hash", file);
const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(
file,
)}`,
).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file);
console.log(hash);
return hash.file_hash;
},
handleFileUpload: async (file, hash, prevhash) => {
console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file);
try {
const { download_url } = await fetch(
`/comfyui-deploy/upload-file`,
{
method: "POST",
body: JSON.stringify({
file_path: file,
token: apiKey,
url: endpoint + "/api/upload-url",
}),
},
)
.then((x) => x.json())
.catch(() => {
loadingDialog.close();
confirmDialog.confirm("Error", "Unable to upload file " + file);
});
loadingDialog.showLoading("Uploaded file", file);
console.log(download_url);
return download_url;
} catch (error) {
return undefined;
}
},
existingDependencies: existing_workflow.dependencies,
});
loadingDialog.close();
const depsOk = await confirmDialog.confirm(
"Check dependencies",
// JSON.stringify(deps, null, 2),
createDynamicUIHtml(deps),
);
if (!depsOk) return;
console.log(deps);
} }
const ok = await confirmDialog.confirm("Confirm deployment", "A new version will be deployed, are you conform?") loadingDialog.showLoading("Deploying...");
if (!ok) return;
title.innerText = "Deploying..."; title.innerText = "Deploying...";
title.style.color = "orange"; title.style.color = "orange";
console.log(prompt); // console.log(prompt);
// TODO trim the ending / from endpoint is there is // TODO trim the ending / from endpoint is there is
if (endpoint.endsWith("/")) { if (endpoint.endsWith("/")) {
endpoint = endpoint.slice(0, -1); endpoint = endpoint.slice(0, -1);
} }
const apiRoute = endpoint + "/api/upload"; // console.log(prompt.workflow);
const apiRoute = endpoint + "/api/workflow";
// const userId = apiKey // const userId = apiKey
try { try {
const body = {
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
dependencies: deps,
};
console.log(body);
let data = await fetch(apiRoute, { let data = await fetch(apiRoute, {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify(body),
workflow_name,
workflow_id,
workflow: prompt.workflow,
workflow_api: prompt.output,
snapshot: snapshot,
}),
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + apiKey, Authorization: "Bearer " + apiKey,
@@ -214,6 +466,8 @@ function addButton() {
data = await data.json(); data = await data.json();
} }
loadingDialog.close();
title.textContent = "Done"; title.textContent = "Done";
title.style.color = "green"; title.style.color = "green";
@@ -230,6 +484,7 @@ function addButton() {
title.style.color = "white"; title.style.color = "white";
}, 1000); }, 1000);
} catch (e) { } catch (e) {
loadingDialog.close();
app.ui.dialog.show(e); app.ui.dialog.show(e);
console.error(e); console.error(e);
title.textContent = "Error"; title.textContent = "Error";
@@ -275,14 +530,16 @@ export class InfoDialog extends ComfyDialog {
this.element.classList.add("comfy-normal-modal"); this.element.classList.add("comfy-normal-modal");
this.element.style.paddingBottom = "20px"; this.element.style.paddingBottom = "20px";
} }
button = undefined;
createButtons() { createButtons() {
return [ this.button = $el("button", {
$el("button", { type: "button",
type: "button", textContent: "Close",
textContent: "Close", onclick: () => this.close(),
onclick: () => this.close(), });
}), return [this.button];
];
} }
close() { close() {
@@ -301,6 +558,72 @@ export class InfoDialog extends ComfyDialog {
this.element.style.display = "flex"; this.element.style.display = "flex";
this.element.style.zIndex = 1001; this.element.style.zIndex = 1001;
} }
showMessage(title, message) {
this.show(`
<div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px;">${title}</h3>
<label>
${message}
</label>
</div>
`);
}
loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) {
this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center;">${title} ${this.loadingIcon}</h3>
<label>
${message}
</label>
</div>
`);
}
}
export class LoadingDialog extends ComfyDialog {
constructor() {
super();
this.element.classList.add("comfy-normal-modal");
// this.element.style.paddingBottom = "20px";
}
createButtons() {
return [];
}
close() {
this.element.style.display = "none";
}
show(html) {
this.textElement.style["white-space"] = "normal";
this.textElement.style.color = "white";
this.textElement.style.marginTop = "0px";
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
}
this.element.style.display = "flex";
this.element.style.zIndex = 1001;
}
loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) {
this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 12px;">${title} ${
this.loadingIcon
}</h3>
${message ? `<label>${message}</label>` : ""}
</div>
`);
}
} }
export class InputDialog extends InfoDialog { export class InputDialog extends InfoDialog {
@@ -367,7 +690,6 @@ export class InputDialog extends InfoDialog {
} }
} }
export class ConfirmDialog extends InfoDialog { export class ConfirmDialog extends InfoDialog {
callback = undefined; callback = undefined;
@@ -417,7 +739,7 @@ export class ConfirmDialog extends InfoDialog {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.callback = resolve; this.callback = resolve;
this.show(` this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset"> <div style="width: 100%; max-width: 600px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px;">${title}</h3> <h3 style="margin: 0px;">${title}</h3>
<label> <label>
${message} ${message}
@@ -429,9 +751,15 @@ export class ConfirmDialog extends InfoDialog {
} }
export const inputDialog = new InputDialog(); export const inputDialog = new InputDialog();
export const loadingDialog = new LoadingDialog();
export const infoDialog = new InfoDialog(); export const infoDialog = new InfoDialog();
export const confirmDialog = new ConfirmDialog(); export const confirmDialog = new ConfirmDialog();
/**
* Retrieves deployment data from local storage or defaults.
* @param {string} [environment] - The environment to get the data for.
* @returns {{endpoint: string, apiKey: string, displayName: string, environment?: string}} The deployment data.
*/
function getData(environment) { function getData(environment) {
const deployOption = const deployOption =
environment || localStorage.getItem("comfy_deploy_env") || "cloud"; environment || localStorage.getItem("comfy_deploy_env") || "cloud";
@@ -454,8 +782,21 @@ function getData(environment) {
}; };
} }
/**
* Retrieves deployment data from local storage or defaults.
* @param {{endpoint: string, apiKey: string, displayName: string, environment?: string}} [data] - The environment to get the data for.
*/
function saveData(data) {
localStorage.setItem(
"comfy_deploy_env_data_" + data.environment,
JSON.stringify(data),
);
}
export class ConfigDialog extends ComfyDialog { export class ConfigDialog extends ComfyDialog {
container = null; container = null;
poll = null;
timeout = null;
constructor() { constructor() {
super(); super();
@@ -498,22 +839,24 @@ export class ConfigDialog extends ComfyDialog {
close() { close() {
this.element.style.display = "none"; this.element.style.display = "none";
clearInterval(this.poll);
clearTimeout(this.timeout);
} }
save() { save(api_key, displayName) {
if (!displayName) displayName = getData().displayName;
const deployOption = this.container.querySelector("#deployOption").value; const deployOption = this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption); localStorage.setItem("comfy_deploy_env", deployOption);
const endpoint = this.container.querySelector("#endpoint").value; const endpoint = this.container.querySelector("#endpoint").value;
const apiKey = this.container.querySelector("#apiKey").value; const apiKey = api_key ?? this.container.querySelector("#apiKey").value;
const data = { saveData({
endpoint, endpoint,
apiKey, apiKey,
}; displayName,
localStorage.setItem( environment: deployOption,
"comfy_deploy_env_data_" + deployOption, });
JSON.stringify(data),
);
this.close(); this.close();
} }
@@ -527,12 +870,8 @@ export class ConfigDialog extends ComfyDialog {
<h3 style="margin: 0px;">Comfy Deploy Config</h3> <h3 style="margin: 0px;">Comfy Deploy Config</h3>
<label style="color: white; width: 100%;"> <label style="color: white; width: 100%;">
<select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" > <select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" >
<option value="cloud" ${ <option value="cloud" ${data.environment === "cloud" ? "selected" : ""}>Cloud</option>
data.environment === "cloud" ? "selected" : "" <option value="local" ${data.environment === "local" ? "selected" : ""}>Local</option>
}>Cloud</option>
<option value="local" ${
data.environment === "local" ? "selected" : ""
}>Local</option>
</select> </select>
</label> </label>
<label style="color: white; width: 100%;"> <label style="color: white; width: 100%;">
@@ -542,16 +881,61 @@ export class ConfigDialog extends ComfyDialog {
}"> }">
</label> </label>
<label style="color: white;"> <label style="color: white;">
API Key: API Key: ${data.displayName ?? ""}
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${ <input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
data.apiKey data.apiKey
}"> }">
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
${
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
}
</button>
</label> </label>
</div> </div>
`; `;
const button = this.container.querySelector("#loginButton");
button.onclick = () => {
const uuid =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
window.open(data.endpoint + "/auth-request/" + uuid, "_blank");
this.timeout = setTimeout(() => {
clearInterval(poll);
infoDialog.showMessage(
"Timeout",
"Wait too long for the response, please try re-login",
);
}, 30000); // Stop polling after 30 seconds
this.poll = setInterval(() => {
fetch(data.endpoint + "/api/auth-response/" + uuid)
.then((response) => response.json())
.then((json) => {
if (json.api_key) {
this.save(json.api_key, json.name);
this.container.querySelector("#apiKey").value = json.api_key;
infoDialog.show();
clearInterval(this.poll);
clearTimeout(this.timeout);
infoDialog.showMessage(
"Authenticated",
"You will be able to upload workflow to " + json.name,
);
}
})
.catch((error) => {
console.error("Error:", error);
clearInterval(this.poll);
clearTimeout(this.timeout);
infoDialog.showMessage("Error", error);
});
}, 2000);
};
const apiKeyInput = this.container.querySelector("#apiKey"); const apiKeyInput = this.container.querySelector("#apiKey");
apiKeyInput.addEventListener("paste", function (e) { apiKeyInput.addEventListener("paste", (e) => {
e.stopPropagation(); e.stopPropagation();
}); });
-6
View File
@@ -1,6 +0,0 @@
node_modules
**/node_modules
**/.next
**/public
packages/prisma/zod
apps/web/public/embed
-95
View File
@@ -1,95 +0,0 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [
// "plugin:playwright/playwright-test",
"next",
// "next/core-web-vitals",
"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",
// },
// },
],
};
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "comfyui_deploy"."workflow_runs" ADD COLUMN "started_at" timestamp;
-62
View File
@@ -1,62 +0,0 @@
DO $$ BEGIN
CREATE TYPE "model_upload_type" AS ENUM('civitai', 'huggingface', 'other');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
CREATE TYPE "resource_upload" AS ENUM('started', 'failed', 'succeded');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."checkpoints" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text,
"org_id" text,
"description" text,
"checkpoint_volume_id" uuid NOT NULL,
"model_name" text,
"civitai_id" text,
"civitai_version_id" text,
"civitai_url" text,
"civitai_download_url" text,
"civitai_model_response" jsonb,
"hf_url" text,
"s3_url" text,
"client_url" text,
"is_public" boolean DEFAULT false NOT NULL,
"status" "resource_upload" DEFAULT 'started' NOT NULL,
"upload_machine_id" text,
"upload_type" "model_upload_type" NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."checkpoint_volume" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text,
"org_id" text,
"volume_name" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"disabled" boolean DEFAULT false NOT NULL
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoints" ADD CONSTRAINT "checkpoints_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoints" ADD CONSTRAINT "checkpoints_checkpoint_volume_id_workflow_runs_id_fk" FOREIGN KEY ("checkpoint_volume_id") REFERENCES "comfyui_deploy"."workflow_runs"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "comfyui_deploy"."checkpoint_volume" ADD CONSTRAINT "checkpoint_volume_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "comfyui_deploy"."users"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-2
View File
@@ -1,2 +0,0 @@
ALTER TYPE "resource_upload" ADD VALUE 'error';--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."checkpoints" ADD COLUMN "build_log" text;
+2
View File
@@ -0,0 +1,2 @@
ALTER TABLE "comfyui_deploy"."deployments" ADD COLUMN "share_slug" text;--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."deployments" ADD CONSTRAINT "deployments_share_slug_unique" UNIQUE("share_slug");
+8
View File
@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS "comfyui_deploy"."auth_requests" (
"request_id" text PRIMARY KEY NOT NULL,
"user_id" text,
"org_id" text,
"api_hash" text,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "comfyui_deploy"."auth_requests" ADD COLUMN "expired_date" timestamp;
+7 -249
View File
@@ -1,5 +1,5 @@
{ {
"id": "4d5b29d0-848f-4c2e-a2cd-2932f1fa38c6", "id": "1ca4fdb7-c0c4-4c39-8b47-f40282293da0",
"prevId": "db06ea66-92c2-4ebe-93c1-6cb8a90ccd8b", "prevId": "db06ea66-92c2-4ebe-93c1-6cb8a90ccd8b",
"version": "5", "version": "5",
"dialect": "pg", "dialect": "pg",
@@ -88,238 +88,6 @@
} }
} }
}, },
"checkpoints": {
"name": "checkpoints",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"checkpoint_volume_id": {
"name": "checkpoint_volume_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"model_name": {
"name": "model_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_id": {
"name": "civitai_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_version_id": {
"name": "civitai_version_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_url": {
"name": "civitai_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_download_url": {
"name": "civitai_download_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_model_response": {
"name": "civitai_model_response",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hf_url": {
"name": "hf_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"s3_url": {
"name": "s3_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"client_url": {
"name": "client_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"is_public": {
"name": "is_public",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"status": {
"name": "status",
"type": "resource_upload",
"primaryKey": false,
"notNull": true,
"default": "'started'"
},
"upload_machine_id": {
"name": "upload_machine_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"upload_type": {
"name": "upload_type",
"type": "model_upload_type",
"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": {
"checkpoints_user_id_users_id_fk": {
"name": "checkpoints_user_id_users_id_fk",
"tableFrom": "checkpoints",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"checkpoints_checkpoint_volume_id_workflow_runs_id_fk": {
"name": "checkpoints_checkpoint_volume_id_workflow_runs_id_fk",
"tableFrom": "checkpoints",
"tableTo": "workflow_runs",
"columnsFrom": [
"checkpoint_volume_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"checkpoint_volume": {
"name": "checkpoint_volume",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"volume_name": {
"name": "volume_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()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {
"checkpoint_volume_user_id_users_id_fk": {
"name": "checkpoint_volume_user_id_users_id_fk",
"tableFrom": "checkpoint_volume",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": { "deployments": {
"name": "deployments", "name": "deployments",
"schema": "comfyui_deploy", "schema": "comfyui_deploy",
@@ -738,6 +506,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"default": "now()" "default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
} }
}, },
"indexes": {}, "indexes": {},
@@ -958,22 +732,6 @@
"comfy-deploy-serverless": "comfy-deploy-serverless" "comfy-deploy-serverless": "comfy-deploy-serverless"
} }
}, },
"model_upload_type": {
"name": "model_upload_type",
"values": {
"civitai": "civitai",
"huggingface": "huggingface",
"other": "other"
}
},
"resource_upload": {
"name": "resource_upload",
"values": {
"started": "started",
"failed": "failed",
"succeded": "succeded"
}
},
"workflow_run_origin": { "workflow_run_origin": {
"name": "workflow_run_origin", "name": "workflow_run_origin",
"values": { "values": {
+23 -257
View File
@@ -1,6 +1,6 @@
{ {
"id": "fed3e81d-c0d7-4deb-a63a-370039fb5edc", "id": "1425ee00-66fb-4541-8da7-19b217944545",
"prevId": "4d5b29d0-848f-4c2e-a2cd-2932f1fa38c6", "prevId": "1ca4fdb7-c0c4-4c39-8b47-f40282293da0",
"version": "5", "version": "5",
"dialect": "pg", "dialect": "pg",
"tables": { "tables": {
@@ -88,244 +88,6 @@
} }
} }
}, },
"checkpoints": {
"name": "checkpoints",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"checkpoint_volume_id": {
"name": "checkpoint_volume_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"model_name": {
"name": "model_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_id": {
"name": "civitai_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_version_id": {
"name": "civitai_version_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_url": {
"name": "civitai_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_download_url": {
"name": "civitai_download_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"civitai_model_response": {
"name": "civitai_model_response",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"hf_url": {
"name": "hf_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"s3_url": {
"name": "s3_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"client_url": {
"name": "client_url",
"type": "text",
"primaryKey": false,
"notNull": false
},
"is_public": {
"name": "is_public",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"status": {
"name": "status",
"type": "resource_upload",
"primaryKey": false,
"notNull": true,
"default": "'started'"
},
"upload_machine_id": {
"name": "upload_machine_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"upload_type": {
"name": "upload_type",
"type": "model_upload_type",
"primaryKey": false,
"notNull": true
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"checkpoints_user_id_users_id_fk": {
"name": "checkpoints_user_id_users_id_fk",
"tableFrom": "checkpoints",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"checkpoints_checkpoint_volume_id_workflow_runs_id_fk": {
"name": "checkpoints_checkpoint_volume_id_workflow_runs_id_fk",
"tableFrom": "checkpoints",
"tableTo": "workflow_runs",
"columnsFrom": [
"checkpoint_volume_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"checkpoint_volume": {
"name": "checkpoint_volume",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"volume_name": {
"name": "volume_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()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
}
},
"indexes": {},
"foreignKeys": {
"checkpoint_volume_user_id_users_id_fk": {
"name": "checkpoint_volume_user_id_users_id_fk",
"tableFrom": "checkpoint_volume",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": { "deployments": {
"name": "deployments", "name": "deployments",
"schema": "comfyui_deploy", "schema": "comfyui_deploy",
@@ -367,6 +129,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": true "notNull": true
}, },
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": { "description": {
"name": "description", "name": "description",
"type": "text", "type": "text",
@@ -456,7 +224,15 @@
} }
}, },
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
}, },
"machines": { "machines": {
"name": "machines", "name": "machines",
@@ -744,6 +520,12 @@
"primaryKey": false, "primaryKey": false,
"notNull": true, "notNull": true,
"default": "now()" "default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
} }
}, },
"indexes": {}, "indexes": {},
@@ -964,22 +746,6 @@
"comfy-deploy-serverless": "comfy-deploy-serverless" "comfy-deploy-serverless": "comfy-deploy-serverless"
} }
}, },
"model_upload_type": {
"name": "model_upload_type",
"values": {
"civitai": "civitai",
"huggingface": "huggingface",
"other": "other"
}
},
"resource_upload": {
"name": "resource_upload",
"values": {
"started": "started",
"error": "error",
"succeded": "succeded"
}
},
"workflow_run_origin": { "workflow_run_origin": {
"name": "workflow_run_origin", "name": "workflow_run_origin",
"values": { "values": {
+824
View File
@@ -0,0 +1,824 @@
{
"id": "97662b25-3992-4859-9bdc-560e2a70daea",
"prevId": "1425ee00-66fb-4541-8da7-19b217944545",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"auth_requests": {
"name": "auth_requests",
"schema": "comfyui_deploy",
"columns": {
"request_id": {
"name": "request_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"api_hash": {
"name": "api_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
+830
View File
@@ -0,0 +1,830 @@
{
"id": "8d654f92-7f7e-420f-bbd3-73b6b27adf35",
"prevId": "97662b25-3992-4859-9bdc-560e2a70daea",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"auth_requests": {
"name": "auth_requests",
"schema": "comfyui_deploy",
"columns": {
"request_id": {
"name": "request_id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"api_hash": {
"name": "api_hash",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"expired_date": {
"name": "expired_date",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}
+18 -4
View File
@@ -222,15 +222,29 @@
{ {
"idx": 31, "idx": 31,
"version": "5", "version": "5",
"when": 1705975916818, "when": 1705763980972,
"tag": "0031_safe_multiple_man", "tag": "0031_fast_lyja",
"breakpoints": true "breakpoints": true
}, },
{ {
"idx": 32, "idx": 32,
"version": "5", "version": "5",
"when": 1705979098372, "when": 1705806921697,
"tag": "0032_material_wallflower", "tag": "0032_shallow_vermin",
"breakpoints": true
},
{
"idx": 33,
"version": "5",
"when": 1705853314500,
"tag": "0033_awesome_human_fly",
"breakpoints": true
},
{
"idx": 34,
"version": "5",
"when": 1705902960991,
"tag": "0034_even_lady_ursula",
"breakpoints": true "breakpoints": true
} }
] ]
+12 -7
View File
@@ -12,26 +12,31 @@ let sslMode: string | boolean = process.env.SSL || "require";
if (sslMode === "false") sslMode = false; if (sslMode === "false") sslMode = false;
console.log(migrationsFolderName, sslMode); let connectionString = process.env.POSTGRES_URL!;
const isDevContainer = process.env.REMOTE_CONTAINERS !== undefined;
if (isDevContainer)
connectionString = connectionString.replace(
"localhost",
"host.docker.internal",
);
const connectionString = process.env.POSTGRES_URL!;
console.log(connectionString);
const sql = postgres(connectionString, { max: 1, ssl: sslMode as any }); const sql = postgres(connectionString, { max: 1, ssl: sslMode as any });
const db = drizzle(sql, { const db = drizzle(sql, {
logger: true, logger: true,
}); });
let retries = 5; let retries = 5;
while(retries) { while (retries) {
try { try {
await sql`SELECT NOW()`; await sql`SELECT NOW()`;
console.log('Database is live'); console.log("Database is live");
break; break;
} catch (error) { } catch (error) {
console.error('Database is not live yet', error); console.error("Database is not live yet", error);
retries -= 1; retries -= 1;
console.log(`Retries left: ${retries}`); console.log(`Retries left: ${retries}`);
await new Promise(res => setTimeout(res, 1000)); await new Promise((res) => setTimeout(res, 1000));
} }
} }
+4 -11
View File
@@ -12,7 +12,8 @@
"migrate-production": "bun run migrate.mts", "migrate-production": "bun run migrate.mts",
"migrate-local": "SSL=false LOCAL=true bun run migrate.mts", "migrate-local": "SSL=false LOCAL=true bun run migrate.mts",
"db-up": "docker-compose up", "db-up": "docker-compose up",
"db-dev": "bun run db-up && bun run migrate-local" "db-dev": "bun run db-up && bun run migrate-local",
"lint:fix": "bunx @biomejs/biome lint --apply ./src"
}, },
"dependencies": { "dependencies": {
"@algolia/autocomplete-core": "^1.13.0", "@algolia/autocomplete-core": "^1.13.0",
@@ -71,6 +72,7 @@
"mdx-annotations": "^0.1.4", "mdx-annotations": "^0.1.4",
"million": "latest", "million": "latest",
"mitata": "^0.1.6", "mitata": "^0.1.6",
"ms": "^2.1.3",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"next": "14.1", "next": "14.1",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
@@ -105,26 +107,17 @@
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@trivago/prettier-plugin-sort-imports": "4.1.1", "@biomejs/biome": "1.5.2",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"autoprefixer": "^10.0.1", "autoprefixer": "^10.0.1",
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"drizzle-kit": "^0.20.6", "drizzle-kit": "^0.20.6",
"eslint": "8.34.0", "eslint": "8.34.0",
"eslint-config-next": "^14.0.4",
"eslint-config-prettier": "^8.6.0",
"eslint-config-turbo": "latest",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unused-imports": "^3.0.0",
"postcss": "^8", "postcss": "^8",
"postgres": "^3.4.3", "postgres": "^3.4.3",
"prettier": "2.8.6",
"prettier-plugin-tailwindcss": "0.2.5",
"sharp": "^0.33.1", "sharp": "^0.33.1",
"tailwindcss": "^3.3.0", "tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

+42 -11
View File
@@ -1,4 +1,3 @@
import { app } from "../../../../routes/app";
import { registerCreateRunRoute } from "@/routes/registerCreateRunRoute"; import { registerCreateRunRoute } from "@/routes/registerCreateRunRoute";
import { registerGetOutputRoute } from "@/routes/registerGetOutputRoute"; import { registerGetOutputRoute } from "@/routes/registerGetOutputRoute";
import { registerUploadRoute } from "@/routes/registerUploadRoute"; import { registerUploadRoute } from "@/routes/registerUploadRoute";
@@ -6,7 +5,11 @@ import { isKeyRevoked } from "@/server/curdApiKeys";
import { parseJWT } from "@/server/parseJWT"; import { parseJWT } from "@/server/parseJWT";
import type { Context, Next } from "hono"; import type { Context, Next } from "hono";
import { handle } from "hono/vercel"; import { handle } from "hono/vercel";
import { app } from "../../../../routes/app";
import { registerWorkflowUploadRoute } from "@/routes/registerWorkflowUploadRoute";
import { registerGetAuthResponse } from "@/routes/registerGetAuthResponse";
import { registerGetWorkflowRoute } from "@/routes/registerGetWorkflow";
import { cors } from "hono/cors";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export const maxDuration = 300; // 5 minutes export const maxDuration = 300; // 5 minutes
@@ -16,14 +19,24 @@ declare module "hono" {
} }
} }
async function checkAuth(c: Context, next: Next) { async function checkAuth(c: Context, next: Next, headers?: HeadersInit) {
const token = c.req.raw.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token" const token = c.req.raw.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token"
const userData = token ? parseJWT(token) : undefined; const userData = token ? parseJWT(token) : undefined;
if (!userData || token === undefined) { if (!userData || token === undefined) {
return c.text("Invalid or expired token", 401); return c.text("Invalid or expired token", {
} else { status: 401,
headers: headers,
});
}
// If the key has expiration, this is a temporary key and not in our db, so we can skip checking
if (userData.exp === undefined) {
const revokedKey = await isKeyRevoked(token); const revokedKey = await isKeyRevoked(token);
if (revokedKey) return c.text("Revoked token", 401); if (revokedKey)
return c.text("Revoked token", {
status: 401,
headers: headers,
});
} }
c.set("apiKeyTokenData", userData); c.set("apiKeyTokenData", userData);
@@ -31,18 +44,35 @@ async function checkAuth(c: Context, next: Next) {
await next(); await next();
} }
app.use("/run", async (c, next) => { app.use("/run", checkAuth);
return checkAuth(c, next); app.use("/upload-url", checkAuth);
const corsHandler = cors({
origin: "*",
allowHeaders: ["Authorization", "Content-Type"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}); });
app.use("/upload-url", async (c, next) => { // CORS Check
return checkAuth(c, next); app.use("/workflow", corsHandler, checkAuth);
}); app.use("/workflow-version/*", corsHandler, checkAuth);
// create run endpoint
registerCreateRunRoute(app); registerCreateRunRoute(app);
registerGetOutputRoute(app); registerGetOutputRoute(app);
// file upload endpoint
registerUploadRoute(app); registerUploadRoute(app);
// Anon
registerGetAuthResponse(app);
registerWorkflowUploadRoute(app);
registerGetWorkflowRoute(app);
// The OpenAPI documentation will be available at /doc // The OpenAPI documentation will be available at /doc
app.doc("/doc", { app.doc("/doc", {
openapi: "3.0.0", openapi: "3.0.0",
@@ -69,3 +99,4 @@ const handler = handle(app);
export const GET = handler; export const GET = handler;
export const POST = handler; export const POST = handler;
export const OPTIONS = handler;
+4 -3
View File
@@ -1,11 +1,12 @@
import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { handleResourceUpload } from "@/server/resource"; import { handleResourceUpload } from "@/server/resource";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { z } from "zod"; import { z } from "zod";
import { parseDataSafe } from "../../../../lib/parseDataSafe";
const Request = z.object({ const Request = z.object({
file_name: z.string(), file_name: z.string(),
run_id: z.string(), run_id: z.string(),
type: z.string(), type: z.string(),
}); });
@@ -29,7 +30,7 @@ export async function GET(request: Request) {
{ {
url: uploadUrl, url: uploadUrl,
}, },
{ status: 200 } { status: 200 },
); );
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = const errorMessage =
@@ -38,7 +39,7 @@ export async function GET(request: Request) {
{ {
error: errorMessage, error: errorMessage,
}, },
{ status: 500 } { status: 500 },
); );
} }
} }
+21 -63
View File
@@ -1,17 +1,14 @@
import { createNewWorkflow } from "../../../../server/createNewWorkflow"; import { snapshotType, workflowAPIType, workflowType } from "@/db/schema";
import { parseJWT } from "../../../../server/parseJWT";
import { db } from "@/db/db";
import {
snapshotType,
workflowAPIType,
workflowTable,
workflowType,
workflowVersionTable,
} from "@/db/schema";
import { parseDataSafe } from "@/lib/parseDataSafe"; import { parseDataSafe } from "@/lib/parseDataSafe";
import { eq, sql } from "drizzle-orm";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { z } from "zod"; import { z } from "zod";
import {
createNewWorkflow,
createNewWorkflowVersion,
} from "../../../../server/createNewWorkflow";
import { parseJWT } from "../../../../server/parseJWT";
// This is will be deprecated
const corsHeaders = { const corsHeaders = {
"Access-Control-Allow-Origin": "*", "Access-Control-Allow-Origin": "*",
@@ -55,7 +52,7 @@ export async function POST(request: Request) {
const [data, error] = await parseDataSafe( const [data, error] = await parseDataSafe(
UploadRequest, UploadRequest,
request, request,
corsHeaders corsHeaders,
); );
if (!data || error) return error; if (!data || error) return error;
@@ -75,7 +72,7 @@ export async function POST(request: Request) {
// Case 1 new workflow // Case 1 new workflow
try { try {
if ((!workflow_id || workflow_id.length == 0) && workflow_name) { if ((!workflow_id || workflow_id.length === 0) && workflow_name) {
// Create a new parent workflow // Create a new parent workflow
const { workflow_id: _workflow_id, version: _version } = const { workflow_id: _workflow_id, version: _version } =
await createNewWorkflow({ await createNewWorkflow({
@@ -91,56 +88,17 @@ export async function POST(request: Request) {
workflow_id = _workflow_id; workflow_id = _workflow_id;
version = _version; version = _version;
// const workflow_parent = await db
// .insert(workflowTable)
// .values({
// user_id,
// name: workflow_name,
// org_id: org_id,
// })
// .returning();
// workflow_id = workflow_parent[0].id;
// // Create a new version
// const data = await db
// .insert(workflowVersionTable)
// .values({
// workflow_id: workflow_id,
// workflow,
// workflow_api,
// version: 1,
// snapshot: snapshot,
// })
// .returning();
// version = data[0].version;
} else if (workflow_id) { } else if (workflow_id) {
// Case 2 update workflow // Case 2 update workflow
const data = await db const { version: _version } = await createNewWorkflowVersion({
.insert(workflowVersionTable) workflow_id: workflow_id,
.values({ workflowData: {
workflow_id, workflow,
workflow: workflow,
workflow_api, workflow_api,
// version: sql`${workflowVersionTable.version} + 1`, snapshot,
snapshot: snapshot, },
version: sql`( });
SELECT COALESCE(MAX(version), 0) + 1 version = _version;
FROM ${workflowVersionTable}
WHERE workflow_id = ${workflow_id}
)`,
})
.returning();
version = data[0].version;
await db
.update(workflowTable)
.set({
updated_at: new Date(),
})
.where(eq(workflowTable.id, workflow_id))
.returning();
} else { } else {
return NextResponse.json( return NextResponse.json(
{ {
@@ -150,7 +108,7 @@ export async function POST(request: Request) {
status: 500, status: 500,
statusText: "Invalid request", statusText: "Invalid request",
headers: corsHeaders, headers: corsHeaders,
} },
); );
} }
} catch (error: any) { } catch (error: any) {
@@ -162,7 +120,7 @@ export async function POST(request: Request) {
status: 500, status: 500,
statusText: "Invalid request", statusText: "Invalid request",
headers: corsHeaders, headers: corsHeaders,
} },
); );
} }
@@ -174,6 +132,6 @@ export async function POST(request: Request) {
{ {
status: 200, status: 200,
headers: corsHeaders, headers: corsHeaders,
} },
); );
} }
@@ -1,50 +0,0 @@
import { parseDataSafe } from "../../../../lib/parseDataSafe";
import { db } from "@/db/db";
import { checkpointTable, machinesTable } from "@/db/schema";
import { eq } from "drizzle-orm";
import { NextResponse } from "next/server";
import { z } from "zod";
const Request = z.object({
machine_id: z.string(),
endpoint: z.string().optional(),
build_log: z.string().optional(),
});
export async function POST(request: Request) {
const [data, error] = await parseDataSafe(Request, request);
if (!data || error) return error;
// console.log(data);
const { machine_id, endpoint, build_log } = data;
if (endpoint) {
await db
.update(checkpointTable)
.set({
// status: "ready",
// endpoint: endpoint,
// build_log: build_log,
})
.where(eq(machinesTable.id, machine_id));
} else {
// console.log(data);
await db
.update(machinesTable)
.set({
// status: "error",
// build_log: build_log,
})
.where(eq(machinesTable.id, machine_id));
}
return NextResponse.json(
{
message: "success",
},
{
status: 200,
}
);
}
@@ -0,0 +1,54 @@
import { ButtonAction } from "@/components/ButtonActionLoader";
import { Button } from "@/components/ui/button";
import { createAuthRequest } from "@/server/curdApiKeys";
import { auth } from "@clerk/nextjs";
import { redirect } from "next/navigation";
import { getOrgOrUserDisplayName } from "../../../../server/getOrgOrUserDisplayName";
import { db } from "@/db/db";
import { eq } from "drizzle-orm";
import { authRequestsTable } from "@/db/schema";
export default async function Home({
params,
}: {
params: { request_id: string };
}) {
const { userId, orgId } = await auth();
if (!userId) redirect("/");
if (!params.request_id)
return (
<div className="h-full w-full flex flex-col gap-2 items-center justify-center">
No valid request_id
</div>
);
const existingResult = await db.query.authRequestsTable.findFirst({
where: eq(authRequestsTable.request_id, params.request_id),
});
if (existingResult?.api_hash) {
return (
<div className="h-full w-full flex flex-col gap-2 items-center justify-center">
Request already consumed.
</div>
);
}
const userName = await getOrgOrUserDisplayName(orgId, userId);
return (
<div className="h-full w-full flex flex-col gap-2 items-center justify-center">
<div className="text-lg">Grant API Access to {userName}</div>
<Button asChild>
<ButtonAction
routerAction="do-nothing"
action={createAuthRequest.bind(null, params.request_id)}
>
Grant Access
</ButtonAction>
</Button>
</div>
);
}
+117
View File
@@ -0,0 +1,117 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import Image from "next/image";
import Link from "next/link";
export default function Page() {
return <Examples />;
}
type exampleWorkflow = {
title: string;
description: string;
previewURL: string;
image: {
src: string,
alt: string,
};
};
const exampleWorkflows: exampleWorkflow[] = [
{
title: "Txt2Img SDXL",
description: "The basic workflow, type a prompt and generate images based on that.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-txt2img-sdxl',
image: {
src: '/example-workflows/txt2img.webp',
alt: 'IPAdapter workflow',
}
},
{
title: "Txt2Img LCM SDXL",
description: "Images in a couple of seconds, increase the speed of each generation using LCM Lora.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-lcm-sdxl',
image: {
src: '/example-workflows/txt2img-lcm.webp',
alt: 'txt2img LCM SDXL',
}
},
{
title: "IPAdapter SDXL",
description: "Load images and use them as reference for new generations.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-ip-adapter-sdxl',
image: {
src: '/example-workflows/ipadapter.webp',
alt: 'IPAdapter workflow',
}
},
{
title: "Upscale and Add Detail SDXL",
description: "Upscale and Add Details to your creations.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-upscale-and-add-detail-sdxl',
image: {
src: '/example-workflows/upscale.webp',
alt: 'Upscale and Add Detail SDXL',
}
},
{
title: "Txt2Img SDXL Turbo",
description: "Try SDXL turbo and generate images since 1 step in seconds.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-txt2img-sdxl-turbo',
image: {
src: '/example-workflows/txt2img-sdxl-turbo.webp',
alt: 'Txt2Img SDXL Turbo',
}
},
{
title: "Img2Img SDXL Controlnet",
description: "This workflow uses canny. Generate lines of you original image and create variations.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-img2-img-sdxl-controlnet',
image: {
src: '/example-workflows/txt2img-controlnet.webp',
alt: 'Img2Img SDXL Controlnet',
}
},
{
title: "Automatic Inpainting (SEG)",
description: "Type what do you want to select and change that area with your prompt.",
previewURL: 'https://www.comfydeploy.com/share/comfy-deploy-example-automatic-inpainting-clip-seg',
image: {
src: '/example-workflows/automatic-inpainting-seg.webp',
alt: 'Img2Img SDXL Controlnet',
}
}
];
async function Examples() {
return (
<div className="w-full py-4">
<section className="mx-auto flex max-w-[980px] flex-col items-center gap-2 py-8 md:py-12 md:pb-8 lg:py-24 lg:pb-20">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl text-center">
Check out some examples
</h1>
<p className="max-w-[560px] text-center text-lg text-muted-foreground">Text to Image, Image to Image, IPAdapter, and more. Here are some examples that you can use to deploy your workflow.</p>
</section>
<section className="flex justify-center flex-wrap gap-5">
{exampleWorkflows.map(workflow => {
return <Card className="w-[350px]">
<CardHeader>
<CardTitle>{workflow.title}</CardTitle>
<CardDescription>{workflow.description}</CardDescription>
</CardHeader>
<CardContent>
<Image src={workflow.image.src} alt={workflow.image.alt} width={350} height={230} />
</CardContent>
<CardFooter className="flex justify-end gap-2">
<Button asChild>
<Link href={workflow.previewURL}>View Workflow</Link>
</Button>
</CardFooter>
</Card>;
})}
</section>
</div>
);
}
+3 -1
View File
@@ -80,7 +80,9 @@
/* @apply rounded-lg p-2 overflow-x-scroll */ /* @apply rounded-lg p-2 overflow-x-scroll */
@apply p-2 max-w-full overflow-auto w-full @apply p-2 max-w-full overflow-auto w-full
} }
.vsc-controller{
position: absolute;
}
@layer base { @layer base {
* { * {
@apply border-border; @apply border-border;
+79 -81
View File
@@ -2,11 +2,11 @@ import { ButtonActionMenu } from "@/components/ButtonActionLoader";
import { RunWorkflowInline } from "@/components/RunWorkflowInline"; import { RunWorkflowInline } from "@/components/RunWorkflowInline";
import { PublicRunOutputs } from "@/components/VersionSelect"; import { PublicRunOutputs } from "@/components/VersionSelect";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { usersTable } from "@/db/schema"; import { usersTable } from "@/db/schema";
@@ -14,9 +14,9 @@ import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import { getRelativeTime } from "@/lib/getRelativeTime"; import { getRelativeTime } from "@/lib/getRelativeTime";
import { setInitialUserData } from "@/lib/setInitialUserData"; import { setInitialUserData } from "@/lib/setInitialUserData";
import { import {
cloneMachine, cloneMachine,
cloneWorkflow, cloneWorkflow,
findSharedDeployment, findSharedDeployment,
} from "@/server/curdDeploments"; } from "@/server/curdDeploments";
import { auth, clerkClient } from "@clerk/nextjs/server"; import { auth, clerkClient } from "@clerk/nextjs/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -25,89 +25,87 @@ import { redirect } from "next/navigation";
export const maxDuration = 300; // 5 minutes export const maxDuration = 300; // 5 minutes
export default async function Page({ export default async function Page({
params, params,
}: { }: {
params: { share_id: string }; params: { share_id: string };
}) { }) {
const { userId } = await auth(); const { userId } = await auth();
// If there is user, check if the user data is present // If there is user, check if the user data is present
if (userId) { if (userId) {
const user = await db.query.usersTable.findFirst({ const user = await db.query.usersTable.findFirst({
where: eq(usersTable.id, userId), where: eq(usersTable.id, userId),
}); });
if (!user) { if (!user) {
await setInitialUserData(userId); await setInitialUserData(userId);
} }
} }
const sharedDeployment = await findSharedDeployment(params.share_id); const sharedDeployment = await findSharedDeployment(params.share_id);
if (!sharedDeployment) return redirect("/"); if (!sharedDeployment) return redirect("/");
const userName = sharedDeployment.workflow.org_id const userName = sharedDeployment.workflow.org_id
? await clerkClient.organizations ? await clerkClient.organizations
.getOrganization({ .getOrganization({
organizationId: sharedDeployment.workflow.org_id, organizationId: sharedDeployment.workflow.org_id,
}) })
.then((x) => x.name) .then((x) => x.name)
: sharedDeployment.user.name; : sharedDeployment.user.name;
const inputs = getInputsFromWorkflow(sharedDeployment.version); const inputs = getInputsFromWorkflow(sharedDeployment.version);
return ( return (
<div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]"> <div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]">
<Card className="w-full h-fit mt-4"> <Card className="w-full h-fit mt-4">
<CardHeader> <CardHeader>
<CardTitle className="flex justify-between items-center"> <CardTitle className="flex justify-between items-center">
<div> <div>
{userName} {userName}
{" / "} {" / "}
{sharedDeployment.workflow.name} {sharedDeployment.workflow.name}
</div> </div>
<ButtonActionMenu <ButtonActionMenu
title="Clone" title="Clone"
actions={[ actions={[
{ {
title: "Workflow", title: "Workflow",
action: cloneWorkflow.bind(null, sharedDeployment.id), action: cloneWorkflow.bind(null, sharedDeployment.id),
}, },
{ {
title: "Machine", title: "Machine",
action: cloneMachine.bind(null, sharedDeployment.id), action: cloneMachine.bind(null, sharedDeployment.id),
}, },
]} ]}
/> />
</CardTitle> </CardTitle>
<CardDescription suppressHydrationWarning={true}> <CardDescription suppressHydrationWarning={true}>
{getRelativeTime(sharedDeployment?.updated_at)} {getRelativeTime(sharedDeployment?.updated_at)}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div> <div>
{sharedDeployment?.description && ( {sharedDeployment?.description && sharedDeployment?.description}
<>{sharedDeployment?.description}</> </div>
)} <RunWorkflowInline
</div> inputs={inputs}
<RunWorkflowInline machine_id={sharedDeployment.machine_id}
inputs={inputs} workflow_version_id={sharedDeployment.workflow_version_id}
machine_id={sharedDeployment.machine_id} />
workflow_version_id={sharedDeployment.workflow_version_id} </CardContent>
/> </Card>
</CardContent> <Card className="w-full h-fit mt-4">
</Card> <CardHeader>
<Card className="w-full h-fit mt-4"> <CardDescription>Run outputs</CardDescription>
<CardHeader> </CardHeader>
<CardDescription>Run outputs</CardDescription>
</CardHeader>
<CardContent> <CardContent>
<PublicRunOutputs preview={sharedDeployment.showcase_media} /> <PublicRunOutputs preview={sharedDeployment.showcase_media} />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );
} }
-35
View File
@@ -1,35 +0,0 @@
import { setInitialUserData } from "../../../lib/setInitialUserData";
import { auth } from "@clerk/nextjs";
import { clerkClient } from "@clerk/nextjs/server";
import { CheckpointList } from "@/components/CheckpointList"
import { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
export default function Page() {
return <CheckpointListServer />;
}
async function CheckpointListServer() {
const { userId } = auth();
if (!userId) {
return <div>No auth</div>;
}
const user = await clerkClient.users.getUser(userId);
if (!user) {
await setInitialUserData(userId);
}
const checkpoints = await getAllUserCheckpoints()
if (!checkpoints) {
return <div>No checkpoints found</div>;
}
return (
<div className="w-full">
<CheckpointList data={checkpoints}/>
</div>
);
}
@@ -1,10 +1,11 @@
import { CreateShareButton } from "@/components/CreateShareButton";
import { MachinesWSMain } from "@/components/MachinesWS"; import { MachinesWSMain } from "@/components/MachinesWS";
import { VersionDetails } from "@/components/VersionDetails"; import { VersionDetails } from "@/components/VersionDetails";
import { import {
CopyWorkflowVersion, CopyWorkflowVersion,
CreateDeploymentButton, CreateDeploymentButton,
CreateShareButton,
MachineSelect, MachineSelect,
OpenEditButton,
RunWorkflowButton, RunWorkflowButton,
VersionSelect, VersionSelect,
ViewWorkflowDetailsButton, ViewWorkflowDetailsButton,
@@ -48,6 +49,7 @@ export default async function Page({
<CreateShareButton workflow={workflow} machines={machines} /> <CreateShareButton workflow={workflow} machines={machines} />
<CopyWorkflowVersion workflow={workflow} /> <CopyWorkflowVersion workflow={workflow} />
<ViewWorkflowDetailsButton workflow={workflow} /> <ViewWorkflowDetailsButton workflow={workflow} />
<OpenEditButton workflow={workflow} machines={machines} />
</div> </div>
<VersionDetails workflow={workflow} /> <VersionDetails workflow={workflow} />
+1 -1
View File
@@ -1,5 +1,5 @@
import { setInitialUserData } from "../../../lib/setInitialUserData"; import { setInitialUserData } from "../../../lib/setInitialUserData";
import { getAllUserWorkflow } from "../../../server/getAllUserWorkflow"; import { getAllUserWorkflow } from "../../../server/crudWorkflow";
import { WorkflowList } from "@/components/WorkflowList"; import { WorkflowList } from "@/components/WorkflowList";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { usersTable } from "@/db/schema"; import { usersTable } from "@/db/schema";
+6 -1
View File
@@ -17,9 +17,11 @@ import { useState } from "react";
export function ButtonAction({ export function ButtonAction({
action, action,
children, children,
routerAction = "back",
...rest ...rest
}: { }: {
action: () => Promise<any>; action: () => Promise<any>;
routerAction?: "refresh" | "back" | "do-nothing";
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
@@ -34,7 +36,10 @@ export function ButtonAction({
await callServerPromise(action()); await callServerPromise(action());
setPending(false); setPending(false);
router.refresh(); if (routerAction === "back") {
router.back();
router.refresh();
} else if (routerAction === "refresh") router.refresh();
}} }}
{...rest} {...rest}
> >
-315
View File
@@ -1,315 +0,0 @@
"use client";
import { getRelativeTime } from "../lib/getRelativeTime";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { InsertModal, UpdateModal } from "./InsertModal";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
import { addCivitaiCheckpoint } from "@/server/curdCheckpoint";
import { addCivitaiCheckpointSchema } from "@/server/addCheckpointSchema";
export type CheckpointItemList = NonNullable<
Awaited<ReturnType<typeof getAllUserCheckpoints>>
>[0];
export const columns: ColumnDef<CheckpointItemList>[] = [
{
accessorKey: "id",
id: "select",
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "name",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
const checkpoint = row.original;
return (
<a
className="hover:underline flex gap-2"
href={`/storage/${checkpoint.id}`} // TODO
>
<span className="truncate max-w-[200px]">{row.original.model_name}</span>
<Badge variant="default">{}</Badge>
{checkpoint.is_public
? <Badge variant="success">Public</Badge>
: <Badge variant="teal">Private</Badge>}
</a>
);
},
},
{
accessorKey: "creator",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Creator
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
// return <Badge variant="cyan">{row?.original?.user?.name ? row.original.user.name : "Public"}</Badge>;
},
},
{
accessorKey: "date",
sortingFn: "datetime",
enableSorting: true,
header: ({ column }) => {
return (
<button
className="w-full flex items-center justify-end hover:underline truncate"
// variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Update Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => (
<div className="w-full capitalize text-right truncate">
{getRelativeTime(row.original.updated_at)}
</div>
),
},
// {
// id: "actions",
// enableHiding: false,
// cell: ({ row }) => {
// const checkpoint = row.original;
//
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" className="h-8 w-8 p-0">
// <span className="sr-only">Open menu</span>
// <MoreHorizontal className="h-4 w-4" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
// <DropdownMenuItem
// className="text-destructive"
// onClick={() => {
// deleteWorkflow(checkpoint.id);
// }}
// >
// Delete Workflow
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
// },
// },
];
export function CheckpointList({ data }: { data: CheckpointItemList[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[],
);
const [columnVisibility, setColumnVisibility] = React.useState<
VisibilityState
>({});
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 (
<div className="grid grid-rows-[auto,1fr,auto] h-full">
<div className="flex flex-row w-full items-center py-4">
<Input
placeholder="Filter workflows..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)}
className="max-w-sm"
/>
<div className="ml-auto flex gap-2">
<InsertModal
dialogClassName="sm:max-w-[600px]"
disabled={
false
// TODO: limitations based on plan
}
tooltip={"Add models using their civitai url!"}
title="Civitai Checkpoint"
description="Pick a model from civitai"
serverAction={addCivitaiCheckpoint}
formSchema={addCivitaiCheckpointSchema}
fieldConfig={{
civitai_url: {
fieldType: "fallback",
// fieldType: "fallback",
inputProps: { required: true },
description: (
<>
Pick a checkpoint from{" "}
<a
href="https://www.civitai.com/models"
target="_blank"
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
>
civitai.com
</a>{" "}
and place it's url here
</>
),
},
}}
/>
</div>
</div>
<ScrollArea className="h-full w-full rounded-md border">
<Table>
<TableHeader className="bg-background top-0 sticky">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length
? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)
: (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ScrollArea>
<div className="flex flex-row items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
+66
View File
@@ -0,0 +1,66 @@
"use client";
import { LoadingIcon } from "@/components/LoadingIcon";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { Share } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useState } from "react";
import { useSelectedMachine } from "./VersionSelect";
import { callServerPromise } from "./callServerPromise";
export function CreateShareButton({
workflow,
machines,
}: {
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>;
machines: Awaited<ReturnType<typeof getMachines>>;
}) {
const [version] = useQueryState("version", {
defaultValue: workflow?.versions[0].version ?? 1,
...parseAsInteger,
});
const [machine] = useSelectedMachine(machines);
const [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find(
(x) => x.version == version,
)?.id;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" disabled={isLoading} variant="outline">
Share {isLoading ? <LoadingIcon /> : <Share size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={async () => {
if (!workflow_version_id) return;
setIsLoading(true);
await callServerPromise(
createDeployments(
workflow.id,
workflow_version_id,
machine,
"public-share",
),
);
setIsLoading(false);
}}
>
Public
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+11 -10
View File
@@ -1,4 +1,3 @@
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
import { CodeBlock } from "@/components/CodeBlock"; import { CodeBlock } from "@/components/CodeBlock";
import { import {
Dialog, Dialog,
@@ -13,6 +12,7 @@ import { TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow"; import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import type { findAllDeployments } from "@/server/findAllRuns"; import type { findAllDeployments } from "@/server/findAllRuns";
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
const curlTemplate = ` const curlTemplate = `
curl --request POST \ curl --request POST \
@@ -91,7 +91,7 @@ export function DeploymentDisplay({
}) { }) {
const workflowInput = getInputsFromWorkflow(deployment.version); const workflowInput = getInputsFromWorkflow(deployment.version);
if (deployment.environment == "public-share") { if (deployment.environment === "public-share") {
return <SharePageDeploymentRow deployment={deployment} />; return <SharePageDeploymentRow deployment={deployment} />;
} }
@@ -123,6 +123,7 @@ export function DeploymentDisplay({
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts" href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
className="text-blue-500 hover:underline" className="text-blue-500 hover:underline"
target="_blank" target="_blank"
rel="noreferrer"
> >
here here
</a> </a>
@@ -135,7 +136,7 @@ export function DeploymentDisplay({
: jsClientSetupTemplate, : jsClientSetupTemplate,
deployment, deployment,
domain, domain,
workflowInput workflowInput,
)} )}
/> />
Create a run via deployment id Create a run via deployment id
@@ -147,7 +148,7 @@ export function DeploymentDisplay({
: jsClientCreateRunNoInputsTemplate, : jsClientCreateRunNoInputsTemplate,
deployment, deployment,
domain, domain,
workflowInput workflowInput,
)} )}
/> />
Check the status of the run, and retrieve the outputs Check the status of the run, and retrieve the outputs
@@ -156,7 +157,7 @@ export function DeploymentDisplay({
code={formatCode( code={formatCode(
clientTemplate_checkStatus, clientTemplate_checkStatus,
deployment, deployment,
domain domain,
)} )}
/> />
</TabsContent> </TabsContent>
@@ -194,7 +195,7 @@ function formatCode(
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0], deployment: Awaited<ReturnType<typeof findAllDeployments>>[0],
domain: string, domain: string,
inputs?: ReturnType<typeof getInputsFromWorkflow>, inputs?: ReturnType<typeof getInputsFromWorkflow>,
inputsTabs?: number inputsTabs?: number,
) { ) {
if (inputs && inputs.length > 0) { if (inputs && inputs.length > 0) {
codeTemplate = codeTemplate.replace( codeTemplate = codeTemplate.replace(
@@ -203,20 +204,20 @@ function formatCode(
Object.fromEntries( Object.fromEntries(
inputs.map((x) => { inputs.map((x) => {
return [x?.input_id, ""]; return [x?.input_id, ""];
}) }),
), ),
null, null,
2 2,
) )
.split("\n") .split("\n")
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line .map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
.join("\n")}` .join("\n")}`,
); );
} else { } else {
codeTemplate = codeTemplate.replace( codeTemplate = codeTemplate.replace(
` `
inputs: {}`, inputs: {}`,
"" "",
); );
} }
return codeTemplate return codeTemplate
+46 -44
View File
@@ -6,55 +6,57 @@ import type { findAllDeployments } from "@/server/findAllRuns";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export function SharePageDeploymentRow({ export function SharePageDeploymentRow({
deployment, deployment,
}: { }: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]; deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) { }) {
const router = useRouter(); const router = useRouter();
return ( return (
<TableRow <TableRow
className="appearance-none hover:cursor-pointer" className="appearance-none hover:cursor-pointer"
onClick={() => { onClick={() => {
if (deployment.environment == "public-share") { if (deployment.environment === "public-share") {
router.push(`/share/${deployment.id}/settings`); router.push(
} `/share/${deployment.share_slug ?? deployment.id}/settings`,
}} );
> }
<TableCell className="capitalize truncate"> }}
{deployment.environment} >
</TableCell> <TableCell className="capitalize truncate">
<TableCell className="font-medium truncate"> {deployment.environment}
{deployment.version?.version} </TableCell>
</TableCell> <TableCell className="font-medium truncate">
<TableCell className="font-medium truncate"> {deployment.version?.version}
{deployment.machine?.name} </TableCell>
</TableCell> <TableCell className="font-medium truncate">
<TableCell className="text-right truncate"> {deployment.machine?.name}
{getRelativeTime(deployment.updated_at)} </TableCell>
</TableCell> <TableCell className="text-right truncate">
</TableRow> {getRelativeTime(deployment.updated_at)}
); </TableCell>
</TableRow>
);
} }
export function DeploymentRow({ export function DeploymentRow({
deployment, deployment,
}: { }: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]; deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) { }) {
return ( return (
<> <>
<TableCell className="capitalize truncate"> <TableCell className="capitalize truncate">
{deployment.environment} {deployment.environment}
</TableCell> </TableCell>
<TableCell className="font-medium truncate"> <TableCell className="font-medium truncate">
{deployment.version?.version} {deployment.version?.version}
</TableCell> </TableCell>
<TableCell className="font-medium truncate"> <TableCell className="font-medium truncate">
{deployment.machine?.name} {deployment.machine?.name}
</TableCell> </TableCell>
<TableCell className="text-right truncate"> <TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)} {getRelativeTime(deployment.updated_at)}
</TableCell> </TableCell>
</> </>
); );
} }
+1 -1
View File
@@ -223,7 +223,7 @@ export const columns: ColumnDef<Machine>[] = [
href={machine.endpoint.replace( href={machine.endpoint.replace(
"comfyui-api", "comfyui-api",
"comfyui-app" "comfyui-app"
)} )} rel="noreferrer"
> >
Open ComfyUI Open ComfyUI
</a> </a>
+7 -3
View File
@@ -29,6 +29,7 @@ export function Navbar() {
const { organization } = useOrganization(); const { organization } = useOrganization();
const _isDesktop = useMediaQuery("(min-width: 1024px)"); const _isDesktop = useMediaQuery("(min-width: 1024px)");
const [isDesktop, setIsDesktop] = useState(true); const [isDesktop, setIsDesktop] = useState(true);
const [isSheetOpen, setSheetOpen] = useState(false);
useEffect(() => { useEffect(() => {
setIsDesktop(_isDesktop); setIsDesktop(_isDesktop);
}, [_isDesktop]); }, [_isDesktop]);
@@ -36,7 +37,7 @@ export function Navbar() {
<> <>
<div className="flex flex-row items-center gap-4"> <div className="flex flex-row items-center gap-4">
{!isDesktop && ( {!isDesktop && (
<Sheet> <Sheet open={isSheetOpen} onOpenChange={(open) => setSheetOpen(open)}>
<SheetTrigger asChild> <SheetTrigger asChild>
<button className="flex items-center justify-center w-8 h-8 p-2"> <button className="flex items-center justify-center w-8 h-8 p-2">
<Menu /> <Menu />
@@ -47,7 +48,10 @@ export function Navbar() {
<SheetTitle className="text-start">Comfy Deploy</SheetTitle> <SheetTitle className="text-start">Comfy Deploy</SheetTitle>
</SheetHeader> </SheetHeader>
<div className="grid h-full grid-rows-[1fr_auto]"> <div className="grid h-full grid-rows-[1fr_auto]">
<NavbarMenu className=" h-full" /> <NavbarMenu
className=" h-full"
closeSheet={() => setSheetOpen(false)}
/>
{/* <OrganizationSwitcher {/* <OrganizationSwitcher
appearance={{ appearance={{
elements: { elements: {
@@ -98,7 +102,7 @@ export function Navbar() {
variant="outline" variant="outline"
className="rounded-full aspect-square p-2" className="rounded-full aspect-square p-2"
> >
<a target="_blank" href="https://github.com/BennyKok/comfyui-deploy"> <a target="_blank" href="https://github.com/BennyKok/comfyui-deploy" rel="noreferrer">
<Github /> <Github />
</a> </a>
</Button> </Button>
+15 -6
View File
@@ -9,7 +9,13 @@ import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useMediaQuery } from "usehooks-ts"; import { useMediaQuery } from "usehooks-ts";
export function NavbarMenu({ className }: { className?: string }) { export function NavbarMenu({
className,
closeSheet,
}: {
className?: string;
closeSheet?: () => void;
}) {
const _isDesktop = useMediaQuery("(min-width: 1024px)"); const _isDesktop = useMediaQuery("(min-width: 1024px)");
const [isDesktop, setIsDesktop] = useState(true); const [isDesktop, setIsDesktop] = useState(true);
useEffect(() => { useEffect(() => {
@@ -35,9 +41,9 @@ export function NavbarMenu({ className }: { className?: string }) {
path: "/api-keys", path: "/api-keys",
}, },
{ {
name: "Storage", name: "Examples",
path: "/storage", path: "/examples"
}, }
]; ];
return ( return (
@@ -46,9 +52,9 @@ export function NavbarMenu({ className }: { className?: string }) {
{isDesktop && ( {isDesktop && (
<Tabs <Tabs
defaultValue={pathname} defaultValue={pathname}
className="w-[400px] flex pointer-events-auto" className="w-fit flex pointer-events-auto"
> >
<TabsList className="grid w-full grid-cols-4"> <TabsList className="w-full">
{pages.map((page) => ( {pages.map((page) => (
<TabsTrigger <TabsTrigger
key={page.name} key={page.name}
@@ -72,6 +78,9 @@ export function NavbarMenu({ className }: { className?: string }) {
<Link <Link
key={page.name} key={page.name}
href={page.path} href={page.path}
onClick={() => {
if (!!closeSheet) closeSheet();
}}
className="p-2 hover:bg-gray-100/20 hover:underline" className="p-2 hover:bg-gray-100/20 hover:underline"
> >
{page.name} {page.name}
+16 -2
View File
@@ -5,6 +5,20 @@ export async function OutputRender(props: {
run_id: string; run_id: string;
filename: string; filename: string;
}) { }) {
if (props.filename.endsWith(".mp4") || props.filename.endsWith(".webm")) {
const url = await getFileDownloadUrl(
`outputs/runs/${props.run_id}/${props.filename}`,
);
return (
<video controls autoPlay className="w-[400px]">
<source src={url} type="video/mp4" />
<source src={url} type="video/webm" />
Your browser does not support the video tag.
</video>
);
}
if ( if (
props.filename.endsWith(".png") || props.filename.endsWith(".png") ||
props.filename.endsWith(".gif") || props.filename.endsWith(".gif") ||
@@ -12,13 +26,13 @@ export async function OutputRender(props: {
props.filename.endsWith(".jpeg") props.filename.endsWith(".jpeg")
) { ) {
const url = await getFileDownloadUrl( const url = await getFileDownloadUrl(
`outputs/runs/${props.run_id}/${props.filename}` `outputs/runs/${props.run_id}/${props.filename}`,
); );
return <img className="max-w-[200px]" alt={props.filename} src={url} />; return <img className="max-w-[200px]" alt={props.filename} src={url} />;
} else { } else {
const url = await getFileDownloadUrl( const url = await getFileDownloadUrl(
`outputs/runs/${props.run_id}/${props.filename}` `outputs/runs/${props.run_id}/${props.filename}`,
); );
// console.log(url); // console.log(url);
+60 -49
View File
@@ -1,63 +1,74 @@
import { LiveStatus } from "./LiveStatus";
import { RunInputs } from "@/components/RunInputs"; import { RunInputs } from "@/components/RunInputs";
import { RunOutputs } from "@/components/RunOutputs"; import { RunOutputs } from "@/components/RunOutputs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { TableCell, TableRow } from "@/components/ui/table"; import { TableCell, TableRow } from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { getDuration, getRelativeTime } from "@/lib/getRelativeTime"; import { getDuration, getRelativeTime } from "@/lib/getRelativeTime";
import { type findAllRuns } from "@/server/findAllRuns"; import { type findAllRuns } from "@/server/findAllRuns";
import { Suspense } from "react"; import { Suspense } from "react";
import { LiveStatus } from "./LiveStatus";
export async function RunDisplay({ export async function RunDisplay({
run, run,
}: { }: {
run: Awaited<ReturnType<typeof findAllRuns>>[0]; run: Awaited<ReturnType<typeof findAllRuns>>[0];
}) { }) {
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer"> <DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow> <TableRow>
<TableCell>{run.number}</TableCell> <TableCell>{run.number}</TableCell>
<TableCell className="font-medium truncate"> <TableCell className="font-medium truncate">
{run.machine?.name} {run.machine?.name}
</TableCell> </TableCell>
<TableCell className="truncate"> <TableCell className="truncate">
{getRelativeTime(run.created_at)} {getRelativeTime(run.created_at)}
</TableCell> </TableCell>
<TableCell>{run.version?.version}</TableCell> <TableCell>{run.version?.version}</TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="truncate"> <Badge variant="outline" className="truncate">
{run.origin} {run.origin}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell className="truncate"> <TableCell className="truncate">
{getDuration(run.duration)} <Tooltip>
</TableCell> <TooltipTrigger>{getDuration(run.duration)}</TooltipTrigger>
<LiveStatus run={run} /> <TooltipContent>
</TableRow> <div>Cold start: {getDuration(run.cold_start_duration)}</div>
</DialogTrigger> <div>Run duration: {getDuration(run.run_duration)}</div>
<DialogContent className="max-w-3xl"> </TooltipContent>
<DialogHeader> </Tooltip>
<DialogTitle>Run outputs</DialogTitle> </TableCell>
<DialogDescription> <LiveStatus run={run} />
You can view your run&apos;s outputs here </TableRow>
</DialogDescription> </DialogTrigger>
</DialogHeader> <DialogContent className="max-w-3xl">
<div className="max-h-96 overflow-y-scroll"> <DialogHeader>
<RunInputs run={run} /> <DialogTitle>Run outputs</DialogTitle>
<Suspense> <DialogDescription>
<RunOutputs run_id={run.id} /> You can view your run&apos;s outputs here
</Suspense> </DialogDescription>
</div> </DialogHeader>
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */} <div className="max-h-96 overflow-y-scroll">
</DialogContent> <RunInputs run={run} />
</Dialog> <Suspense>
); <RunOutputs run_id={run.id} />
</Suspense>
</div>
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
</DialogContent>
</Dialog>
);
} }
+40 -40
View File
@@ -1,10 +1,3 @@
import {
findAllDeployments,
findAllRunsWithCounts,
} from "../server/findAllRuns";
import { DeploymentDisplay } from "./DeploymentDisplay";
import { PaginationControl } from "./PaginationControl";
import { RunDisplay } from "./RunDisplay";
import { import {
Table, Table,
TableBody, TableBody,
@@ -15,6 +8,13 @@ import {
} from "@/components/ui/table"; } from "@/components/ui/table";
import { parseAsInteger } from "next-usequerystate"; import { parseAsInteger } from "next-usequerystate";
import { headers } from "next/headers"; import { headers } from "next/headers";
import {
findAllDeployments,
findAllRunsWithCounts,
} from "../server/findAllRuns";
import { DeploymentDisplay } from "./DeploymentDisplay";
import { PaginationControl } from "./PaginationControl";
import { RunDisplay } from "./RunDisplay";
const itemPerPage = 6; const itemPerPage = 6;
const pageParser = parseAsInteger.withDefault(1); const pageParser = parseAsInteger.withDefault(1);
@@ -33,40 +33,40 @@ export async function RunsTable(props: {
offset: (page - 1) * itemPerPage, offset: (page - 1) * itemPerPage,
}); });
return ( return (
<div> <div>
<div className="overflow-auto h-fit w-full"> <div className="overflow-auto h-fit w-full">
<Table className=""> <Table className="">
{allRuns.length == 0 && ( {allRuns.length === 0 && (
<TableCaption>A list of your recent runs.</TableCaption> <TableCaption>A list of your recent runs.</TableCaption>
)} )}
<TableHeader className="bg-background top-0 sticky"> <TableHeader className="bg-background top-0 sticky">
<TableRow> <TableRow>
<TableHead className="truncate">Number</TableHead> <TableHead className="truncate">Number</TableHead>
<TableHead className="truncate">Machine</TableHead> <TableHead className="truncate">Machine</TableHead>
<TableHead className="truncate">Time</TableHead> <TableHead className="truncate">Time</TableHead>
<TableHead className="truncate">Version</TableHead> <TableHead className="truncate">Version</TableHead>
<TableHead className="truncate">Origin</TableHead> <TableHead className="truncate">Origin</TableHead>
<TableHead className="truncate">Duration</TableHead> <TableHead className="truncate">Duration</TableHead>
<TableHead className="truncate">Live Status</TableHead> <TableHead className="truncate">Live Status</TableHead>
<TableHead className="text-right">Status</TableHead> <TableHead className="text-right">Status</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{allRuns.map((run) => ( {allRuns.map((run) => (
<RunDisplay run={run} key={run.id} /> <RunDisplay run={run} key={run.id} />
))} ))}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
{Math.ceil(total / itemPerPage) > 0 && ( {Math.ceil(total / itemPerPage) > 0 && (
<PaginationControl <PaginationControl
totalPage={Math.ceil(total / itemPerPage)} totalPage={Math.ceil(total / itemPerPage)}
currentPage={page} currentPage={page}
/> />
)} )}
</div> </div>
); );
} }
export async function DeploymentsTable(props: { workflow_id: string }) { export async function DeploymentsTable(props: { workflow_id: string }) {
+3 -2
View File
@@ -1,6 +1,5 @@
"use client"; "use client";
import { useServerActionData } from "./useServerActionData";
import { ButtonAction } from "@/components/ButtonActionLoader"; import { ButtonAction } from "@/components/ButtonActionLoader";
import { UpdateModal } from "@/components/InsertModal"; import { UpdateModal } from "@/components/InsertModal";
import { LoadingPageWrapper } from "@/components/LoadingWrapper"; import { LoadingPageWrapper } from "@/components/LoadingWrapper";
@@ -15,6 +14,7 @@ import { ExternalLink } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
import { useServerActionData } from "./useServerActionData";
export function SharePageSettings({ export function SharePageSettings({
deployment_id, deployment_id,
@@ -58,13 +58,14 @@ export function SharePageSettings({
type="button" type="button"
> >
<ButtonAction <ButtonAction
routerAction="back"
action={removePublicShareDeployment.bind(null, deployment.id)} action={removePublicShareDeployment.bind(null, deployment.id)}
> >
Remove Remove
</ButtonAction> </ButtonAction>
</Button> </Button>
<Button asChild className="gap-2 truncate" type="button"> <Button asChild className="gap-2 truncate" type="button">
<Link href={`/share/${deployment.id}`} target="_blank"> <Link href={`/share/${deployment.share_slug ?? deployment.id}`} target="_blank">
View Share Page <ExternalLink size={14} /> View Share Page <ExternalLink size={14} />
</Link> </Link>
</Button> </Button>
+115 -81
View File
@@ -1,8 +1,5 @@
"use client"; "use client";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { LoadingIcon } from "@/components/LoadingIcon"; import { LoadingIcon } from "@/components/LoadingIcon";
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form"; import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@@ -46,11 +43,11 @@ import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion"; import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { import {
Copy, Copy,
Edit,
ExternalLink, ExternalLink,
Info, Info,
MoreVertical, MoreVertical,
Play, Play,
Share,
} from "lucide-react"; } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate"; import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
@@ -58,6 +55,12 @@ import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
import type { z } from "zod"; import type { z } from "zod";
import { create } from "zustand"; import { create } from "zustand";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { ButtonAction } from "@/components/ButtonActionLoader";
import { editWorkflowOnMachine } from "@/server/editWorkflowOnMachine";
import { VisualizeImagesGrid } from "@/components/VisualizeImagesGrid";
export function VersionSelect({ export function VersionSelect({
workflow, workflow,
@@ -122,28 +125,66 @@ export function MachineSelect({
); );
} }
function useSelectedMachine(machines: Awaited<ReturnType<typeof getMachines>>) { type SelectedMachineStore = {
const a = useQueryState("machine", { selectedMachine: string | undefined;
defaultValue: machines?.[0]?.id ?? "", setSelectedMachine: (machine: string) => void;
}); };
return a; export const selectedMachineStore = create<SelectedMachineStore>((set) => ({
selectedMachine: undefined,
setSelectedMachine: (machine) => set(() => ({ selectedMachine: machine })),
}));
export function useSelectedMachine(
machines: Awaited<ReturnType<typeof getMachines>>,
): [string, (v: string) => void] {
const { selectedMachine, setSelectedMachine } = selectedMachineStore();
return [selectedMachine ?? machines?.[0]?.id ?? "", setSelectedMachine];
// const searchParams = useSearchParams();
// const pathname = usePathname();
// const router = useRouter();
// const createQueryString = useCallback(
// (name: string, value: string) => {
// const params = new URLSearchParams(searchParams.toString());
// params.set(name, value);
// return params.toString();
// },
// [searchParams],
// );
// return [
// searchParams.get("machine") ?? machines?.[0]?.id ?? "",
// (v: string) => {
// // window.history.pushState(
// // "new url",
// // "",
// // pathname + "?" + createQueryString("machine", v),
// // );
// // router.push(pathname + "?" + createQueryString("machine", v));
// router.replace(pathname + "?" + createQueryString("machine", v));
// },
// ];
} }
type PublicRunStore = { type PublicRunStore = {
image: string; image: {
url: string;
}[] | null;
loading: boolean; loading: boolean;
runId: string; runId: string;
status: string; status: string;
setImage: (image: string) => void; setImage: (image: { url: string; }[]) => void;
setLoading: (loading: boolean) => void; setLoading: (loading: boolean) => void;
setRunId: (runId: string) => void; setRunId: (runId: string) => void;
setStatus: (status: string) => void; setStatus: (status: string) => void;
}; };
export const publicRunStore = create<PublicRunStore>((set) => ({ export const publicRunStore = create<PublicRunStore>((set) => ({
image: "", image: null,
loading: false, loading: false,
runId: "", runId: "",
status: "", status: "",
@@ -167,7 +208,10 @@ export function PublicRunOutputs(props: {
console.log(res?.status); console.log(res?.status);
if (res) setStatus(res.status); if (res) setStatus(res.status);
if (res && res.status === "success") { if (res && res.status === "success") {
setImage(res.outputs[0]?.data.images[0].url); const imageURLs = res.outputs[0]?.data.images.map((item: { url: string; }) => {
return { url: item.url };
});
setImage(imageURLs);
setLoading(false); setLoading(false);
clearInterval(interval); clearInterval(interval);
} }
@@ -176,30 +220,25 @@ export function PublicRunOutputs(props: {
return () => clearInterval(interval); return () => clearInterval(interval);
}, [runId]); }, [runId]);
return ( if (loading) {
<div className="border border-gray-200 w-full square h-[400px] rounded-lg relative"> return (
{!loading && !image && props.preview && props.preview.length > 0 && ( <div className="border border-gray-200 w-full h-[400px] square rounded-lg relative p-4 ">
<>
<img
className="w-full h-full object-contain"
src={props.preview[0]?.url}
alt="Generated image"
/>
</>
)}
{!loading && image && (
<img
className="w-full h-full object-contain"
src={image}
alt="Generated image"
/>
)}
{loading && (
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2"> <div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
{status} <LoadingIcon /> {status} <LoadingIcon />
</div> </div>
<Skeleton className="w-full h-full" />
</div>
);
}
return (
<div className="border border-gray-200 w-full min-h-[400px] square rounded-lg relative p-4 ">
{!image && props.preview && props.preview.length > 0 &&
<VisualizeImagesGrid images={props.preview} />
}
{image && (
<VisualizeImagesGrid images={image} />
)} )}
{loading && <Skeleton className="w-full h-full" />}
</div> </div>
); );
} }
@@ -224,7 +263,7 @@ export function RunWorkflowButton({
const schema = useMemo(() => { const schema = useMemo(() => {
const workflow_version = getWorkflowVersionFromVersionIndex( const workflow_version = getWorkflowVersionFromVersionIndex(
workflow, workflow,
version version,
); );
if (!workflow_version) return null; if (!workflow_version) return null;
@@ -238,7 +277,7 @@ export function RunWorkflowButton({
const val = Object.keys(values).length > 0 ? values : undefined; const val = Object.keys(values).length > 0 ? values : undefined;
const workflow_version_id = workflow?.versions.find( const workflow_version_id = workflow?.versions.find(
(x) => x.version === version (x) => x.version === version,
)?.id; )?.id;
console.log(workflow_version_id); console.log(workflow_version_id);
if (!workflow_version_id) return; if (!workflow_version_id) return;
@@ -253,7 +292,7 @@ export function RunWorkflowButton({
machine_id: machine, machine_id: machine,
inputs: val, inputs: val,
runOrigin: "manual", runOrigin: "manual",
}) }),
); );
// console.log(res.json()); // console.log(res.json());
setIsLoading(false); setIsLoading(false);
@@ -290,7 +329,7 @@ export function RunWorkflowButton({
className="px-1" className="px-1"
> >
<div className="flex justify-end"> <div className="flex justify-end">
<AutoFormSubmit> <AutoFormSubmit disabled={isLoading}>
Run Run
{isLoading ? <LoadingIcon /> : <Play size={14} />} {isLoading ? <LoadingIcon /> : <Play size={14} />}
</AutoFormSubmit> </AutoFormSubmit>
@@ -322,7 +361,7 @@ export function CreateDeploymentButton({
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find( const workflow_version_id = workflow?.versions.find(
(x) => x.version === version (x) => x.version === version,
)?.id; )?.id;
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -342,8 +381,8 @@ export function CreateDeploymentButton({
workflow.id, workflow.id,
workflow_version_id, workflow_version_id,
machine, machine,
"production" "production",
) ),
); );
setIsLoading(false); setIsLoading(false);
}} }}
@@ -360,8 +399,8 @@ export function CreateDeploymentButton({
workflow.id, workflow.id,
workflow_version_id, workflow_version_id,
machine, machine,
"staging" "staging",
) ),
); );
setIsLoading(false); setIsLoading(false);
}} }}
@@ -373,7 +412,7 @@ export function CreateDeploymentButton({
); );
} }
export function CreateShareButton({ export function OpenEditButton({
workflow, workflow,
machines, machines,
}: { }: {
@@ -385,40 +424,34 @@ export function CreateShareButton({
...parseAsInteger, ...parseAsInteger,
}); });
const [machine] = useSelectedMachine(machines); const [machine] = useSelectedMachine(machines);
const [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find( const workflow_version_id = workflow?.versions.find(
(x) => x.version === version (x) => x.version == version,
)?.id; )?.id;
const [isLoading, setIsLoading] = useState(false);
return ( return (
<DropdownMenu> workflow_version_id &&
<DropdownMenuTrigger asChild> machine && (
<Button className="gap-2" disabled={isLoading} variant="outline"> <Button
Share {isLoading ? <LoadingIcon /> : <Share size={14} />} className="gap-2"
</Button> onClick={async () => {
</DropdownMenuTrigger> setIsLoading(true);
<DropdownMenuContent className="w-56"> const url = await callServerPromise(
<DropdownMenuItem editWorkflowOnMachine(workflow_version_id, machine),
onClick={async () => { );
if (!workflow_version_id) return; if (url && typeof url !== "object") {
window.open(url, "_blank");
setIsLoading(true); } else if (url && typeof url === "object" && url.error) {
await callServerPromise( console.error(url.error);
createDeployments( }
workflow.id, setIsLoading(false);
workflow_version_id, }}
machine, // asChild
"public-share" variant="outline"
) >
); Edit {isLoading ? <LoadingIcon /> : <Edit size={14} />}
setIsLoading(false); </Button>
}} )
>
Public
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
); );
} }
@@ -432,7 +465,7 @@ export function CopyWorkflowVersion({
...parseAsInteger, ...parseAsInteger,
}); });
const workflow_version = workflow?.versions.find( const workflow_version = workflow?.versions.find(
(x) => x.version === version (x) => x.version === version,
); );
return ( return (
<DropdownMenu> <DropdownMenu>
@@ -456,7 +489,7 @@ export function CopyWorkflowVersion({
}); });
navigator.clipboard.writeText( navigator.clipboard.writeText(
JSON.stringify(workflow_version?.workflow) JSON.stringify(workflow_version?.workflow),
); );
toast("Copied to clipboard"); toast("Copied to clipboard");
}} }}
@@ -466,7 +499,7 @@ export function CopyWorkflowVersion({
<DropdownMenuItem <DropdownMenuItem
onClick={async () => { onClick={async () => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
JSON.stringify(workflow_version?.workflow_api) JSON.stringify(workflow_version?.workflow_api),
); );
toast("Copied to clipboard"); toast("Copied to clipboard");
}} }}
@@ -480,7 +513,7 @@ export function CopyWorkflowVersion({
export function getWorkflowVersionFromVersionIndex( export function getWorkflowVersionFromVersionIndex(
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>, workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>,
version: number version: number,
) { ) {
const workflow_version = workflow?.versions.find((x) => x.version == version); const workflow_version = workflow?.versions.find((x) => x.version == version);
@@ -506,7 +539,7 @@ export function ViewWorkflowDetailsButton({
isLoading: isNodesIndexLoading, isLoading: isNodesIndexLoading,
} = useSWR( } = useSWR(
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json", "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json",
fetcher fetcher,
); );
const groupedByAuxName = useMemo(() => { const groupedByAuxName = useMemo(() => {
@@ -516,7 +549,7 @@ export function ViewWorkflowDetailsButton({
const workflow_version = getWorkflowVersionFromVersionIndex( const workflow_version = getWorkflowVersionFromVersionIndex(
workflow, workflow,
version version,
); );
const api = workflow_version?.workflow_api; const api = workflow_version?.workflow_api;
@@ -527,7 +560,7 @@ export function ViewWorkflowDetailsButton({
.map(([_, value]) => { .map(([_, value]) => {
const classType = value.class_type; const classType = value.class_type;
const classTypeData = Object.entries(data).find(([_, nodeArray]) => const classTypeData = Object.entries(data).find(([_, nodeArray]) =>
nodeArray[0].includes(classType) nodeArray[0].includes(classType),
); );
return classTypeData ? { node: value, classTypeData } : null; return classTypeData ? { node: value, classTypeData } : null;
}) })
@@ -557,7 +590,7 @@ export function ViewWorkflowDetailsButton({
node: z.infer<typeof workflowAPINodeType>[]; node: z.infer<typeof workflowAPINodeType>[];
url: string; url: string;
} }
> >,
); );
// console.log(groupedByAuxName); // console.log(groupedByAuxName);
@@ -599,6 +632,7 @@ export function ViewWorkflowDetailsButton({
href={group.url} href={group.url}
target="_blank" target="_blank"
className="hover:underline" className="hover:underline"
rel="noreferrer"
> >
{key} {key}
<ExternalLink <ExternalLink
@@ -0,0 +1,42 @@
type imagesType = {
url: string;
width?: number;
height?: number;
};
type VisualizeImagesGridProps = {
images: imagesType[];
layout?: 'justify-between' | 'justify-center' | 'justify-start' | 'justify-end';
};
export function VisualizeImagesGrid({ images, layout }: VisualizeImagesGridProps) {
return (
<div className={`flex gap-4 flex-wrap ${layout || 'justify-center'}`}>
<>
{images && images.length > 0 &&
images.map(item => {
if (!item) {
return;
}
if (item?.url.endsWith(".mp4") || item?.url.endsWith(".webm")) {
return (
<video key={item?.url} controls autoPlay className="rounded-xl" style={{ maxHeight: item.height || 370, maxWidth: item.width || "auto" }}>
<source src={item?.url} type="video/mp4" />
<source src={item?.url} type="video/webm" />
Your browser does not support the video tag.
</video>
);
}
return <img
key={item?.url}
className="object-contain overflow-hidden rounded-xl"
src={item?.url}
alt="Generated image"
style={{ maxHeight: item.height || 370, maxWidth: item.width || "auto" }}
/>;
})
}
</>
</div>
);
}
+4 -4
View File
@@ -21,8 +21,8 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { getAllUserWorkflow } from "@/server/crudWorkflow";
import { deleteWorkflow } from "@/server/deleteWorkflow"; import { deleteWorkflow } from "@/server/deleteWorkflow";
import type { getAllUserWorkflow } from "@/server/getAllUserWorkflow";
import type { import type {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
@@ -169,7 +169,7 @@ export const columns: ColumnDef<WorkflowItemList>[] = [
export function WorkflowList({ data }: { data: WorkflowItemList[] }) { export function WorkflowList({ data }: { data: WorkflowItemList[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]); const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>( const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[] [],
); );
const [columnVisibility, setColumnVisibility] = const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({}); React.useState<VisibilityState>({});
@@ -218,7 +218,7 @@ export function WorkflowList({ data }: { data: WorkflowItemList[] }) {
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext(),
)} )}
</TableHead> </TableHead>
); );
@@ -237,7 +237,7 @@ export function WorkflowList({ data }: { data: WorkflowItemList[] }) {
<TableCell key={cell.id}> <TableCell key={cell.id}>
{flexRender( {flexRender(
cell.column.columnDef.cell, cell.column.columnDef.cell,
cell.getContext() cell.getContext(),
)} )}
</TableCell> </TableCell>
))} ))}
+102 -104
View File
@@ -42,105 +42,105 @@ const Model = z.object({
url: z.string(), url: z.string(),
}); });
export const CivitaiModel = z.object({ export const CivitalModelSchema = z.object({
id: z.number(), items: z.array(
name: z.string(),
description: z.string(),
type: z.string(),
// poi: z.boolean(),
// nsfw: z.boolean(),
// allowNoCredit: z.boolean(),
// allowCommercialUse: z.string(),
// allowDerivatives: z.boolean(),
// allowDifferentLicense: z.boolean(),
// stats: z.object({
// downloadCount: z.number(),
// favoriteCount: z.number(),
// commentCount: z.number(),
// ratingCount: z.number(),
// rating: z.number(),
// tippedAmountCount: z.number(),
// }),
creator: z
.object({
username: z.string().nullable(),
image: z.string().nullable().default(null),
})
.nullable(),
tags: z.array(z.string()),
modelVersions: z.array(
z.object({ z.object({
id: z.number(), id: z.number(),
modelId: z.number(),
name: z.string(), name: z.string(),
createdAt: z.string(), description: z.string(),
updatedAt: z.string(), type: z.string(),
status: z.string(), // poi: z.boolean(),
publishedAt: z.string(), // nsfw: z.boolean(),
trainedWords: z.array(z.unknown()), // allowNoCredit: z.boolean(),
trainingStatus: z.string().nullable(), // allowCommercialUse: z.string(),
trainingDetails: z.string().nullable(), // allowDerivatives: z.boolean(),
baseModel: z.string(), // allowDifferentLicense: z.boolean(),
baseModelType: z.string().nullable(), // stats: z.object({
earlyAccessTimeFrame: z.number(), // downloadCount: z.number(),
description: z.string().nullable(), // favoriteCount: z.number(),
vaeId: z.number().nullable(), // commentCount: z.number(),
stats: z.object({ // ratingCount: z.number(),
downloadCount: z.number(), // rating: z.number(),
ratingCount: z.number(), // tippedAmountCount: z.number(),
rating: z.number(), // }),
}), creator: z
files: z.array( .object({
username: z.string().nullable(),
image: z.string().nullable().default(null),
})
.nullable(),
tags: z.array(z.string()),
modelVersions: z.array(
z.object({ z.object({
id: z.number(), id: z.number(),
sizeKB: z.number(), modelId: z.number(),
name: z.string(), name: z.string(),
type: z.string(), createdAt: z.string(),
// metadata: z.object({ updatedAt: z.string(),
// fp: z.string().nullable().optional(), status: z.string(),
// size: z.string().nullable().optional(), publishedAt: z.string(),
// format: z.string().nullable().optional(), trainedWords: z.array(z.unknown()),
// }), trainingStatus: z.string().nullable(),
// pickleScanResult: z.string(), trainingDetails: z.string().nullable(),
// pickleScanMessage: z.string(), baseModel: z.string(),
// virusScanResult: z.string(), baseModelType: z.string().nullable(),
// virusScanMessage: z.string().nullable(), earlyAccessTimeFrame: z.number(),
// scannedAt: z.string(), description: z.string().nullable(),
// hashes: z.object({ vaeId: z.number().nullable(),
// AutoV1: z.string().nullable().optional(), stats: z.object({
// AutoV2: z.string().nullable().optional(), downloadCount: z.number(),
// SHA256: z.string().nullable().optional(), ratingCount: z.number(),
// CRC32: z.string().nullable().optional(), rating: z.number(),
// BLAKE3: z.string().nullable().optional(),
// }),
downloadUrl: z.string(),
// primary: z.boolean().default(false),
}),
),
images: z.array(
z.object({
id: z.number(),
url: z.string(),
nsfw: z.string(),
width: z.number(),
height: z.number(),
hash: z.string(),
type: z.string(),
metadata: z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
}), }),
meta: z.any(), files: z.array(
}), z.object({
id: z.number(),
sizeKB: z.number(),
name: z.string(),
type: z.string(),
// metadata: z.object({
// fp: z.string().nullable().optional(),
// size: z.string().nullable().optional(),
// format: z.string().nullable().optional(),
// }),
// pickleScanResult: z.string(),
// pickleScanMessage: z.string(),
// virusScanResult: z.string(),
// virusScanMessage: z.string().nullable(),
// scannedAt: z.string(),
// hashes: z.object({
// AutoV1: z.string().nullable().optional(),
// AutoV2: z.string().nullable().optional(),
// SHA256: z.string().nullable().optional(),
// CRC32: z.string().nullable().optional(),
// BLAKE3: z.string().nullable().optional(),
// }),
downloadUrl: z.string(),
// primary: z.boolean().default(false),
})
),
images: z.array(
z.object({
id: z.number(),
url: z.string(),
nsfw: z.string(),
width: z.number(),
height: z.number(),
hash: z.string(),
type: z.string(),
metadata: z.object({
hash: z.string(),
width: z.number(),
height: z.number(),
}),
meta: z.any(),
})
),
downloadUrl: z.string(),
})
), ),
downloadUrl: z.string(), })
}),
), ),
});
export const CivitalModelSchema = z.object({
items: z.array(CivitaiModel),
metadata: z.object({ metadata: z.object({
totalItems: z.number(), totalItems: z.number(),
currentPage: z.number(), currentPage: z.number(),
@@ -197,7 +197,7 @@ function mapType(type: string) {
} }
function mapModelsList( function mapModelsList(
models: z.infer<typeof CivitalModelSchema>, models: z.infer<typeof CivitalModelSchema>
): z.infer<typeof ModelListWrapper> { ): z.infer<typeof ModelListWrapper> {
return { return {
models: models.items.flatMap((item) => { models: models.items.flatMap((item) => {
@@ -241,9 +241,8 @@ function getUrl(search?: string) {
export function CivitaiModelRegistry({ export function CivitaiModelRegistry({
field, field,
}: Pick<AutoFormInputComponentProps, "field">) { }: Pick<AutoFormInputComponentProps, "field">) {
const [modelList, setModelList] = React.useState< const [modelList, setModelList] =
z.infer<typeof ModelListWrapper> React.useState<z.infer<typeof ModelListWrapper>>();
>();
const [loading, setLoading] = React.useState(false); const [loading, setLoading] = React.useState(false);
@@ -302,9 +301,8 @@ export function CivitaiModelRegistry({
export function ComfyUIManagerModelRegistry({ export function ComfyUIManagerModelRegistry({
field, field,
}: Pick<AutoFormInputComponentProps, "field">) { }: Pick<AutoFormInputComponentProps, "field">) {
const [modelList, setModelList] = React.useState< const [modelList, setModelList] =
z.infer<typeof ModelListWrapper> React.useState<z.infer<typeof ModelListWrapper>>();
>();
React.useEffect(() => { React.useEffect(() => {
const controller = new AbortController(); const controller = new AbortController();
@@ -312,7 +310,7 @@ export function ComfyUIManagerModelRegistry({
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json", "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
{ {
signal: controller.signal, signal: controller.signal,
}, }
) )
.then((x) => x.json()) .then((x) => x.json())
.then((a) => { .then((a) => {
@@ -355,14 +353,14 @@ export function ModelSelector({
if ( if (
prevSelectedModels.some( prevSelectedModels.some(
(selectedModel) => (selectedModel) =>
selectedModel.url + selectedModel.name === model.url + model.name, selectedModel.url + selectedModel.name === model.url + model.name
) )
) { ) {
field.onChange( field.onChange(
prevSelectedModels.filter( prevSelectedModels.filter(
(selectedModel) => (selectedModel) =>
selectedModel.url + selectedModel.name !== model.url + model.name, selectedModel.url + selectedModel.name !== model.url + model.name
), )
); );
} else { } else {
field.onChange([...prevSelectedModels, model]); field.onChange([...prevSelectedModels, model]);
@@ -410,10 +408,10 @@ export function ModelSelector({
className={cn( className={cn(
"ml-auto h-4 w-4", "ml-auto h-4 w-4",
value.some( value.some(
(selectedModel) => selectedModel.url === model.url, (selectedModel) => selectedModel.url === model.url
) )
? "opacity-100" ? "opacity-100"
: "opacity-0", : "opacity-0"
)} )}
/> />
</CommandItem> </CommandItem>
@@ -1,89 +0,0 @@
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { FormControl, FormItem, FormLabel } from "../ui/form";
import { LoadingIcon } from "@/components/LoadingIcon";
import * as React from "react";
import AutoFormInput from "../ui/auto-form/fields/input";
import { useDebouncedCallback } from "use-debounce";
import { CivitaiModel } from "./ModelPickerView";
import { z } from "zod";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export default function AutoFormCheckpointInput(
props: AutoFormInputComponentProps,
) {
const [loading, setLoading] = React.useState(false);
const [modelRes, setModelRes] = React.useState<
z.infer<typeof CivitaiModel>
>();
const [modelVersionid, setModelVersionId] = React.useState<string | null>();
const { label, isRequired, fieldProps, zodItem, fieldConfigItem } = props;
const handleSearch = useDebouncedCallback((search) => {
const validationResult = insertCivitaiCheckpointSchema.shape.civitai_url
.safeParse(search);
if (!validationResult.success) {
console.error(validationResult.error);
// Optionally set an error state here
return;
}
setLoading(true);
const controller = new AbortController();
const { url, modelVersionId: versionId } = getUrl(search);
setModelVersionId(versionId);
fetch(url, {
signal: controller.signal,
})
.then((x) => x.json())
.then((a) => {
const res = CivitaiModel.parse(a);
console.log(a);
console.log(res);
setModelRes(res);
setLoading(false);
});
return () => {
controller.abort();
setLoading(false);
};
}, 300);
const modifiedField = {
...fieldProps,
// onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
// handleSearch(event.target.value);
// },
};
return (
<FormItem>
{fieldConfigItem.inputProps?.showLabel && (
<FormLabel>
{label}
{isRequired && <span className="text-destructive">*</span>}
</FormLabel>
)}
<FormControl>
<AutoFormInput
{...props}
fieldProps={modifiedField}
/>
</FormControl>
</FormItem>
);
}
+1
View File
@@ -5,4 +5,5 @@ export const customInputNodes: Record<string, string> = {
ComfyUIDeployExternalNumber: "float", ComfyUIDeployExternalNumber: "float",
ComfyUIDeployExternalNumberInt: "integer", ComfyUIDeployExternalNumberInt: "integer",
ComfyUIDeployExternalLora: "string - (public lora download url)", ComfyUIDeployExternalLora: "string - (public lora download url)",
ComfyUIDeployExternalCheckpoint: "string - (public checkpoints download url)",
}; };
+1 -1
View File
@@ -69,7 +69,7 @@ const FeedbackThanks = forwardRef<React.ElementRef<'div'>>(
) )
export function Feedback() { export function Feedback() {
let [submitted, setSubmitted] = useState(false) const [submitted, setSubmitted] = useState(false)
function onSubmit(event: React.FormEvent<HTMLFormElement>) { function onSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault() event.preventDefault()
+1 -1
View File
@@ -8,7 +8,7 @@ export function Prose<T extends React.ElementType = 'div'>({
as?: T as?: T
className?: string className?: string
}) { }) {
let Component = as ?? 'div' const Component = as ?? 'div'
return ( return (
<Component <Component
+3 -3
View File
@@ -22,9 +22,9 @@ function MoonIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
} }
export function ThemeToggle() { export function ThemeToggle() {
let { resolvedTheme, setTheme } = useTheme() const { resolvedTheme, setTheme } = useTheme()
let otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark' const otherTheme = resolvedTheme === 'dark' ? 'light' : 'dark'
let [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
useEffect(() => { useEffect(() => {
setMounted(true) setMounted(true)
@@ -8,7 +8,6 @@ import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea"; import AutoFormTextarea from "./fields/textarea";
import AutoFormModelsPicker from "@/components/custom-form/model-picker"; import AutoFormModelsPicker from "@/components/custom-form/model-picker";
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker"; import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
import AutoFormCheckpointInput from "@/components/custom-form/checkpoint-input";
export const INPUT_COMPONENTS = { export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox, checkbox: AutoFormCheckbox,
@@ -23,7 +22,6 @@ export const INPUT_COMPONENTS = {
// Customs // Customs
snapshot: AutoFormSnapshotPicker, snapshot: AutoFormSnapshotPicker,
models: AutoFormModelsPicker, models: AutoFormModelsPicker,
checkpoints: AutoFormCheckpointInput,
}; };
/** /**
+12 -4
View File
@@ -1,11 +1,19 @@
import * as schema from "./schema"; import { Pool, neonConfig } from "@neondatabase/serverless";
import { neonConfig, Pool } from "@neondatabase/serverless";
import { drizzle as neonDrizzle } from "drizzle-orm/neon-serverless"; import { drizzle as neonDrizzle } from "drizzle-orm/neon-serverless";
import * as schema from "./schema";
const isDevContainer = process.env.REMOTE_CONTAINERS !== undefined;
// if we're running locally // if we're running locally
if (process.env.VERCEL_ENV !== "production") { if (process.env.VERCEL_ENV !== "production") {
// Set the WebSocket proxy to work with the local instance // Set the WebSocket proxy to work with the local instance
neonConfig.wsProxy = (host) => `${host}:5481/v1`; if (isDevContainer) {
// Running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => "host.docker.internal:5481/v1";
} else {
// Not running inside a VS Code devcontainer
neonConfig.wsProxy = (host) => `${host}:5481/v1`;
}
// Disable all authentication and encryption // Disable all authentication and encryption
neonConfig.useSecureWebSocket = false; neonConfig.useSecureWebSocket = false;
neonConfig.pipelineTLS = false; neonConfig.pipelineTLS = false;
@@ -18,5 +26,5 @@ export const db = neonDrizzle(
}), }),
{ {
schema, schema,
} },
); );
+15 -102
View File
@@ -1,4 +1,3 @@
import { CivitaiModelResponse } from "@/types/civitai";
import { type InferSelectModel, relations } from "drizzle-orm"; import { type InferSelectModel, relations } from "drizzle-orm";
import { import {
boolean, boolean,
@@ -10,7 +9,7 @@ import {
timestamp, timestamp,
uuid, uuid,
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod"; import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod"; import { z } from "zod";
export const dbSchema = pgSchema("comfyui_deploy"); export const dbSchema = pgSchema("comfyui_deploy");
@@ -36,6 +35,8 @@ export const workflowTable = dbSchema.table("workflows", {
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const workflowSchema = createSelectSchema(workflowTable);
export const workflowRelations = relations(workflowTable, ({ many, one }) => ({ export const workflowRelations = relations(workflowTable, ({ many, one }) => ({
user: one(usersTable, { user: one(usersTable, {
fields: [workflowTable.user_id], fields: [workflowTable.user_id],
@@ -80,6 +81,7 @@ export const workflowVersionTable = dbSchema.table("workflow_versions", {
created_at: timestamp("created_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const workflowVersionSchema = createSelectSchema(workflowVersionTable);
export const workflowVersionRelations = relations( export const workflowVersionRelations = relations(
workflowVersionTable, workflowVersionTable,
@@ -139,9 +141,8 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
onDelete: "set null", onDelete: "set null",
}, },
), ),
workflow_inputs: jsonb("workflow_inputs").$type< workflow_inputs:
Record<string, string | number> jsonb("workflow_inputs").$type<Record<string, string | number>>(),
>(),
workflow_id: uuid("workflow_id") workflow_id: uuid("workflow_id")
.notNull() .notNull()
.references(() => workflowTable.id, { .references(() => workflowTable.id, {
@@ -155,6 +156,7 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
status: workflowRunStatus("status").notNull().default("not-started"), status: workflowRunStatus("status").notNull().default("not-started"),
ended_at: timestamp("ended_at"), ended_at: timestamp("ended_at"),
created_at: timestamp("created_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
started_at: timestamp("started_at"),
}); });
export const workflowRunRelations = relations( export const workflowRunRelations = relations(
@@ -276,10 +278,10 @@ export const deploymentsTable = dbSchema.table("deployments", {
machine_id: uuid("machine_id") machine_id: uuid("machine_id")
.notNull() .notNull()
.references(() => machinesTable.id), .references(() => machinesTable.id),
share_slug: text("share_slug").unique(),
description: text("description"), description: text("description"),
showcase_media: jsonb("showcase_media").$type< showcase_media:
z.infer<typeof showcaseMedia> jsonb("showcase_media").$type<z.infer<typeof showcaseMedia>>(),
>(),
environment: deploymentEnvironment("environment").notNull(), environment: deploymentEnvironment("environment").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
@@ -332,107 +334,18 @@ export const apiKeyTable = dbSchema.table("api_keys", {
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const resourceUpload = pgEnum("resource_upload", [ export const authRequestsTable = dbSchema.table("auth_requests", {
"started", request_id: text("request_id").primaryKey().notNull(),
"error", user_id: text("user_id"),
"succeded",
]);
export const modelUploadType = pgEnum("model_upload_type", [
"civitai",
"huggingface",
"other",
]);
export const checkpointTable = dbSchema.table("checkpoints", {
id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id")
.references(() => usersTable.id, {}), // perhaps a "special" user_id for global checkpoints
org_id: text("org_id"), org_id: text("org_id"),
description: text("description"), api_hash: text("api_hash"),
checkpoint_volume_id: uuid("checkpoint_volume_id")
.notNull()
.references(() => workflowRunsTable.id, {
onDelete: "cascade",
}).notNull(),
model_name: text("model_name"),
civitai_id: text("civitai_id"),
civitai_version_id: text("civitai_version_id"),
civitai_url: text("civitai_url"),
civitai_download_url: text("civitai_download_url"),
civitai_model_response: jsonb("civitai_model_response").$type<
z.infer<typeof CivitaiModelResponse>
>(),
hf_url: text("hf_url"),
s3_url: text("s3_url"),
user_url: text("client_url"),
is_public: boolean("is_public").notNull().default(false),
status: resourceUpload("status").notNull().default("started"),
upload_machine_id: text("upload_machine_id"),
upload_type: modelUploadType("upload_type").notNull(),
build_log: text("build_log"),
created_at: timestamp("created_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
expired_date: timestamp("expired_date"),
updated_at: timestamp("updated_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const insertCivitaiCheckpointSchema = createInsertSchema(
checkpointTable,
{
civitai_url: (schema) =>
schema.civitai_url.trim().url({ message: "URL required" }).includes(
"civitai.com/models",
{ message: "civitai.com/models link required" },
),
},
);
export const checkpointVolumeTable = dbSchema.table("checkpoint_volume", {
id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id")
.references(() => usersTable.id, {
// onDelete: "cascade",
}),
org_id: text("org_id"),
volume_name: text("volume_name").notNull(),
created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
disabled: boolean("disabled").default(false).notNull(),
});
export const checkpointRelations = relations(checkpointTable, ({ one }) => ({
user: one(usersTable, {
fields: [checkpointTable.user_id],
references: [usersTable.id],
}),
volume: one(checkpointVolumeTable, {
fields: [checkpointTable.checkpoint_volume_id],
references: [checkpointVolumeTable.id],
}),
}));
export const checkpointVolumeRelations = relations(
checkpointVolumeTable,
({ many, one }) => ({
checkpoint: many(checkpointTable),
user: one(usersTable, {
fields: [checkpointVolumeTable.user_id],
references: [usersTable.id],
}),
}),
);
export type UserType = InferSelectModel<typeof usersTable>; export type UserType = InferSelectModel<typeof usersTable>;
export type WorkflowType = InferSelectModel<typeof workflowTable>; export type WorkflowType = InferSelectModel<typeof workflowTable>;
export type MachineType = InferSelectModel<typeof machinesTable>; export type MachineType = InferSelectModel<typeof machinesTable>;
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>; export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
export type DeploymentType = InferSelectModel<typeof deploymentsTable>; export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
export type CheckpointType = InferSelectModel<typeof checkpointTable>;
export type CheckpointVolumeType = InferSelectModel<
typeof checkpointVolumeTable
>;
+6 -4
View File
@@ -1,8 +1,10 @@
export function remToPx(remValue: number) { export function remToPx(remValue: number) {
let rootFontSize = const rootFontSize =
typeof window === 'undefined' typeof window === "undefined"
? 16 ? 16
: parseFloat(window.getComputedStyle(document.documentElement).fontSize) : parseFloat(
window.getComputedStyle(document.documentElement).fontSize,
);
return remValue * rootFontSize return remValue * rootFontSize
} }
+10 -10
View File
@@ -27,13 +27,13 @@ function rehypeShiki() {
visit(tree, "element", (node) => { visit(tree, "element", (node) => {
if (node.tagName === "pre" && node.children[0]?.tagName === "code") { if (node.tagName === "pre" && node.children[0]?.tagName === "code") {
let codeNode = node.children[0]; const codeNode = node.children[0];
let textNode = codeNode.children[0]; const textNode = codeNode.children[0];
node.properties.code = textNode.value; node.properties.code = textNode.value;
if (node.properties.language) { if (node.properties.language) {
let tokens = highlighter.codeToThemedTokens( const tokens = highlighter.codeToThemedTokens(
textNode.value, textNode.value,
node.properties.language node.properties.language
); );
@@ -53,7 +53,7 @@ function rehypeShiki() {
function rehypeSlugify() { function rehypeSlugify() {
return (tree) => { return (tree) => {
let slugify = slugifyWithCounter(); const slugify = slugifyWithCounter();
visit(tree, "element", (node) => { visit(tree, "element", (node) => {
if (node.tagName === "h2" && !node.properties.id) { if (node.tagName === "h2" && !node.properties.id) {
node.properties.id = slugify(toString(node)); node.properties.id = slugify(toString(node));
@@ -64,10 +64,10 @@ function rehypeSlugify() {
function rehypeAddMDXExports(getExports) { function rehypeAddMDXExports(getExports) {
return (tree) => { return (tree) => {
let exports = Object.entries(getExports(tree)); const exports = Object.entries(getExports(tree));
for (let [name, value] of exports) { for (const [name, value] of exports) {
for (let node of tree.children) { for (const node of tree.children) {
if ( if (
node.type === "mdxjsEsm" && node.type === "mdxjsEsm" &&
new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value) new RegExp(`export\\s+const\\s+${name}\\s*=`).test(node.value)
@@ -76,7 +76,7 @@ function rehypeAddMDXExports(getExports) {
} }
} }
let exportStr = `export const ${name} = ${value}`; const exportStr = `export const ${name} = ${value}`;
tree.children.push({ tree.children.push({
type: "mdxjsEsm", type: "mdxjsEsm",
@@ -93,9 +93,9 @@ function rehypeAddMDXExports(getExports) {
} }
function getSections(node) { function getSections(node) {
let sections = []; const sections = [];
for (let child of node.children ?? []) { for (const child of node.children ?? []) {
if (child.type === "element" && child.tagName === "h2") { if (child.type === "element" && child.tagName === "h2") {
sections.push(`{ sections.push(`{
title: ${JSON.stringify(toString(child))}, title: ${JSON.stringify(toString(child))},
+8 -8
View File
@@ -31,9 +31,9 @@ function extractSections() {
visit(tree, (node) => { visit(tree, (node) => {
if (node.type === "heading" || node.type === "paragraph") { if (node.type === "heading" || node.type === "paragraph") {
let content = toString(excludeObjectExpressions(node)); const content = toString(excludeObjectExpressions(node));
if (node.type === "heading" && node.depth <= 2) { if (node.type === "heading" && node.depth <= 2) {
let hash = node.depth === 1 ? null : slugify(content); const hash = node.depth === 1 ? null : slugify(content);
sections.push([content, hash, []]); sections.push([content, hash, []]);
} else { } else {
sections.at(-1)?.[2].push(content); sections.at(-1)?.[2].push(content);
@@ -45,7 +45,7 @@ function extractSections() {
} }
export default function (nextConfig = {}) { export default function (nextConfig = {}) {
let cache = new Map(); const cache = new Map();
return Object.assign({}, nextConfig, { return Object.assign({}, nextConfig, {
webpack(config, options) { webpack(config, options) {
@@ -53,20 +53,20 @@ export default function (nextConfig = {}) {
test: __filename, test: __filename,
use: [ use: [
createLoader(function () { createLoader(function () {
let appDir = path.resolve("./src/app/(docs)/docs"); const appDir = path.resolve("./src/app/(docs)/docs");
this.addContextDependency(appDir); this.addContextDependency(appDir);
let files = glob.sync("**/*.mdx", { cwd: appDir }); const files = glob.sync("**/*.mdx", { cwd: appDir });
let data = files.map((file) => { const data = files.map((file) => {
let url = `/${file.replace(/(^|\/)page\.mdx$/, "")}`; let url = `/${file.replace(/(^|\/)page\.mdx$/, "")}`;
let mdx = fs.readFileSync(path.join(appDir, file), "utf8"); const mdx = fs.readFileSync(path.join(appDir, file), "utf8");
let sections = []; let sections = [];
if (cache.get(file)?.[0] === mdx) { if (cache.get(file)?.[0] === mdx) {
sections = cache.get(file)[1]; sections = cache.get(file)[1];
} else { } else {
let vfile = { value: mdx, sections }; const vfile = { value: mdx, sections };
processor.runSync(processor.parse(vfile), vfile); processor.runSync(processor.parse(vfile), vfile);
cache.set(file, [mdx, sections]); cache.set(file, [mdx, sections]);
} }
+2
View File
@@ -1,4 +1,6 @@
import type { ResponseConfig } from "@asteasolutions/zod-to-openapi"; import type { ResponseConfig } from "@asteasolutions/zod-to-openapi";
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
export const authError = { export const authError = {
+13
View File
@@ -0,0 +1,13 @@
import { customAlphabet } from "nanoid";
export const nanoid = customAlphabet(
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",
);
const prefixes = {
img: "img",
vid: "vid",
} as const;
export function newId(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], nanoid(16)].join("_");
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { createRun } from "../server/createRun";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { deploymentsTable } from "@/db/schema"; import { deploymentsTable } from "@/db/schema";
import type { App } from "@/routes/app"; import type { App } from "@/routes/app";
import { authError } from "@/routes/authError"; import { authError } from "@/routes/authError";
import { z, createRoute } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { createRun } from "../server/createRun";
const createRunRoute = createRoute({ const createRunRoute = createRoute({
method: "post", method: "post",
@@ -99,7 +99,7 @@ export const registerCreateRunRoute = (app: App) => {
}, },
{ {
status: 500, status: 500,
} },
); );
} }
}); });
+150
View File
@@ -0,0 +1,150 @@
import { db } from "@/db/db";
import { authRequestsTable } from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import { z, createRoute } from "@hono/zod-openapi";
import { eq } from "drizzle-orm";
import jwt from "jsonwebtoken";
import crypto from "crypto";
import { getOrgOrUserDisplayName } from "@/server/getOrgOrUserDisplayName";
import ms from "ms";
const route = createRoute({
method: "get",
path: "/auth-response/:request_id",
tags: ["comfyui"],
summary: "Get an API Key with code",
description:
"This endpoints is specifically built for ComfyUI workflow upload.",
request: {
params: z.object({
request_id: z.string(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({
api_key: z.string(),
name: z.string(),
}),
},
},
description: "The returned API Key",
},
201: {
content: {
"application/json": {
schema: z.object({
message: z.string(),
}),
},
},
description: "The API key is not yet ready",
},
500: {
content: {
"application/json": {
schema: z.object({
error: z.string(),
}),
},
},
description: "Error when fetching the API Key with code",
},
...authError,
},
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export const registerGetAuthResponse = (app: App) => {
return app.openapi(route, async (c) => {
const { request_id } = c.req.valid("param");
try {
const result = await db.query.authRequestsTable.findFirst({
where: eq(authRequestsTable.request_id, request_id),
});
if (result?.api_hash) {
return c.json(
{
message: "Already used.",
},
{
status: 201,
headers: corsHeaders,
},
);
}
if (result && result.user_id) {
const expireTime = "1w";
const token = jwt.sign(
{ user_id: result.user_id, org_id: result.org_id },
process.env.JWT_SECRET!,
{
expiresIn: expireTime,
},
);
const hash = crypto.createHash("sha256").update(token).digest("hex");
const now = new Date();
const expiryDate = new Date(now.getTime() + ms(expireTime));
await db
.update(authRequestsTable)
.set({
api_hash: hash,
expired_date: expiryDate,
})
.where(eq(authRequestsTable.request_id, request_id));
const userName = await getOrgOrUserDisplayName(
result.org_id,
result.user_id,
);
return c.json(
{
api_key: token,
name: userName,
},
{
status: 200,
headers: corsHeaders,
},
);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return c.json(
{
error: errorMessage,
},
{
statusText: "Invalid request",
status: 500,
headers: corsHeaders,
},
);
}
return c.json(
{
message: "Not ready yet.",
},
{
status: 201,
headers: corsHeaders,
},
);
});
};
+100
View File
@@ -0,0 +1,100 @@
import { workflowVersionSchema, workflowVersionTable } from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import { getWorkflowVersion } from "@/server/crudWorkflow";
import { z, createRoute } from "@hono/zod-openapi";
import { createSelectSchema } from "drizzle-zod";
const route = createRoute({
method: "get",
path: "/workflow-version/:version_id",
tags: ["comfyui"],
summary: "Get comfyui workflow",
description: "Use this to retrieve comfyui workflow by id",
request: {
params: z.object({
version_id: z.string(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: createSelectSchema(workflowVersionTable, {
workflow_api: (schema) =>
schema.workflow_api.openapi({
type: "object",
}),
workflow: (schema) =>
schema.workflow.openapi({
type: "object",
}),
snapshot: (schema) =>
schema.snapshot.openapi({
type: "object",
}),
}),
},
},
description: "Retrieve the output",
},
500: {
content: {
"application/json": {
schema: z.object({
error: z.string(),
}),
},
},
description: "Error when uploading the workflow",
},
...authError,
},
});
export const registerGetWorkflowRoute = (app: App) => {
return app.openapi(route, async (c) => {
const { version_id } = c.req.valid("param");
const apiUser = c.get("apiKeyTokenData")!;
if (!apiUser.user_id)
return c.json(
{
error: "Invalid user_id",
},
{
status: 500,
},
);
try {
const workflow_version = await getWorkflowVersion(apiUser, version_id);
if (workflow_version) {
return c.json(workflow_version, {
status: 200,
});
} else {
return c.json(
{
error: "No version found",
},
{
status: 500,
},
);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return c.json(
{
error: errorMessage,
},
{
statusText: "Invalid request",
status: 500,
},
);
}
});
};
+3 -16
View File
@@ -3,20 +3,7 @@ import { authError } from "@/routes/authError";
import { getFileDownloadUrl } from "@/server/getFileDownloadUrl"; import { getFileDownloadUrl } from "@/server/getFileDownloadUrl";
import { handleResourceUpload } from "@/server/resource"; import { handleResourceUpload } from "@/server/resource";
import { z, createRoute } from "@hono/zod-openapi"; import { z, createRoute } from "@hono/zod-openapi";
import { customAlphabet } from "nanoid"; import { newId } from "./newId";
export const nanoid = customAlphabet(
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
);
const prefixes = {
img: "img",
vid: "vid",
} as const;
export function newId(prefix: keyof typeof prefixes): string {
return [prefixes[prefix], nanoid(16)].join("_");
}
const uploadUrlRoute = createRoute({ const uploadUrlRoute = createRoute({
method: "get", method: "get",
@@ -96,7 +83,7 @@ export const registerUploadRoute = (app: App) => {
file_id: id, file_id: id,
download_url: await getFileDownloadUrl(filePath), download_url: await getFileDownloadUrl(filePath),
}, },
200 200,
); );
} catch (error: unknown) { } catch (error: unknown) {
const errorMessage = const errorMessage =
@@ -107,7 +94,7 @@ export const registerUploadRoute = (app: App) => {
}, },
{ {
status: 500, status: 500,
} },
); );
} }
}); });
@@ -0,0 +1,197 @@
import { db } from "@/db/db";
import {
snapshotType,
workflowAPIType,
workflowTable,
workflowType,
} from "@/db/schema";
import type { App } from "@/routes/app";
import { authError } from "@/routes/authError";
import {
createNewWorkflow,
createNewWorkflowVersion,
} from "@/server/createNewWorkflow";
import { z, createRoute } from "@hono/zod-openapi";
import { and, eq, isNull } from "drizzle-orm";
const route = createRoute({
method: "post",
path: "/workflow",
tags: ["comfyui"],
summary: "Upload workflow from ComfyUI",
description:
"This endpoints is specifically built for ComfyUI workflow upload.",
request: {
body: {
content: {
"application/json": {
schema: z.object({
workflow_id: z.string().optional(),
workflow_name: z.string().min(1).optional(),
workflow: workflowType,
workflow_api: workflowAPIType,
snapshot: snapshotType,
}),
},
},
},
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({
workflow_id: z.string(),
version: z.string(),
}),
},
},
description: "Retrieve the output",
},
500: {
content: {
"application/json": {
schema: z.object({
error: z.string(),
}),
},
},
description: "Error when uploading the workflow",
},
...authError,
},
});
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
export const registerWorkflowUploadRoute = (app: App) => {
app.openapi(route, async (c) => {
const {
// user_id,
workflow,
workflow_api,
workflow_id: _workflow_id,
workflow_name,
snapshot,
} = c.req.valid("json");
const { org_id, user_id } = c.get("apiKeyTokenData")!;
if (!user_id)
return c.json(
{
error: "Invalid user_id",
},
{
headers: corsHeaders,
status: 500,
},
);
let workflow_id = _workflow_id;
let version = -1;
try {
if ((!workflow_id || workflow_id.length === 0) && workflow_name) {
// Create a new parent workflow
const { workflow_id: _workflow_id, version: _version } =
await createNewWorkflow({
user_id: user_id,
org_id: org_id,
workflow_name: workflow_name,
workflowData: {
workflow,
workflow_api,
snapshot,
},
});
workflow_id = _workflow_id;
version = _version;
} else if (workflow_id) {
const _workflow = await db
.select()
.from(workflowTable)
.where(
and(
eq(workflowTable.id, workflow_id),
eq(workflowTable.user_id, user_id),
org_id
? eq(workflowTable.org_id, org_id)
: isNull(workflowTable.org_id),
),
);
if (_workflow.length === 0) {
return c.json(
{
error: "Invalid workflow_id",
},
{
status: 500,
statusText: "Invalid workflow_id",
headers: corsHeaders,
},
);
}
// Case 2 update workflow
const { version: _version } = await createNewWorkflowVersion({
workflow_id: workflow_id,
workflowData: {
workflow,
workflow_api,
snapshot,
},
});
version = _version;
} else {
return c.json(
{
error: "Invalid request, missing either workflow_id or name",
},
{
status: 500,
statusText: "Invalid request",
headers: corsHeaders,
},
);
}
} catch (error: unknown) {
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
return c.json(
{
error: errorMessage,
},
{
statusText: "Invalid request",
status: 500,
headers: corsHeaders,
},
);
}
return c.json(
{
workflow_id: workflow_id,
version: version,
},
{
status: 200,
headers: corsHeaders,
},
);
});
app.route("/upload-workflow").options(async (c) => {
return new Response(null, {
status: 204,
headers: corsHeaders,
});
});
};
+3 -2
View File
@@ -1,9 +1,10 @@
import { z } from "zod"; import { z } from "zod";
export const APIKeyBodyRequest = z.object({ export const APIKeyBodyRequest = z.object({
user_id: z.string().optional(), user_id: z.string().optional().nullable(),
org_id: z.string().optional(), org_id: z.string().optional().nullable(),
iat: z.number(), iat: z.number(),
exp: z.number().optional(),
}); });
export type APIKeyUserType = z.infer<typeof APIKeyBodyRequest>; export type APIKeyUserType = z.infer<typeof APIKeyBodyRequest>;
-5
View File
@@ -1,5 +0,0 @@
import { insertCivitaiCheckpointSchema } from "@/db/schema";
export const addCivitaiCheckpointSchema = insertCivitaiCheckpointSchema.pick({
civitai_url: true,
});
+1 -1
View File
@@ -17,7 +17,7 @@ export const insertCustomMachineSchema = createInsertSchema(machinesTable, {
comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804", comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
git_custom_nodes: { git_custom_nodes: {
"https://github.com/BennyKok/comfyui-deploy.git": { "https://github.com/BennyKok/comfyui-deploy.git": {
hash: "1e99474780951043e1b001e1877de3f2eb45cfba", hash: "43fe0a384aa5fa9e141d4a264b2ed40a73b817bc",
disabled: false, disabled: false,
}, },
}, },
+41 -1
View File
@@ -1,6 +1,46 @@
import { db } from "@/db/db"; import { db } from "@/db/db";
import type { WorkflowVersionType } from "@/db/schema"; import type { WorkflowVersionType } from "@/db/schema";
import { workflowTable, workflowVersionTable } from "@/db/schema"; import { workflowTable, workflowVersionTable } from "@/db/schema";
import { eq, sql } from "drizzle-orm";
export async function createNewWorkflowVersion({
workflow_id,
workflowData,
}: {
workflow_id: string;
workflowData: Pick<
WorkflowVersionType,
"workflow" | "workflow_api" | "snapshot"
>;
}) {
// Add a new version
const data = await db
.insert(workflowVersionTable)
.values({
workflow_id,
...workflowData,
version: sql`(
SELECT COALESCE(MAX(version), 0) + 1
FROM ${workflowVersionTable}
WHERE workflow_id = ${workflow_id}
)`,
})
.returning();
const version = data[0].version;
// Touch up the last updated time
await db
.update(workflowTable)
.set({
updated_at: new Date(),
})
.where(eq(workflowTable.id, workflow_id))
.returning();
return {
version,
};
}
export async function createNewWorkflow({ export async function createNewWorkflow({
workflow_name, workflow_name,
@@ -10,7 +50,7 @@ export async function createNewWorkflow({
}: { }: {
workflow_name: string; workflow_name: string;
user_id: string; user_id: string;
org_id?: string; org_id?: string | null;
workflowData: Pick< workflowData: Pick<
WorkflowVersionType, WorkflowVersionType,
"workflow" | "workflow_api" | "snapshot" "workflow" | "workflow_api" | "snapshot"
+20 -6
View File
@@ -1,6 +1,5 @@
"use server"; "use server";
import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db"; import { db } from "@/db/db";
import type { import type {
MachineType, MachineType,
@@ -16,6 +15,7 @@ import { and, eq } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import "server-only"; import "server-only";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { withServerPromise } from "./withServerPromise";
export const createRun = withServerPromise( export const createRun = withServerPromise(
async ({ async ({
@@ -38,7 +38,7 @@ export const createRun = withServerPromise(
? await db.query.machinesTable.findFirst({ ? await db.query.machinesTable.findFirst({
where: and( where: and(
eq(machinesTable.id, machine_id), eq(machinesTable.id, machine_id),
eq(machinesTable.disabled, false) eq(machinesTable.disabled, false),
), ),
}) })
: machine_id; : machine_id;
@@ -90,7 +90,12 @@ export const createRun = withServerPromise(
Object.entries(workflow_api).forEach(([_, node]) => { Object.entries(workflow_api).forEach(([_, node]) => {
if (node.inputs["input_id"] === key) { if (node.inputs["input_id"] === key) {
node.inputs["input_id"] = inputs[key]; node.inputs["input_id"] = inputs[key];
// Fix for external text default value
if (node.class_type == "ComfyUIDeployExternalText") {
node.inputs["default_value"] = inputs[key];
}
} }
}); });
} }
} }
@@ -143,7 +148,7 @@ export const createRun = withServerPromise(
throw new Error( throw new Error(
`Error creating run, ${ `Error creating run, ${
___result.statusText ___result.statusText
} ${await ___result.text()}` } ${await ___result.text()}`,
); );
console.log(_data, ___result); console.log(_data, ___result);
break; break;
@@ -177,7 +182,7 @@ export const createRun = withServerPromise(
throw new Error( throw new Error(
`Error creating run, ${ `Error creating run, ${
__result.statusText __result.statusText
} ${await __result.text()}` } ${await __result.text()}`,
); );
console.log(data, __result); console.log(data, __result);
break; break;
@@ -199,7 +204,7 @@ export const createRun = withServerPromise(
let message = `Error creating run, ${_result.statusText}`; let message = `Error creating run, ${_result.statusText}`;
try { try {
const result = await ComfyAPI_Run.parseAsync( const result = await ComfyAPI_Run.parseAsync(
await _result.json() await _result.json(),
); );
message += ` ${result.node_errors}`; message += ` ${result.node_errors}`;
} catch (error) {} } catch (error) {}
@@ -219,11 +224,20 @@ export const createRun = withServerPromise(
throw e; throw e;
} }
// It successfully started, update the started_at time
await db
.update(workflowRunsTable)
.set({
started_at: new Date(),
})
.where(eq(workflowRunsTable.id, workflow_run[0].id));
return { return {
workflow_run_id: workflow_run[0].id, workflow_run_id: workflow_run[0].id,
message: "Successful workflow run", message: "Successful workflow run",
}; };
} },
); );
export async function checkStatus(run_id: string) { export async function checkStatus(run_id: string) {
@@ -4,8 +4,10 @@ import {
workflowTable, workflowTable,
workflowVersionTable, workflowVersionTable,
} from "@/db/schema"; } from "@/db/schema";
import { APIKeyUserType } from "@/server/APIKeyBodyRequest";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { and, desc, eq, isNull } from "drizzle-orm"; import { and, desc, eq, isNull } from "drizzle-orm";
import { redirect } from "next/navigation";
export async function getAllUserWorkflow() { export async function getAllUserWorkflow() {
const { userId, orgId } = await auth(); const { userId, orgId } = await auth();
@@ -51,3 +53,29 @@ export async function getAllUserWorkflow() {
return workflow; return workflow;
} }
export async function getWorkflowVersion(
apiUser: APIKeyUserType,
version_id: string,
) {
const { org_id, user_id } = apiUser;
if (!user_id) {
throw new Error("No user id");
}
const parentWorkflow = await db.query.workflowTable.findFirst({
where:
org_id != undefined
? eq(workflowTable.org_id, org_id)
: and(eq(workflowTable.user_id, user_id), isNull(workflowTable.org_id)),
});
if (!parentWorkflow) {
throw new Error("No workflow found");
}
return db.query.workflowVersionTable.findFirst({
where: eq(workflowVersionTable.id, version_id),
});
}
+17 -12
View File
@@ -1,23 +1,28 @@
"use server"; "use server";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { apiKeyTable } from "@/db/schema"; import { apiKeyTable, authRequestsTable } from "@/db/schema";
import { withServerPromise } from "@/server/withServerPromise";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { and, desc, eq, isNull } from "drizzle-orm"; import { and, desc, eq, isNull } from "drizzle-orm";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
// export const nanoid = customAlphabet( export const createAuthRequest = withServerPromise(
// "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" async (request_id: string) => {
// ); const { userId, orgId } = auth();
// const prefixes = { const result = await db.insert(authRequestsTable).values({
// cd: "cd", request_id: request_id,
// } as const; user_id: userId,
org_id: orgId,
});
// function newId(prefix: keyof typeof prefixes): string { return {
// return [prefixes[prefix], nanoid(16)].join("_"); message: "Auth request created, you may now return to your application.",
// } };
},
);
export async function addNewAPIKey(name: string) { export async function addNewAPIKey(name: string) {
const { userId, orgId } = auth(); const { userId, orgId } = auth();
@@ -29,7 +34,7 @@ export async function addNewAPIKey(name: string) {
if (orgId) { if (orgId) {
token = jwt.sign( token = jwt.sign(
{ user_id: userId, org_id: orgId }, { user_id: userId, org_id: orgId },
process.env.JWT_SECRET! process.env.JWT_SECRET!,
); );
} else { } else {
token = jwt.sign({ user_id: userId }, process.env.JWT_SECRET!); token = jwt.sign({ user_id: userId }, process.env.JWT_SECRET!);
@@ -93,7 +98,7 @@ export async function getAPIKeys() {
where: and( where: and(
eq(apiKeyTable.user_id, userId), eq(apiKeyTable.user_id, userId),
isNull(apiKeyTable.org_id), isNull(apiKeyTable.org_id),
eq(apiKeyTable.revoked, false) eq(apiKeyTable.revoked, false),
), ),
orderBy: desc(apiKeyTable.created_at), orderBy: desc(apiKeyTable.created_at),
}); });
-224
View File
@@ -1,224 +0,0 @@
"use server";
import { auth } from "@clerk/nextjs";
import {
checkpointTable,
CheckpointType,
volumeTable,
CheckpointVolumeType,
} from "@/db/schema";
import { withServerPromise } from "./withServerPromise";
import { redirect } from "next/navigation";
import { db } from "@/db/db";
import type { z } from "zod";
import { headers } from "next/headers";
import { addCivitaiCheckpointSchema } from "./addCheckpointSchema";
import { and, eq, isNull } from "drizzle-orm";
import { CivitaiModelResponse } from "@/types/civitai";
export async function getCheckpoints() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpoints = await db
.select()
.from(checkpointTable)
.where(
orgId
? eq(checkpointTable.org_id, orgId)
// make sure org_id is null
: and(
eq(checkpointTable.user_id, userId),
isNull(checkpointTable.org_id),
),
);
return checkpoints;
}
export async function getCheckpointById(id: string) {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpoint = await db
.select()
.from(checkpointTable)
.where(
and(
orgId ? eq(checkpointTable.org_id, orgId) : and(
eq(checkpointTable.user_id, userId),
isNull(checkpointTable.org_id),
),
eq(checkpointTable.id, id),
),
);
return checkpoint[0];
}
export async function getCheckpointVolumes() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const checkpointVolume = await db
.select()
.from(volumeTable)
.where(
and(
orgId
? eq(volumeTable.org_id, orgId)
// make sure org_id is null
: and(
eq(volumeTable.user_id, userId),
isNull(volumeTable.org_id),
),
eq(volumeTable.disabled, false),
),
);
return checkpointVolume;
}
export async function addCheckpointVolume() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
// Insert the new volume into the checkpointVolumeTable
const insertedVolume = await db
.insert(volumeTable)
.values({
user_id: userId,
org_id: orgId,
volume_name: `checkpoints_${userId}`,
// created_at and updated_at will be set to current timestamp by default
disabled: false, // Default value
})
.returning(); // Returns the inserted row
return insertedVolume;
}
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export const addCivitaiCheckpoint = withServerPromise(
async (data: z.infer<typeof addCivitaiCheckpointSchema>) => {
const { userId, orgId } = auth();
if (!data.civitai_url) return { error: "no civitai_url" };
if (!userId) return { error: "No user id" };
const { url, modelVersionId } = getUrl(data?.civitai_url);
const civitaiModelRes = await fetch(url)
.then((x) => x.json())
.then((a) => {
return CivitaiModelResponse.parse(a);
});
if (civitaiModelRes?.modelVersions?.length === 0) {
return; // no versions to download
}
let selectedModelVersion;
let selectedModelVersionId: string | null = modelVersionId;
if (!selectedModelVersionId) {
selectedModelVersion = civitaiModelRes.modelVersions[0];
selectedModelVersionId = civitaiModelRes.modelVersions[0].id.toString();
} else {
selectedModelVersion = civitaiModelRes.modelVersions.find((version) =>
version.id.toString() === selectedModelVersionId
);
if (!selectedModelVersion) {
return; // version id is wrong
}
selectedModelVersionId = selectedModelVersion?.id.toString();
}
const checkpointVolumes = await getCheckpointVolumes();
let cVolume;
if (checkpointVolumes.length === 0) {
const volume = await addCheckpointVolume();
cVolume = volume[0];
} else {
cVolume = checkpointVolumes[0];
}
const a = await db
.insert(checkpointTable)
.values({
user_id: userId,
org_id: orgId,
upload_type: "civitai",
civitai_id: civitaiModelRes.id.toString(),
civitai_version_id: selectedModelVersionId,
civitai_url: data.civitai_url,
civitai_download_url: selectedModelVersion.downloadUrl,
civitai_model_response: civitaiModelRes,
checkpoint_volume_id: cVolume.id,
})
.returning();
const b = a[0];
await uploadCheckpoint(data, b, cVolume);
redirect(`/checkpoints/${b.id}`);
},
);
async function uploadCheckpoint(
data: z.infer<typeof addCivitaiCheckpointSchema>,
b: CheckpointType,
v: CheckpointVolumeType,
) {
const headersList = headers();
const domain = headersList.get("x-forwarded-host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
if (domain === "") {
throw new Error("No domain");
}
// Call remote builder
const result = await fetch(
`${process.env.MODAL_BUILDER_URL!}/upload_volume`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
download_url: data.civitai_url,
volume_name: v.volume_name,
volume_id: v.id,
callback_url: `${protocol}://${domain}/api/volume-updated`,
}),
},
);
if (!result.ok) {
const error_log = await result.text();
await db
.update(checkpointTable)
.set({
...data,
status: "error",
build_log: error_log,
})
.where(eq(checkpointTable.id, b.id));
throw new Error(`Error: ${result.statusText} ${error_log}`);
} else {
// setting the build machine id
const json = await result.json();
await db
.update(checkpointTable)
.set({
...data,
upload_machine_id: json.build_machine_instance_id,
})
.where(eq(checkpointTable.id, b.id));
}
}
+56 -22
View File
@@ -7,17 +7,19 @@ import { createNewWorkflow } from "@/server/createNewWorkflow";
import { addCustomMachine } from "@/server/curdMachine"; import { addCustomMachine } from "@/server/curdMachine";
import { withServerPromise } from "@/server/withServerPromise"; import { withServerPromise } from "@/server/withServerPromise";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { and, eq, isNull } from "drizzle-orm"; import { clerkClient } from "@clerk/nextjs/server";
import slugify from "@sindresorhus/slugify";
import { and, eq, isNull, or } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import "server-only"; import "server-only";
import { validate as isValidUUID } from "uuid";
import type { z } from "zod"; import type { z } from "zod";
export async function createDeployments( export async function createDeployments(
workflow_id: string, workflow_id: string,
version_id: string, version_id: string,
machine_id: string, machine_id: string,
environment: DeploymentType["environment"] environment: DeploymentType["environment"],
) { ) {
const { userId, orgId } = auth(); const { userId, orgId } = auth();
if (!userId) throw new Error("No user id"); if (!userId) throw new Error("No user id");
@@ -30,7 +32,7 @@ export async function createDeployments(
const existingDeployment = await db.query.deploymentsTable.findFirst({ const existingDeployment = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.workflow_id, workflow_id), eq(deploymentsTable.workflow_id, workflow_id),
eq(deploymentsTable.environment, environment) eq(deploymentsTable.environment, environment),
), ),
}); });
@@ -45,6 +47,27 @@ export async function createDeployments(
}) })
.where(eq(deploymentsTable.id, existingDeployment.id)); .where(eq(deploymentsTable.id, existingDeployment.id));
} else { } else {
const workflow = await db.query.workflowTable.findFirst({
where: eq(workflowTable.id, workflow_id),
with: {
user: {
columns: {
name: true,
},
},
},
});
if (!workflow) throw new Error("No workflow found");
const userName = workflow.org_id
? await clerkClient.organizations
.getOrganization({
organizationId: workflow.org_id,
})
.then((x) => x.name)
: workflow.user.name;
await db.insert(deploymentsTable).values({ await db.insert(deploymentsTable).values({
user_id: userId, user_id: userId,
workflow_id, workflow_id,
@@ -52,6 +75,8 @@ export async function createDeployments(
machine_id, machine_id,
environment, environment,
org_id: orgId, org_id: orgId,
// only create share slug if this is public share
share_slug: environment == "public-share" ? slugify(`${userName} ${workflow.name}`) : null
}); });
} }
revalidatePath(`/${workflow_id}`); revalidatePath(`/${workflow_id}`);
@@ -68,7 +93,7 @@ export async function findAllDeployments() {
where: and( where: and(
orgId orgId
? eq(workflowTable.org_id, orgId) ? eq(workflowTable.org_id, orgId)
: and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)) : and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)),
), ),
columns: { columns: {
name: true, name: true,
@@ -97,7 +122,9 @@ export async function findSharedDeployment(workflow_id: string) {
const deploymentData = await db.query.deploymentsTable.findFirst({ const deploymentData = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, workflow_id) isValidUUID(workflow_id)
? eq(deploymentsTable.id, workflow_id)
: eq(deploymentsTable.share_slug, workflow_id),
), ),
with: { with: {
user: true, user: true,
@@ -118,15 +145,20 @@ export async function findSharedDeployment(workflow_id: string) {
export const removePublicShareDeployment = withServerPromise( export const removePublicShareDeployment = withServerPromise(
async (deployment_id: string) => { async (deployment_id: string) => {
await db const [removed] = await db
.delete(deploymentsTable) .delete(deploymentsTable)
.where( .where(
and( and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id) eq(deploymentsTable.id, deployment_id),
) ),
); )
} .returning();
// revalidatePath(
// `/workflows/${removed.workflow_id}`
// )
},
); );
export const cloneWorkflow = withServerPromise( export const cloneWorkflow = withServerPromise(
@@ -134,7 +166,7 @@ export const cloneWorkflow = withServerPromise(
const deployment = await db.query.deploymentsTable.findFirst({ const deployment = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id) eq(deploymentsTable.id, deployment_id),
), ),
with: { with: {
version: true, version: true,
@@ -164,14 +196,14 @@ export const cloneWorkflow = withServerPromise(
return { return {
message: "Successfully cloned workflow", message: "Successfully cloned workflow",
}; };
} },
); );
export const cloneMachine = withServerPromise(async (deployment_id: string) => { export const cloneMachine = withServerPromise(async (deployment_id: string) => {
const deployment = await db.query.deploymentsTable.findFirst({ const deployment = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id) eq(deploymentsTable.id, deployment_id),
), ),
with: { with: {
machine: true, machine: true,
@@ -209,15 +241,17 @@ export async function findUserShareDeployment(share_id: string) {
.from(deploymentsTable) .from(deploymentsTable)
.where( .where(
and( and(
eq(deploymentsTable.id, share_id), isValidUUID(share_id)
? eq(deploymentsTable.id, share_id)
: eq(deploymentsTable.share_slug, share_id),
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
orgId orgId
? eq(deploymentsTable.org_id, orgId) ? eq(deploymentsTable.org_id, orgId)
: and( : and(
eq(deploymentsTable.user_id, userId), eq(deploymentsTable.user_id, userId),
isNull(deploymentsTable.org_id) isNull(deploymentsTable.org_id),
) ),
) ),
); );
if (!deployment) throw new Error("No deployment found"); if (!deployment) throw new Error("No deployment found");
@@ -243,11 +277,11 @@ export const updateSharePageInfo = withServerPromise(
.where( .where(
and( and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, id) eq(deploymentsTable.id, id),
) ),
) )
.returning(); .returning();
return { message: "Info Updated" }; return { message: "Info Updated" };
} },
); );
+49
View File
@@ -0,0 +1,49 @@
"use server";
import { getMachineById } from "@/server/curdMachine";
import { auth } from "@clerk/nextjs";
import jwt from "jsonwebtoken";
import { getOrgOrUserDisplayName } from "@/server/getOrgOrUserDisplayName";
import { withServerPromise } from "@/server/withServerPromise";
import "server-only";
import { headers } from "next/headers";
export const editWorkflowOnMachine = withServerPromise(
async (workflow_version_id: string, machine_id: string) => {
const { userId, orgId } = auth();
const headersList = headers();
const host = headersList.get("host") || "";
const protocol = headersList.get("x-forwarded-proto") || "";
const domain = `${protocol}://${host}`;
if (!userId) {
throw new Error("No user id");
}
const machine = await getMachineById(machine_id);
const expireTime = "1w";
const token = jwt.sign(
{ user_id: userId, org_id: orgId },
process.env.JWT_SECRET!,
{
expiresIn: expireTime,
},
);
const userName = await getOrgOrUserDisplayName(orgId, userId);
let endpoint = machine.endpoint;
if (machine.type === "comfy-deploy-serverless") {
endpoint = machine.endpoint.replace("comfyui-api", "comfyui-app");
}
return `${endpoint}?workflow_version_id=${encodeURIComponent(
workflow_version_id,
)}&auth_token=${encodeURIComponent(token)}&org_display=${encodeURIComponent(
userName,
)}&origin=${encodeURIComponent(domain)}`;
},
);

Some files were not shown because too many files have changed in this diff Show More