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 pydantic import BaseModel
from typing import Union, Optional, Dict, List
from pydantic import BaseModel, Field, field_validator
from fastapi import FastAPI, HTTPException, WebSocket, BackgroundTasks, WebSocketDisconnect
from fastapi.responses import JSONResponse
from fastapi.logger import logger as fastapi_logger
import os
from enum import Enum
import json
import subprocess
import time
@ -104,17 +105,49 @@ class Snapshot(BaseModel):
comfyui: str
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):
machine_id: str
name: str
snapshot: Snapshot
models: List[Model]
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}")
async def websocket_endpoint(websocket: WebSocket, machine_id: str):
await websocket.accept()
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:
while True:
data = await websocket.receive_text()
@ -156,6 +189,9 @@ async def create_item(item: Item):
return JSONResponse(status_code=200, content={"message": "Build Queued"})
# Initialize the logs cache
machine_logs_cache = {}
async def build_logic(item: Item):
# Deploy to modal
folder_path = f"/app/builds/{item.machine_id}"
@ -175,7 +211,8 @@ async def build_logic(item: Item):
# Write the config file
config = {
"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:
f.write("config = " + json.dumps(config))
@ -183,79 +220,99 @@ async def build_logic(item: Item):
with open(f"{folder_path}/data/snapshot.json", "w") as f:
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)
# 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(
f"modal deploy app.py",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=folder_path
cwd=folder_path,
# env={**os.environ, "PYTHONUNBUFFERED": "1"}
)
url = None
# Initialize the logs cache
machine_logs_cache = []
# Stream the output
# Read output
while True:
line = await process.stdout.readline()
error = await process.stderr.readline()
if not line and not error:
break
l = line.decode('utf-8').strip()
e = error.decode('utf-8').strip()
if item.machine_id not in machine_logs_cache:
machine_logs_cache[item.machine_id] = []
if l != "":
logger.info(l)
machine_logs_cache.append({
"logs": l,
"timestamp": time.time()
})
machine_logs = machine_logs_cache[item.machine_id]
if item.machine_id in machine_id_websocket_dict:
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "LOGS", "data": {
"machine_id": item.machine_id,
"logs": l,
"timestamp": time.time()
}}))
async def read_stream(stream, isStderr):
while True:
line = await stream.readline()
if line:
l = line.decode('utf-8').strip()
if l == "":
continue
if "Created comfyui_app =>" in l or (l.startswith("https://") and l.endswith(".modal.run")):
if "Created comfyui_app =>" in l:
url = l.split("=>")[1].strip()
else:
# Some case it only prints the url on a blank line
url = l
if url:
machine_logs_cache.append({
"logs": f"App image built, url: {url}",
if not isStderr:
logger.info(l)
machine_logs.append({
"logs": l,
"timestamp": time.time()
})
if item.machine_id in machine_id_websocket_dict:
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "LOGS", "data": {
"machine_id": item.machine_id,
"logs": f"App image built, url: {url}",
"logs": l,
"timestamp": time.time()
}}))
if e != "":
logger.info(e)
machine_logs_cache.append({
"logs": e,
"timestamp": time.time()
})
if item.machine_id in machine_id_websocket_dict:
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "LOGS", "data": {
"machine_id": item.machine_id,
"logs": e,
"timestamp": time.time()
}}))
if "Created comfyui_app =>" in l or (l.startswith("https://") and l.endswith(".modal.run")):
if "Created comfyui_app =>" in l:
url = l.split("=>")[1].strip()
else:
# Some case it only prints the url on a blank line
url = l
if url:
machine_logs.append({
"logs": f"App image built, url: {url}",
"timestamp": time.time()
})
if item.machine_id in machine_id_websocket_dict:
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "LOGS", "data": {
"machine_id": item.machine_id,
"logs": f"App image built, url: {url}",
"timestamp": time.time()
}}))
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "FINISHED", "data": {
"status": "succuss",
}}))
else:
# is error
logger.error(l)
machine_logs.append({
"logs": e,
"timestamp": time.time()
})
if item.machine_id in machine_id_websocket_dict:
await machine_id_websocket_dict[item.machine_id].send_text(json.dumps({"event": "LOGS", "data": {
"machine_id": item.machine_id,
"logs": e,
"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
await process.wait()
@ -273,29 +330,39 @@ async def build_logic(item: Item):
if process.returncode != 0:
logger.info("An error occurred.")
# 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.",
"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 JSONResponse(status_code=400, content={"error": "Unable to build the app image."})
# app_suffix = "comfyui-app"
if url is None:
machine_logs_cache.append({
machine_logs.append({
"logs": "App image built, but url is None, unable to parse the url.",
"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 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/
# 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(url)

View File

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

View File

@ -105,7 +105,7 @@ image = Image.debian_slim()
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):
import subprocess
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
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"}
# Load JSON array from deps.json
@ -39,12 +40,15 @@ with open('deps.json') as f:
# Make a POST request for each package
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)
# restore_snapshot_url = "http://127.0.0.1:8188/snapshot/restore?target=snapshot"
# response = requests.request("GET", restore_snapshot_url, headers=headers)
# print(response.text)
with open('models.json') as f:
models = json.load(f)
for model in models:
response = requests.request("POST", f"{root_url}/model/install", json=model, headers=headers)
print(response.text)
# Close the server
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,
"tag": "0022_petite_bishop",
"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 { rehypePlugins } from "./src/mdx/rehype.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",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"date-fns": "^3.0.5",
"dayjs": "^1.11.10",
"drizzle-orm": "^0.29.1",
@ -61,6 +62,7 @@
"lucide-react": "^0.294.0",
"mdast-util-to-string": "^4.0.0",
"mdx-annotations": "^0.1.4",
"million": "latest",
"mitata": "^0.1.6",
"nanoid": "^5.0.4",
"next": "14.0.3",

View File

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

View File

@ -7,6 +7,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { TableCell, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
@ -72,13 +73,13 @@ export function DeploymentDisplay({
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow>
<TableCell className="capitalize">{deployment.environment}</TableCell>
<TableCell className="font-medium">
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium">
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right">
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
@ -90,34 +91,36 @@ export function DeploymentDisplay({
</DialogTitle>
<DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader>
<Tabs defaultValue="js" className="w-full">
<TabsList className="grid w-fit grid-cols-2">
<TabsTrigger value="js">js</TabsTrigger>
<TabsTrigger value="curl">curl</TabsTrigger>
</TabsList>
<TabsContent className="flex flex-col gap-2" value="js">
Trigger the workflow
<CodeBlock
lang="js"
code={formatCode(jsTemplate, deployment, domain, workflowInput)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2" value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment, domain)}
/>
<CodeBlock
lang="bash"
code={formatCode(curlTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
</Tabs>
<ScrollArea className="max-h-[600px]">
<Tabs defaultValue="js" className="w-full">
<TabsList className="grid w-fit grid-cols-2">
<TabsTrigger value="js">js</TabsTrigger>
<TabsTrigger value="curl">curl</TabsTrigger>
</TabsList>
<TabsContent className="flex flex-col gap-2" value="js">
Trigger the workflow
<CodeBlock
lang="js"
code={formatCode(jsTemplate, deployment, domain, workflowInput)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2" value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment, domain)}
/>
<CodeBlock
lang="bash"
code={formatCode(curlTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
</Tabs>
</ScrollArea>
</DialogContent>
</Dialog>
);

View File

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

View File

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

View File

@ -33,6 +33,7 @@ import {
deleteMachine,
disableMachine,
enableMachine,
updateCustomMachine,
updateMachine,
} from "@/server/curdMachine";
import type {
@ -95,7 +96,7 @@ export const columns: ColumnDef<Machine>[] = [
cell: ({ row }) => {
return (
// <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">
{row.getValue("name")}
</a>
@ -115,7 +116,9 @@ export const columns: ColumnDef<Machine>[] = [
header: () => <div className="text-left">Endpoint</div>,
cell: ({ row }) => {
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",
header: () => <div className="text-left">Type</div>,
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 }) => {
return (
<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")}
>
Update Date
@ -195,22 +202,50 @@ export const columns: ColumnDef<Machine>[] = [
Edit
</DropdownMenuItem>
</DropdownMenuContent>
<UpdateModal
data={machine}
open={open}
setOpen={setOpen}
title="Edit"
description="Edit machines"
serverAction={updateMachine}
formSchema={addMachineSchema}
fieldConfig={{
auth_token: {
inputProps: {
type: "password",
{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
data={machine}
open={open}
setOpen={setOpen}
title="Edit"
description="Edit machines"
serverAction={updateMachine}
formSchema={addMachineSchema}
fieldConfig={{
auth_token: {
inputProps: {
type: "password",
},
},
}}
/>
)}
</DropdownMenu>
);
},
@ -273,8 +308,16 @@ export function MachineList({ data }: { data: Machine[] }) {
fieldType: "fallback",
inputProps: {
disabled: true,
showLabel: false,
type: "hidden",
},
},
snapshot: {
fieldType: "snapshot",
},
models: {
fieldType: "models",
},
}}
/>
</div>

View File

@ -191,9 +191,11 @@ export function RunWorkflowButton({
</DialogTrigger>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle>Run inputs</DialogTitle>
<DialogTitle>Confirm run</DialogTitle>
<DialogDescription>
Run your workflow with custom inputs
{schema
? "Run your workflow with custom inputs"
: "Confirm to run your workflow"}
</DialogDescription>
</DialogHeader>
{/* <div className="max-h-96 overflow-y-scroll"> */}
@ -203,6 +205,7 @@ export function RunWorkflowButton({
values={values}
onValuesChange={setValues}
onSubmit={runWorkflow}
className="px-1"
>
<div className="flex justify-end">
<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 AutoFormSwitch from "./fields/switch";
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 = {
checkbox: AutoFormCheckbox,
@ -16,6 +18,10 @@ export const INPUT_COMPONENTS = {
textarea: AutoFormTextarea,
number: AutoFormNumber,
fallback: AutoFormInput,
// Customs
snapshot: AutoFormSnapshotPicker,
models: AutoFormModelsPicker,
};
/**

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { ControllerRenderProps, FieldValues } from "react-hook-form";
import * as z from "zod";
import { INPUT_COMPONENTS } from "./config";
import type { INPUT_COMPONENTS } from "./config";
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
import type * as z from "zod";
export type FieldConfigItem = {
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 { cn } from "@/lib/utils";
import * as React from "react";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
@ -17,9 +16,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
ref={ref}
{...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 * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils";
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<
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",
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}
/>
</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 }) => ({
versions: many(workflowVersionTable),
deployments: many(deploymentsTable),
}));
export const workflowType = z.any();
@ -203,6 +204,7 @@ export const machinesTable = dbSchema.table("machines", {
type: machinesType("type").notNull().default("classic"),
status: machinesStatus("status").notNull().default("ready"),
snapshot: jsonb("snapshot").$type<any>(),
models: jsonb("models").$type<any>(),
build_log: text("build_log"),
});
@ -255,6 +257,10 @@ export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({
fields: [deploymentsTable.workflow_version_id],
references: [workflowVersionTable.id],
}),
workflow: one(workflowTable, {
fields: [deploymentsTable.workflow_id],
references: [workflowTable.id],
}),
}));
export const apiKeyTable = dbSchema.table("api_keys", {

View File

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

View File

@ -1,9 +1,9 @@
"use server";
import { db } from "@/db/db";
import { deploymentsTable } from "@/db/schema";
import { deploymentsTable, workflowTable } from "@/db/schema";
import { auth } from "@clerk/nextjs";
import { and, eq } from "drizzle-orm";
import { and, eq, isNull } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import "server-only";
@ -47,3 +47,36 @@ export async function createDeployments(
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";
import { withServerPromise } from "./withServerPromise";
import { db } from "@/db/db";
import type { MachineType } from "@/db/schema";
import { machinesTable } from "@/db/schema";
import { auth } from "@clerk/nextjs";
import { and, eq, isNull } from "drizzle-orm";
@ -25,7 +26,11 @@ export async function getMachines() {
and(
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)
)
);
@ -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(
async (data: z.infer<typeof addCustomMachineSchema>) => {
const { userId, orgId } = auth();
const headersList = headers();
if (!userId) return { error: "No user id" };
@ -88,51 +142,56 @@ export const addCustomMachine = withServerPromise(
const b = a[0];
// const origin = new URL(request.url).origin;
const domain = headersList.get("x-forwarded-host") || "";
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 === "") {
throw new Error("No domain");
}
// Call remote builder
const result = await fetch(`${process.env.MODAL_BUILDER_URL!}/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
machine_id: b.id,
name: b.id,
snapshot: JSON.parse(data.snapshot as string),
callback_url: `${protocol}://${domain}/api/machine-built`,
}),
});
if (!result.ok) {
const error_log = await result.text();
await db
.update(machinesTable)
.set({
...data,
status: "error",
build_log: error_log,
})
.where(eq(machinesTable.id, b.id));
throw new Error(`Error: ${result.statusText} ${error_log}`);
}
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 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!}/create`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
machine_id: b.id,
name: b.id,
snapshot: data.snapshot, //JSON.parse( as string),
callback_url: `${protocol}://${domain}/api/machine-built`,
models: data.models, //JSON.parse(data.models as string),
gpu: "T4",
}),
});
if (!result.ok) {
const error_log = await result.text();
await db
.update(machinesTable)
.set({
...data,
status: "error",
build_log: error_log,
})
.where(eq(machinesTable.id, b.id));
throw new Error(`Error: ${result.statusText} ${error_log}`);
}
}
export const updateMachine = withServerPromise(
async ({
id,