feat: add millon js, add models picker dialog, update builder

This commit is contained in:
BennyKok 2024-01-07 17:22:28 +08:00
parent 3c4bce630e
commit 01a9c1a1d6
33 changed files with 1693 additions and 190 deletions

View File

@ -1,9 +1,10 @@
from typing import Union, Optional, Dict from typing import Union, Optional, Dict, List
from pydantic import BaseModel from pydantic import BaseModel, Field, field_validator
from fastapi import FastAPI, HTTPException, WebSocket, BackgroundTasks, WebSocketDisconnect from fastapi import FastAPI, HTTPException, WebSocket, BackgroundTasks, WebSocketDisconnect
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.logger import logger as fastapi_logger from fastapi.logger import logger as fastapi_logger
import os import os
from enum import Enum
import json import json
import subprocess import subprocess
import time import time
@ -104,17 +105,49 @@ class Snapshot(BaseModel):
comfyui: str comfyui: str
git_custom_nodes: Dict[str, GitCustomNodes] git_custom_nodes: Dict[str, GitCustomNodes]
class Model(BaseModel):
name: str
type: str
base: str
save_path: str
description: str
reference: str
filename: str
url: str
class GPUType(str, Enum):
T4 = "T4"
A10G = "A10G"
A100 = "A100"
L4 = "L4"
class Item(BaseModel): class Item(BaseModel):
machine_id: str machine_id: str
name: str name: str
snapshot: Snapshot snapshot: Snapshot
models: List[Model]
callback_url: str callback_url: str
gpu: GPUType = Field(default=GPUType.T4)
@field_validator('gpu')
@classmethod
def check_gpu(cls, value):
if value not in GPUType.__members__:
raise ValueError(f"Invalid GPU option. Choose from: {', '.join(GPUType.__members__.keys())}")
return GPUType(value)
@app.websocket("/ws/{machine_id}") @app.websocket("/ws/{machine_id}")
async def websocket_endpoint(websocket: WebSocket, machine_id: str): async def websocket_endpoint(websocket: WebSocket, machine_id: str):
await websocket.accept() await websocket.accept()
machine_id_websocket_dict[machine_id] = websocket machine_id_websocket_dict[machine_id] = websocket
# Send existing logs
if machine_id in machine_logs_cache:
await websocket.send_text(json.dumps({"event": "LOGS", "data": {
"machine_id": machine_id,
"logs": json.dumps(machine_logs_cache[machine_id]) ,
"timestamp": time.time()
}}))
try: try:
while True: while True:
data = await websocket.receive_text() data = await websocket.receive_text()
@ -156,6 +189,9 @@ async def create_item(item: Item):
return JSONResponse(status_code=200, content={"message": "Build Queued"}) return JSONResponse(status_code=200, content={"message": "Build Queued"})
# Initialize the logs cache
machine_logs_cache = {}
async def build_logic(item: Item): async def build_logic(item: Item):
# Deploy to modal # Deploy to modal
folder_path = f"/app/builds/{item.machine_id}" folder_path = f"/app/builds/{item.machine_id}"
@ -175,7 +211,8 @@ async def build_logic(item: Item):
# Write the config file # Write the config file
config = { config = {
"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
} }
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))
@ -183,33 +220,40 @@ async def build_logic(item: Item):
with open(f"{folder_path}/data/snapshot.json", "w") as f: with open(f"{folder_path}/data/snapshot.json", "w") as f:
f.write(item.snapshot.json()) f.write(item.snapshot.json())
with open(f"{folder_path}/data/models.json", "w") as f:
models_json_list = [model.dict() for model in item.models]
models_json_string = json.dumps(models_json_list)
f.write(models_json_string)
# os.chdir(folder_path) # os.chdir(folder_path)
# process = subprocess.Popen(f"modal deploy {folder_path}/app.py", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True) # process = subprocess.Popen(f"modal deploy {folder_path}/app.py", stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
process = await asyncio.subprocess.create_subprocess_shell( process = await asyncio.subprocess.create_subprocess_shell(
f"modal deploy app.py", f"modal deploy app.py",
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
cwd=folder_path cwd=folder_path,
# env={**os.environ, "PYTHONUNBUFFERED": "1"}
) )
url = None url = None
# Initialize the logs cache if item.machine_id not in machine_logs_cache:
machine_logs_cache = [] machine_logs_cache[item.machine_id] = []
# Stream the output machine_logs = machine_logs_cache[item.machine_id]
# Read output
async def read_stream(stream, isStderr):
while True: while True:
line = await process.stdout.readline() line = await stream.readline()
error = await process.stderr.readline() if line:
if not line and not error:
break
l = line.decode('utf-8').strip() l = line.decode('utf-8').strip()
e = error.decode('utf-8').strip()
if l != "": if l == "":
continue
if not isStderr:
logger.info(l) logger.info(l)
machine_logs_cache.append({ machine_logs.append({
"logs": l, "logs": l,
"timestamp": time.time() "timestamp": time.time()
}) })
@ -230,7 +274,7 @@ async def build_logic(item: Item):
url = l url = l
if url: if url:
machine_logs_cache.append({ machine_logs.append({
"logs": f"App image built, url: {url}", "logs": f"App image built, url: {url}",
"timestamp": time.time() "timestamp": time.time()
}) })
@ -241,10 +285,14 @@ async def build_logic(item: Item):
"logs": f"App image built, url: {url}", "logs": f"App image built, url: {url}",
"timestamp": time.time() "timestamp": time.time()
}})) }}))
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "FINISHED", "data": {
"status": "succuss",
}}))
if e != "": else:
logger.info(e) # is error
machine_logs_cache.append({ logger.error(l)
machine_logs.append({
"logs": e, "logs": e,
"timestamp": time.time() "timestamp": time.time()
}) })
@ -255,7 +303,16 @@ async def build_logic(item: Item):
"logs": e, "logs": e,
"timestamp": time.time() "timestamp": time.time()
}})) }}))
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "FINISHED", "data": {
"status": "failed",
}}))
else:
break
stdout_task = asyncio.create_task(read_stream(process.stdout, False))
stderr_task = asyncio.create_task(read_stream(process.stderr, True))
await asyncio.wait([stdout_task, stderr_task])
# Wait for the subprocess to finish # Wait for the subprocess to finish
await process.wait() await process.wait()
@ -273,28 +330,38 @@ async def build_logic(item: Item):
if process.returncode != 0: if process.returncode != 0:
logger.info("An error occurred.") logger.info("An error occurred.")
# Send a post request with the json body machine_id to the callback url # Send a post request with the json body machine_id to the callback url
machine_logs_cache.append({ machine_logs.append({
"logs": "Unable to build the app image.", "logs": "Unable to build the app image.",
"timestamp": time.time() "timestamp": time.time()
}) })
requests.post(item.callback_url, json={"machine_id": item.machine_id, "build_log": json.dumps(machine_logs_cache)}) requests.post(item.callback_url, json={"machine_id": item.machine_id, "build_log": json.dumps(machine_logs)})
if item.machine_id in machine_logs_cache:
del machine_logs_cache[item.machine_id]
return return
# return JSONResponse(status_code=400, content={"error": "Unable to build the app image."}) # return JSONResponse(status_code=400, content={"error": "Unable to build the app image."})
# app_suffix = "comfyui-app" # app_suffix = "comfyui-app"
if url is None: if url is None:
machine_logs_cache.append({ machine_logs.append({
"logs": "App image built, but url is None, unable to parse the url.", "logs": "App image built, but url is None, unable to parse the url.",
"timestamp": time.time() "timestamp": time.time()
}) })
requests.post(item.callback_url, json={"machine_id": item.machine_id, "build_log": json.dumps(machine_logs_cache)}) requests.post(item.callback_url, json={"machine_id": item.machine_id, "build_log": json.dumps(machine_logs)})
if item.machine_id in machine_logs_cache:
del machine_logs_cache[item.machine_id]
return return
# return JSONResponse(status_code=400, content={"error": "App image built, but url is None, unable to parse the url."}) # return JSONResponse(status_code=400, content={"error": "App image built, but url is None, unable to parse the url."})
# example https://bennykok--my-app-comfyui-app.modal.run/ # example https://bennykok--my-app-comfyui-app.modal.run/
# my_url = f"https://{MODAL_ORG}--{item.container_id}-{app_suffix}.modal.run" # my_url = f"https://{MODAL_ORG}--{item.container_id}-{app_suffix}.modal.run"
requests.post(item.callback_url, json={"machine_id": item.machine_id, "endpoint": url, "build_log": json.dumps(machine_logs_cache)}) requests.post(item.callback_url, json={"machine_id": item.machine_id, "endpoint": url, "build_log": json.dumps(machine_logs)})
if item.machine_id in machine_logs_cache:
del machine_logs_cache[item.machine_id]
logger.info("done") logger.info("done")
logger.info(url) logger.info(url)

View File

@ -57,6 +57,7 @@ WORKDIR /
COPY /data/install_deps.py . COPY /data/install_deps.py .
COPY /data/deps.json . COPY /data/deps.json .
COPY /data/models.json .
RUN python3 install_deps.py RUN python3 install_deps.py
WORKDIR /comfyui/custom_nodes WORKDIR /comfyui/custom_nodes

View File

@ -105,7 +105,7 @@ 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="T4") @stub.function(image=target_image, gpu=config["gpu"])
def run(input: Input): def run(input: Input):
import subprocess import subprocess
import time import time

View File

@ -1 +1 @@
config = {"name": "my-app", "deploy_test": "True"} config = {"name": "my-app", "deploy_test": "True", "gpu": "T4"}

View File

@ -28,9 +28,10 @@ def check_server(url, retries=50, delay=500):
) )
return False return False
check_server("http://127.0.0.1:8188") root_url = "http://127.0.0.1:8188"
check_server(root_url)
url = "http://127.0.0.1:8188/customnode/install"
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
# Load JSON array from deps.json # Load JSON array from deps.json
@ -39,12 +40,15 @@ with open('deps.json') as f:
# Make a POST request for each package # Make a POST request for each package
for package in packages: for package in packages:
response = requests.request("POST", url, json=package, headers=headers) response = requests.request("POST", f"{root_url}/customnode/install", json=package, headers=headers)
print(response.text) print(response.text)
# restore_snapshot_url = "http://127.0.0.1:8188/snapshot/restore?target=snapshot" with open('models.json') as f:
# response = requests.request("GET", restore_snapshot_url, headers=headers) models = json.load(f)
# print(response.text)
for model in models:
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()

View File

@ -0,0 +1,12 @@
[
{
"name": "v1-5-pruned-emaonly.ckpt",
"type": "checkpoints",
"base": "SD1.5",
"save_path": "default",
"description": "Stable Diffusion 1.5 base model",
"reference": "https://huggingface.co/runwayml/stable-diffusion-v1-5",
"filename": "v1-5-pruned-emaonly.ckpt",
"url": "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.ckpt"
}
]

Binary file not shown.

View File

@ -0,0 +1 @@
ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "models" jsonb;

View File

@ -0,0 +1,716 @@
{
"id": "027290ed-c130-4d6f-ae37-3f468c50cbc6",
"prevId": "9153404d-9279-4f43-a61f-7f5efefc12b7",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"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"
}
},
"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"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -162,6 +162,13 @@
"when": 1704453649633, "when": 1704453649633,
"tag": "0022_petite_bishop", "tag": "0022_petite_bishop",
"breakpoints": true "breakpoints": true
},
{
"idx": 23,
"version": "5",
"when": 1704540132567,
"tag": "0023_fair_ikaris",
"breakpoints": true
} }
] ]
} }

View File

@ -1,3 +1,4 @@
import million from 'million/compiler';
import { recmaPlugins } from "./src/mdx/recma.mjs"; import { recmaPlugins } from "./src/mdx/recma.mjs";
import { rehypePlugins } from "./src/mdx/rehype.mjs"; import { rehypePlugins } from "./src/mdx/rehype.mjs";
import { remarkPlugins } from "./src/mdx/remark.mjs"; import { remarkPlugins } from "./src/mdx/remark.mjs";
@ -20,4 +21,6 @@ const nextConfig = {
}, },
}; };
export default withSearch(withMDX(nextConfig)); export default million.next(
withSearch(withMDX(nextConfig)), { auto: { rsc: true } }
);

View File

@ -50,6 +50,7 @@
"acorn": "^8.11.2", "acorn": "^8.11.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"cmdk": "^0.2.0",
"date-fns": "^3.0.5", "date-fns": "^3.0.5",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"drizzle-orm": "^0.29.1", "drizzle-orm": "^0.29.1",
@ -61,6 +62,7 @@
"lucide-react": "^0.294.0", "lucide-react": "^0.294.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4", "mdx-annotations": "^0.1.4",
"million": "latest",
"mitata": "^0.1.6", "mitata": "^0.1.6",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"next": "14.0.3", "next": "14.0.3",

View File

@ -33,7 +33,7 @@ export default async function Page({
endpoint={process.env.MODAL_BUILDER_URL!} endpoint={process.env.MODAL_BUILDER_URL!}
/> />
)} )}
{machine.build_log && ( {machine.status !== "building" && machine.build_log && (
<LogsViewer logs={JSON.parse(machine.build_log)} /> <LogsViewer logs={JSON.parse(machine.build_log)} />
)} )}
</CardContent> </CardContent>

View File

@ -7,6 +7,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TableCell, TableRow } from "@/components/ui/table"; import { TableCell, 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";
@ -72,13 +73,13 @@ export function DeploymentDisplay({
<DialogTrigger asChild className="appearance-none hover:cursor-pointer"> <DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow> <TableRow>
<TableCell className="capitalize">{deployment.environment}</TableCell> <TableCell className="capitalize">{deployment.environment}</TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium truncate">
{deployment.version?.version} {deployment.version?.version}
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium truncate">
{deployment.machine?.name} {deployment.machine?.name}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)} {getRelativeTime(deployment.updated_at)}
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -90,6 +91,7 @@ export function DeploymentDisplay({
</DialogTitle> </DialogTitle>
<DialogDescription>Code for your deployment client</DialogDescription> <DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="max-h-[600px]">
<Tabs defaultValue="js" className="w-full"> <Tabs defaultValue="js" className="w-full">
<TabsList className="grid w-fit grid-cols-2"> <TabsList className="grid w-fit grid-cols-2">
<TabsTrigger value="js">js</TabsTrigger> <TabsTrigger value="js">js</TabsTrigger>
@ -118,6 +120,7 @@ export function DeploymentDisplay({
/> />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</ScrollArea>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -43,6 +43,7 @@ export function InsertModal<
<DialogTitle>{props.title}</DialogTitle> <DialogTitle>{props.title}</DialogTitle>
<DialogDescription>{props.description}</DialogDescription> <DialogDescription>{props.description}</DialogDescription>
</DialogHeader> </DialogHeader>
{/* <ScrollArea> */}
<AutoForm <AutoForm
fieldConfig={props.fieldConfig} fieldConfig={props.fieldConfig}
formSchema={props.formSchema} formSchema={props.formSchema}
@ -60,6 +61,7 @@ export function InsertModal<
</AutoFormSubmit> </AutoFormSubmit>
</div> </div>
</AutoForm> </AutoForm>
{/* </ScrollArea> */}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@ -14,12 +14,13 @@ export function MachineBuildLog({
endpoint: string; endpoint: string;
}) { }) {
const [logs, setLogs] = useState<LogsType>([]); const [logs, setLogs] = useState<LogsType>([]);
const [finished, setFinished] = useState(false);
const wsEndpoint = endpoint.replace(/^http/, "ws"); const wsEndpoint = endpoint.replace(/^http/, "ws");
const { lastMessage, readyState } = useWebSocket( const { lastMessage, readyState } = useWebSocket(
`${wsEndpoint}/ws/${machine_id}`, `${wsEndpoint}/ws/${machine_id}`,
{ {
shouldReconnect: () => true, shouldReconnect: () => !finished,
reconnectAttempts: 20, reconnectAttempts: 20,
reconnectInterval: 1000, reconnectInterval: 1000,
} }
@ -36,6 +37,8 @@ export function MachineBuildLog({
if (message?.event === "LOGS") { if (message?.event === "LOGS") {
setLogs((logs) => [...(logs ?? []), message.data]); setLogs((logs) => [...(logs ?? []), message.data]);
} else if (message?.event === "FINISHED") {
setFinished(true);
} }
}, [lastMessage]); }, [lastMessage]);

View File

@ -33,6 +33,7 @@ import {
deleteMachine, deleteMachine,
disableMachine, disableMachine,
enableMachine, enableMachine,
updateCustomMachine,
updateMachine, updateMachine,
} from "@/server/curdMachine"; } from "@/server/curdMachine";
import type { import type {
@ -95,7 +96,7 @@ export const columns: ColumnDef<Machine>[] = [
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
// <a className="hover:underline" href={`/${row.original.id}`}> // <a className="hover:underline" href={`/${row.original.id}`}>
<div className="flex flex-row gap-2 items-center"> <div className="flex flex-row gap-2 items-center truncate">
<a href={`/machines/${row.original.id}`} className="hover:underline"> <a href={`/machines/${row.original.id}`} className="hover:underline">
{row.getValue("name")} {row.getValue("name")}
</a> </a>
@ -115,7 +116,9 @@ export const columns: ColumnDef<Machine>[] = [
header: () => <div className="text-left">Endpoint</div>, header: () => <div className="text-left">Endpoint</div>,
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<div className="text-left font-medium">{row.original.endpoint}</div> <div className="text-left font-medium truncate max-w-[400px]">
{row.original.endpoint}
</div>
); );
}, },
}, },
@ -123,7 +126,11 @@ export const columns: ColumnDef<Machine>[] = [
accessorKey: "type", accessorKey: "type",
header: () => <div className="text-left">Type</div>, header: () => <div className="text-left">Type</div>,
cell: ({ row }) => { cell: ({ row }) => {
return <div className="text-left font-medium">{row.original.type}</div>; return (
<div className="text-left font-medium truncate">
{row.original.type}
</div>
);
}, },
}, },
{ {
@ -133,7 +140,7 @@ export const columns: ColumnDef<Machine>[] = [
header: ({ column }) => { header: ({ column }) => {
return ( return (
<button <button
className="w-full flex items-center justify-end hover:underline" className="w-full flex items-center justify-end hover:underline truncate"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
> >
Update Date Update Date
@ -195,6 +202,33 @@ export const columns: ColumnDef<Machine>[] = [
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
{machine.type === "comfy-deploy-serverless" ? (
<UpdateModal
data={machine}
open={open}
setOpen={setOpen}
title="Edit"
description="Edit machines"
serverAction={updateCustomMachine}
formSchema={addCustomMachineSchema}
fieldConfig={{
type: {
fieldType: "fallback",
inputProps: {
disabled: true,
showLabel: false,
type: "hidden",
},
},
snapshot: {
fieldType: "snapshot",
},
models: {
fieldType: "models",
},
}}
/>
) : (
<UpdateModal <UpdateModal
data={machine} data={machine}
open={open} open={open}
@ -211,6 +245,7 @@ export const columns: ColumnDef<Machine>[] = [
}, },
}} }}
/> />
)}
</DropdownMenu> </DropdownMenu>
); );
}, },
@ -273,8 +308,16 @@ export function MachineList({ data }: { data: Machine[] }) {
fieldType: "fallback", fieldType: "fallback",
inputProps: { inputProps: {
disabled: true, disabled: true,
showLabel: false,
type: "hidden",
}, },
}, },
snapshot: {
fieldType: "snapshot",
},
models: {
fieldType: "models",
},
}} }}
/> />
</div> </div>

View File

@ -191,9 +191,11 @@ export function RunWorkflowButton({
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-xl"> <DialogContent className="max-w-xl">
<DialogHeader> <DialogHeader>
<DialogTitle>Run inputs</DialogTitle> <DialogTitle>Confirm run</DialogTitle>
<DialogDescription> <DialogDescription>
Run your workflow with custom inputs {schema
? "Run your workflow with custom inputs"
: "Confirm to run your workflow"}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{/* <div className="max-h-96 overflow-y-scroll"> */} {/* <div className="max-h-96 overflow-y-scroll"> */}
@ -203,6 +205,7 @@ export function RunWorkflowButton({
values={values} values={values}
onValuesChange={setValues} onValuesChange={setValues}
onSubmit={runWorkflow} onSubmit={runWorkflow}
className="px-1"
> >
<div className="flex justify-end"> <div className="flex justify-end">
<AutoFormSubmit> <AutoFormSubmit>

View File

@ -0,0 +1,157 @@
"use client";
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { useRef } from "react";
import { z } from "zod";
const Model = z.object({
name: z.string(),
type: z.string(),
base: z.string(),
save_path: z.string(),
description: z.string(),
reference: z.string(),
filename: z.string(),
url: z.string(),
});
const ModelList = z.array(Model);
export const ModelListWrapper = z.object({
models: ModelList,
});
export function ModelPickerView({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
const value = (field.value as z.infer<typeof ModelList>) ?? [];
const [open, setOpen] = React.useState(false);
const [modelList, setModelList] =
React.useState<z.infer<typeof ModelListWrapper>>();
// const [selectedModels, setSelectedModels] = React.useState<
// z.infer<typeof ModelList>
// >(field.value ?? []);
React.useEffect(() => {
const controller = new AbortController();
fetch(
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
{
signal: controller.signal,
}
)
.then((x) => x.json())
.then((a) => {
setModelList(ModelListWrapper.parse(a));
});
return () => {
controller.abort();
};
}, []);
function toggleModel(model: z.infer<typeof Model>) {
const prevSelectedModels = value;
if (
prevSelectedModels.some(
(selectedModel) =>
selectedModel.url + selectedModel.name === model.url + model.name
)
) {
field.onChange(
prevSelectedModels.filter(
(selectedModel) =>
selectedModel.url + selectedModel.name !== model.url + model.name
)
);
} else {
field.onChange([...prevSelectedModels, model]);
}
}
// React.useEffect(() => {
// field.onChange(selectedModels);
// }, [selectedModels]);
const containerRef = useRef<HTMLDivElement>(null);
return (
<div className="" ref={containerRef}>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between flex"
>
Select models... ({value.length} selected)
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[375px] p-0" side="top">
<Command>
<CommandInput placeholder="Search framework..." className="h-9" />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandList className="pointer-events-auto">
<CommandGroup>
{modelList?.models.map((model) => (
<CommandItem
key={model.url + model.name}
value={model.url}
onSelect={() => {
toggleModel(model);
// Update field.onChange to pass the array of selected models
}}
>
{model.name}
<Check
className={cn(
"ml-auto h-4 w-4",
value.some(
(selectedModel) => selectedModel.url === model.url
)
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{field.value && (
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[360px] rounded-lg mt-2">
<div className="max-h-[200px]">
<pre className="p-2 rounded-md text-xs ">
{JSON.stringify(field.value, null, 2)}
</pre>
</div>
</ScrollArea>
)}
</div>
);
}

View File

@ -0,0 +1,125 @@
"use client";
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import { findAllDeployments } from "@/server/curdDeploments";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
export function SnapshotPickerView({
field,
}: Pick<AutoFormInputComponentProps, "field">) {
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<string | null>(null);
const [frameworks, setFramework] = React.useState<
{
id: string;
label: string;
value: string;
}[]
>();
React.useEffect(() => {
findAllDeployments().then((a) => {
console.log(a);
const frameworks = a
.map((item) => {
if (
item.deployments.length == 0 ||
item.deployments[0].version.snapshot == null
)
return null;
return {
id: item.deployments[0].version.id,
label: `${item.name} - ${item.deployments[0].environment}`,
value: JSON.stringify(item.deployments[0].version.snapshot),
};
})
.filter((item): item is NonNullable<typeof item> => item != null);
setFramework(frameworks);
});
}, []);
function findItem(value: string) {
// console.log(frameworks);
return frameworks?.find((item) => item.id === value);
}
return (
<div className="">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between flex"
>
{selected ? findItem(selected)?.label : "Select snapshot..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[375px] p-0">
<Command>
<CommandInput placeholder="Search framework..." className="h-9" />
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks?.map((framework) => (
<CommandItem
key={framework.id}
value={framework.id}
onSelect={(currentValue) => {
setSelected(currentValue);
const json =
frameworks?.find((item) => item.id === currentValue)
?.value ?? null;
field.onChange(json ? JSON.parse(json) : null);
setOpen(false);
}}
>
{framework.label}
<Check
className={cn(
"ml-auto h-4 w-4",
field.value === framework.value
? "opacity-100"
: "opacity-0"
)}
/>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
{field.value && (
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[360px] rounded-lg mt-2">
<div className="max-h-[200px]">
<pre className="p-2 rounded-md text-xs ">
{JSON.stringify(field.value, null, 2)}
</pre>
</div>
</ScrollArea>
)}
</div>
);
}

View File

@ -0,0 +1,39 @@
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { LoadingIcon } from "@/components/LoadingIcon";
import { ModelPickerView } from "@/components/custom-form/ModelPickerView";
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { Suspense } from "react";
export default function AutoFormModelsPicker({
label,
isRequired,
field,
fieldConfigItem,
zodItem,
}: AutoFormInputComponentProps) {
return (
<FormItem>
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
<FormControl>
<Suspense fallback={<LoadingIcon />}>
<ModelPickerView field={field} />
</Suspense>
</FormControl>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}

View File

@ -0,0 +1,39 @@
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import {
FormControl,
FormDescription,
FormItem,
FormLabel,
FormMessage,
} from "../ui/form";
import { SnapshotPickerView } from "./SnapshotPickerView";
import { LoadingIcon } from "@/components/LoadingIcon";
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { Suspense } from "react";
export default function AutoFormSnapshotPicker({
label,
isRequired,
field,
fieldConfigItem,
zodItem,
}: AutoFormInputComponentProps) {
return (
<FormItem>
<FormLabel>
{label}
{isRequired && <span className="text-destructive"> *</span>}
</FormLabel>
<FormControl>
<Suspense fallback={<LoadingIcon />}>
<SnapshotPickerView field={field} />
</Suspense>
</FormControl>
{fieldConfigItem.description && (
<FormDescription>{fieldConfigItem.description}</FormDescription>
)}
<FormMessage />
</FormItem>
);
}

View File

@ -6,6 +6,8 @@ import AutoFormNumber from "./fields/number";
import AutoFormRadioGroup from "./fields/radio-group"; import AutoFormRadioGroup from "./fields/radio-group";
import AutoFormSwitch from "./fields/switch"; import AutoFormSwitch from "./fields/switch";
import AutoFormTextarea from "./fields/textarea"; import AutoFormTextarea from "./fields/textarea";
import AutoFormModelsPicker from "@/components/custom-form/model-picker";
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
export const INPUT_COMPONENTS = { export const INPUT_COMPONENTS = {
checkbox: AutoFormCheckbox, checkbox: AutoFormCheckbox,
@ -16,6 +18,10 @@ export const INPUT_COMPONENTS = {
textarea: AutoFormTextarea, textarea: AutoFormTextarea,
number: AutoFormNumber, number: AutoFormNumber,
fallback: AutoFormInput, fallback: AutoFormInput,
// Customs
snapshot: AutoFormSnapshotPicker,
models: AutoFormModelsPicker,
}; };
/** /**

View File

@ -6,7 +6,7 @@ import {
FormMessage, FormMessage,
} from "../../form"; } from "../../form";
import { Input } from "../../input"; import { Input } from "../../input";
import { AutoFormInputComponentProps } from "../types"; import type { AutoFormInputComponentProps } from "../types";
export default function AutoFormInput({ export default function AutoFormInput({
label, label,

View File

@ -6,6 +6,7 @@ import type { FieldConfig } from "./types";
import type { ZodObjectOrWrapped } from "./utils"; import type { ZodObjectOrWrapped } from "./utils";
import { getDefaultValues, getObjectFormSchema } from "./utils"; import { getDefaultValues, getObjectFormSchema } from "./utils";
import AutoFormObject from "@/components/ui/auto-form/fields/object"; import AutoFormObject from "@/components/ui/auto-form/fields/object";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import type { DefaultValues } from "react-hook-form"; import type { DefaultValues } from "react-hook-form";
@ -68,11 +69,15 @@ function AutoForm<SchemaType extends ZodObjectOrWrapped>({
}} }}
className={cn("space-y-5", className)} className={cn("space-y-5", className)}
> >
<ScrollArea>
<div className="max-h-[400px] px-1 py-1 w-full">
<AutoFormObject <AutoFormObject
schema={objectFormSchema} schema={objectFormSchema}
form={form} form={form}
fieldConfig={fieldConfig} fieldConfig={fieldConfig}
/> />
</div>
</ScrollArea>
{children} {children}
</form> </form>

View File

@ -1,6 +1,6 @@
import { ControllerRenderProps, FieldValues } from "react-hook-form"; import type { INPUT_COMPONENTS } from "./config";
import * as z from "zod"; import type { ControllerRenderProps, FieldValues } from "react-hook-form";
import { INPUT_COMPONENTS } from "./config"; import type * as z from "zod";
export type FieldConfigItem = { export type FieldConfigItem = {
description?: React.ReactNode; description?: React.ReactNode;

View File

@ -0,0 +1,154 @@
"use client";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { cn } from "@/lib/utils";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -1,6 +1,5 @@
import * as React from "react" import { cn } from "@/lib/utils";
import * as React from "react";
import { cn } from "@/lib/utils"
export interface InputProps export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {} extends React.InputHTMLAttributes<HTMLInputElement> {}
@ -17,9 +16,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref} ref={ref}
{...props} {...props}
/> />
) );
} }
) );
Input.displayName = "Input" Input.displayName = "Input";
export { Input } export { Input };

View File

@ -1,13 +1,12 @@
"use client" "use client";
import * as React from "react" import { cn } from "@/lib/utils";
import * as PopoverPrimitive from "@radix-ui/react-popover" import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/lib/utils" const Popover = PopoverPrimitive.Root;
const Popover = PopoverPrimitive.Root const PopoverTrigger = PopoverPrimitive.Trigger;
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef< const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>, React.ElementRef<typeof PopoverPrimitive.Content>,
@ -22,10 +21,24 @@ const PopoverContent = React.forwardRef<
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className
)} )}
// https://github.com/shadcn-ui/ui/pull/2123/files#diff-e43c79299129c57a9055c3d6a20ff7bbeea4035dd6aa80eebe381b29f82f90a8
onWheel={(e) => {
e.stopPropagation();
const isScrollingDown = e.deltaY > 0;
if (isScrollingDown) {
e.currentTarget.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowDown" })
);
} else {
e.currentTarget.dispatchEvent(
new KeyboardEvent("keydown", { key: "ArrowUp" })
);
}
}}
{...props} {...props}
/> />
</PopoverPrimitive.Portal> </PopoverPrimitive.Portal>
)) ));
PopoverContent.displayName = PopoverPrimitive.Content.displayName PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverTrigger, PopoverContent } export { Popover, PopoverTrigger, PopoverContent };

View File

@ -37,6 +37,7 @@ export const workflowTable = dbSchema.table("workflows", {
export const workflowRelations = relations(workflowTable, ({ many }) => ({ export const workflowRelations = relations(workflowTable, ({ many }) => ({
versions: many(workflowVersionTable), versions: many(workflowVersionTable),
deployments: many(deploymentsTable),
})); }));
export const workflowType = z.any(); export const workflowType = z.any();
@ -203,6 +204,7 @@ export const machinesTable = dbSchema.table("machines", {
type: machinesType("type").notNull().default("classic"), type: machinesType("type").notNull().default("classic"),
status: machinesStatus("status").notNull().default("ready"), status: machinesStatus("status").notNull().default("ready"),
snapshot: jsonb("snapshot").$type<any>(), snapshot: jsonb("snapshot").$type<any>(),
models: jsonb("models").$type<any>(),
build_log: text("build_log"), build_log: text("build_log"),
}); });
@ -255,6 +257,10 @@ export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({
fields: [deploymentsTable.workflow_version_id], fields: [deploymentsTable.workflow_version_id],
references: [workflowVersionTable.id], references: [workflowVersionTable.id],
}), }),
workflow: one(workflowTable, {
fields: [deploymentsTable.workflow_id],
references: [workflowTable.id],
}),
})); }));
export const apiKeyTable = dbSchema.table("api_keys", { export const apiKeyTable = dbSchema.table("api_keys", {

View File

@ -17,4 +17,5 @@ export const addCustomMachineSchema = insertCustomMachineSchema.pick({
name: true, name: true,
type: true, type: true,
snapshot: true, snapshot: true,
models: true,
}); });

View File

@ -1,9 +1,9 @@
"use server"; "use server";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { deploymentsTable } from "@/db/schema"; import { deploymentsTable, workflowTable } from "@/db/schema";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { and, eq } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import "server-only"; import "server-only";
@ -47,3 +47,36 @@ export async function createDeployments(
message: `Successfully created deployment for ${environment}`, message: `Successfully created deployment for ${environment}`,
}; };
} }
export async function findAllDeployments() {
const { userId, orgId } = auth();
if (!userId) throw new Error("No user id");
const deployments = await db.query.workflowTable.findMany({
where: and(
orgId
? eq(workflowTable.org_id, orgId)
: and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id))
),
columns: {
name: true,
},
with: {
deployments: {
columns: {
environment: true,
},
with: {
version: {
columns: {
id: true,
snapshot: true,
},
},
},
},
},
});
return deployments;
}

View File

@ -6,6 +6,7 @@ import type {
} from "./addMachineSchema"; } from "./addMachineSchema";
import { withServerPromise } from "./withServerPromise"; import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db"; import { db } from "@/db/db";
import type { MachineType } from "@/db/schema";
import { machinesTable } from "@/db/schema"; import { machinesTable } from "@/db/schema";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
@ -25,7 +26,11 @@ export async function getMachines() {
and( and(
orgId orgId
? eq(machinesTable.org_id, orgId) ? eq(machinesTable.org_id, orgId)
: eq(machinesTable.user_id, userId), : // make sure org_id is null
and(
eq(machinesTable.user_id, userId),
isNull(machinesTable.org_id)
),
eq(machinesTable.disabled, false) eq(machinesTable.disabled, false)
) )
); );
@ -67,10 +72,59 @@ export const addMachine = withServerPromise(
} }
); );
export const updateCustomMachine = withServerPromise(
async ({
id,
...data
}: z.infer<typeof addCustomMachineSchema> & {
id: string;
}) => {
const { userId } = auth();
if (!userId) return { error: "No user id" };
const currentMachine = await db.query.machinesTable.findFirst({
where: eq(machinesTable.id, id),
});
if (!currentMachine) return { error: "Machine not found" };
// Check if snapshot or models have changed
const snapshotChanged =
JSON.stringify(data.snapshot) !== JSON.stringify(currentMachine.snapshot);
const modelsChanged =
JSON.stringify(data.models) !== JSON.stringify(currentMachine.models);
// return {
// message: `snapshotChanged: ${snapshotChanged}, modelsChanged: ${modelsChanged}`,
// };
await db.update(machinesTable).set(data).where(eq(machinesTable.id, id));
// If there are changes
if (snapshotChanged || modelsChanged) {
// Update status to building
await db
.update(machinesTable)
.set({
status: "building",
endpoint: "not-ready",
})
.where(eq(machinesTable.id, id));
// Perform custom build if there are changes
await buildMachine(data, currentMachine);
redirect(`/machines/${id}`);
} else {
revalidatePath("/machines");
}
return { message: "Machine Updated" };
}
);
export const addCustomMachine = withServerPromise( export const addCustomMachine = withServerPromise(
async (data: z.infer<typeof addCustomMachineSchema>) => { async (data: z.infer<typeof addCustomMachineSchema>) => {
const { userId, orgId } = auth(); const { userId, orgId } = auth();
const headersList = headers();
if (!userId) return { error: "No user id" }; if (!userId) return { error: "No user id" };
@ -88,12 +142,21 @@ export const addCustomMachine = withServerPromise(
const b = a[0]; const b = a[0];
// const origin = new URL(request.url).origin; await buildMachine(data, b);
redirect(`/machines/${b.id}`);
// revalidatePath("/machines");
return { message: "Machine Building" };
}
);
async function buildMachine(
data: z.infer<typeof addCustomMachineSchema>,
b: MachineType
) {
const headersList = headers();
const domain = headersList.get("x-forwarded-host") || ""; const domain = headersList.get("x-forwarded-host") || "";
const protocol = headersList.get("x-forwarded-proto") || ""; const protocol = headersList.get("x-forwarded-proto") || "";
// console.log("domain", domain);
// console.log("domain", `${protocol}://${domain}/api/machine-built`);
// return { message: "Machine Building" };
if (domain === "") { if (domain === "") {
throw new Error("No domain"); throw new Error("No domain");
@ -108,8 +171,10 @@ export const addCustomMachine = withServerPromise(
body: JSON.stringify({ body: JSON.stringify({
machine_id: b.id, machine_id: b.id,
name: b.id, name: b.id,
snapshot: JSON.parse(data.snapshot as string), snapshot: data.snapshot, //JSON.parse( as string),
callback_url: `${protocol}://${domain}/api/machine-built`, callback_url: `${protocol}://${domain}/api/machine-built`,
models: data.models, //JSON.parse(data.models as string),
gpu: "T4",
}), }),
}); });
@ -125,13 +190,7 @@ export const addCustomMachine = withServerPromise(
.where(eq(machinesTable.id, b.id)); .where(eq(machinesTable.id, b.id));
throw new Error(`Error: ${result.statusText} ${error_log}`); throw new Error(`Error: ${result.statusText} ${error_log}`);
} }
redirect(`/machines/${b.id}`);
// revalidatePath("/machines");
return { message: "Machine Building" };
} }
);
export const updateMachine = withServerPromise( export const updateMachine = withServerPromise(
async ({ async ({