diff --git a/builder/modal-builder/src/main.py b/builder/modal-builder/src/main.py index 72874f6..f717d7c 100644 --- a/builder/modal-builder/src/main.py +++ b/builder/modal-builder/src/main.py @@ -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) diff --git a/builder/modal-builder/src/template/Dockerfile b/builder/modal-builder/src/template/Dockerfile index 3cd223c..ee4c9ec 100644 --- a/builder/modal-builder/src/template/Dockerfile +++ b/builder/modal-builder/src/template/Dockerfile @@ -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 diff --git a/builder/modal-builder/src/template/app.py b/builder/modal-builder/src/template/app.py index e8867d0..0e0fd04 100644 --- a/builder/modal-builder/src/template/app.py +++ b/builder/modal-builder/src/template/app.py @@ -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 diff --git a/builder/modal-builder/src/template/config.py b/builder/modal-builder/src/template/config.py index a4a97e9..e59020b 100644 --- a/builder/modal-builder/src/template/config.py +++ b/builder/modal-builder/src/template/config.py @@ -1 +1 @@ -config = {"name": "my-app", "deploy_test": "True"} \ No newline at end of file +config = {"name": "my-app", "deploy_test": "True", "gpu": "T4"} \ No newline at end of file diff --git a/builder/modal-builder/src/template/data/install_deps.py b/builder/modal-builder/src/template/data/install_deps.py index 01eeca2..568afe6 100644 --- a/builder/modal-builder/src/template/data/install_deps.py +++ b/builder/modal-builder/src/template/data/install_deps.py @@ -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() diff --git a/builder/modal-builder/src/template/data/models.json b/builder/modal-builder/src/template/data/models.json new file mode 100644 index 0000000..6913dac --- /dev/null +++ b/builder/modal-builder/src/template/data/models.json @@ -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" + } +] \ No newline at end of file diff --git a/web/bun.lockb b/web/bun.lockb index 83956cb..907bdf8 100755 Binary files a/web/bun.lockb and b/web/bun.lockb differ diff --git a/web/drizzle/0023_fair_ikaris.sql b/web/drizzle/0023_fair_ikaris.sql new file mode 100644 index 0000000..5c5ab91 --- /dev/null +++ b/web/drizzle/0023_fair_ikaris.sql @@ -0,0 +1 @@ +ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "models" jsonb; \ No newline at end of file diff --git a/web/drizzle/meta/0023_snapshot.json b/web/drizzle/meta/0023_snapshot.json new file mode 100644 index 0000000..2f1fd77 --- /dev/null +++ b/web/drizzle/meta/0023_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/web/drizzle/meta/_journal.json b/web/drizzle/meta/_journal.json index c0a4c41..6d7d96e 100644 --- a/web/drizzle/meta/_journal.json +++ b/web/drizzle/meta/_journal.json @@ -162,6 +162,13 @@ "when": 1704453649633, "tag": "0022_petite_bishop", "breakpoints": true + }, + { + "idx": 23, + "version": "5", + "when": 1704540132567, + "tag": "0023_fair_ikaris", + "breakpoints": true } ] } \ No newline at end of file diff --git a/web/next.config.mjs b/web/next.config.mjs index eb8a553..818745a 100644 --- a/web/next.config.mjs +++ b/web/next.config.mjs @@ -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 } } +); diff --git a/web/package.json b/web/package.json index 808e4cb..173a59b 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/app/(app)/machines/[machine_id]/page.tsx b/web/src/app/(app)/machines/[machine_id]/page.tsx index e2b97c3..eca3a75 100644 --- a/web/src/app/(app)/machines/[machine_id]/page.tsx +++ b/web/src/app/(app)/machines/[machine_id]/page.tsx @@ -33,7 +33,7 @@ export default async function Page({ endpoint={process.env.MODAL_BUILDER_URL!} /> )} - {machine.build_log && ( + {machine.status !== "building" && machine.build_log && ( )} diff --git a/web/src/components/DeploymentDisplay.tsx b/web/src/components/DeploymentDisplay.tsx index 023e590..721b490 100644 --- a/web/src/components/DeploymentDisplay.tsx +++ b/web/src/components/DeploymentDisplay.tsx @@ -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({ {deployment.environment} - + {deployment.version?.version} - + {deployment.machine?.name} - + {getRelativeTime(deployment.updated_at)} @@ -90,34 +91,36 @@ export function DeploymentDisplay({ Code for your deployment client - - - js - curl - - - Trigger the workflow - - Check the status of the run, and retrieve the outputs - - - - - - - + + + + js + curl + + + Trigger the workflow + + Check the status of the run, and retrieve the outputs + + + + + + + + ); diff --git a/web/src/components/InsertModal.tsx b/web/src/components/InsertModal.tsx index 3d0e2f0..64a5039 100644 --- a/web/src/components/InsertModal.tsx +++ b/web/src/components/InsertModal.tsx @@ -43,6 +43,7 @@ export function InsertModal< {props.title} {props.description} + {/* */} + {/* */} ); diff --git a/web/src/components/MachineBuildLog.tsx b/web/src/components/MachineBuildLog.tsx index 13b25be..d03ff7c 100644 --- a/web/src/components/MachineBuildLog.tsx +++ b/web/src/components/MachineBuildLog.tsx @@ -14,12 +14,13 @@ export function MachineBuildLog({ endpoint: string; }) { const [logs, setLogs] = useState([]); + 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]); diff --git a/web/src/components/MachineList.tsx b/web/src/components/MachineList.tsx index b1c3f5c..7f22958 100644 --- a/web/src/components/MachineList.tsx +++ b/web/src/components/MachineList.tsx @@ -33,6 +33,7 @@ import { deleteMachine, disableMachine, enableMachine, + updateCustomMachine, updateMachine, } from "@/server/curdMachine"; import type { @@ -95,7 +96,7 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { return ( // -
+
{row.getValue("name")} @@ -115,7 +116,9 @@ export const columns: ColumnDef[] = [ header: () =>
Endpoint
, cell: ({ row }) => { return ( -
{row.original.endpoint}
+
+ {row.original.endpoint} +
); }, }, @@ -123,7 +126,11 @@ export const columns: ColumnDef[] = [ accessorKey: "type", header: () =>
Type
, cell: ({ row }) => { - return
{row.original.type}
; + return ( +
+ {row.original.type} +
+ ); }, }, { @@ -133,7 +140,7 @@ export const columns: ColumnDef[] = [ header: ({ column }) => { return (
diff --git a/web/src/components/VersionSelect.tsx b/web/src/components/VersionSelect.tsx index 9da1669..3ba2dd9 100644 --- a/web/src/components/VersionSelect.tsx +++ b/web/src/components/VersionSelect.tsx @@ -191,9 +191,11 @@ export function RunWorkflowButton({ - Run inputs + Confirm run - Run your workflow with custom inputs + {schema + ? "Run your workflow with custom inputs" + : "Confirm to run your workflow"} {/*
*/} @@ -203,6 +205,7 @@ export function RunWorkflowButton({ values={values} onValuesChange={setValues} onSubmit={runWorkflow} + className="px-1" >
diff --git a/web/src/components/custom-form/ModelPickerView.tsx b/web/src/components/custom-form/ModelPickerView.tsx new file mode 100644 index 0000000..5c1abe8 --- /dev/null +++ b/web/src/components/custom-form/ModelPickerView.tsx @@ -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) { + const value = (field.value as z.infer) ?? []; + + const [open, setOpen] = React.useState(false); + + const [modelList, setModelList] = + React.useState>(); + + // const [selectedModels, setSelectedModels] = React.useState< + // z.infer + // >(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) { + 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(null); + + return ( +
+ + + + + + + + No framework found. + + + {modelList?.models.map((model) => ( + { + toggleModel(model); + // Update field.onChange to pass the array of selected models + }} + > + {model.name} + selectedModel.url === model.url + ) + ? "opacity-100" + : "opacity-0" + )} + /> + + ))} + + + + + + {field.value && ( + +
+
+              {JSON.stringify(field.value, null, 2)}
+            
+
+
+ )} +
+ ); +} diff --git a/web/src/components/custom-form/SnapshotPickerView.tsx b/web/src/components/custom-form/SnapshotPickerView.tsx new file mode 100644 index 0000000..675393c --- /dev/null +++ b/web/src/components/custom-form/SnapshotPickerView.tsx @@ -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) { + const [open, setOpen] = React.useState(false); + const [selected, setSelected] = React.useState(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 => item != null); + + setFramework(frameworks); + }); + }, []); + + function findItem(value: string) { + // console.log(frameworks); + + return frameworks?.find((item) => item.id === value); + } + + return ( +
+ + + + + + + + No framework found. + + {frameworks?.map((framework) => ( + { + setSelected(currentValue); + const json = + frameworks?.find((item) => item.id === currentValue) + ?.value ?? null; + field.onChange(json ? JSON.parse(json) : null); + setOpen(false); + }} + > + {framework.label} + + + ))} + + + + + {field.value && ( + +
+
+              {JSON.stringify(field.value, null, 2)}
+            
+
+
+ )} +
+ ); +} diff --git a/web/src/components/custom-form/model-picker.tsx b/web/src/components/custom-form/model-picker.tsx new file mode 100644 index 0000000..0d2d0fe --- /dev/null +++ b/web/src/components/custom-form/model-picker.tsx @@ -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 ( + + + {label} + {isRequired && *} + + + }> + + + + {fieldConfigItem.description && ( + {fieldConfigItem.description} + )} + + + ); +} diff --git a/web/src/components/custom-form/snapshot-picker.tsx b/web/src/components/custom-form/snapshot-picker.tsx new file mode 100644 index 0000000..e825173 --- /dev/null +++ b/web/src/components/custom-form/snapshot-picker.tsx @@ -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 ( + + + {label} + {isRequired && *} + + + }> + + + + {fieldConfigItem.description && ( + {fieldConfigItem.description} + )} + + + ); +} diff --git a/web/src/components/ui/auto-form/config.ts b/web/src/components/ui/auto-form/config.ts index b94b9b0..4fdb9ea 100644 --- a/web/src/components/ui/auto-form/config.ts +++ b/web/src/components/ui/auto-form/config.ts @@ -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, }; /** diff --git a/web/src/components/ui/auto-form/fields/input.tsx b/web/src/components/ui/auto-form/fields/input.tsx index 242556b..dbc7fee 100644 --- a/web/src/components/ui/auto-form/fields/input.tsx +++ b/web/src/components/ui/auto-form/fields/input.tsx @@ -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, diff --git a/web/src/components/ui/auto-form/index.tsx b/web/src/components/ui/auto-form/index.tsx index fa5d7e3..c2b7505 100644 --- a/web/src/components/ui/auto-form/index.tsx +++ b/web/src/components/ui/auto-form/index.tsx @@ -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({ }} className={cn("space-y-5", className)} > - + +
+ +
+
{children} diff --git a/web/src/components/ui/auto-form/types.ts b/web/src/components/ui/auto-form/types.ts index d0e2ab6..50d55bc 100644 --- a/web/src/components/ui/auto-form/types.ts +++ b/web/src/components/ui/auto-form/types.ts @@ -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; diff --git a/web/src/components/ui/command.tsx b/web/src/components/ui/command.tsx new file mode 100644 index 0000000..c2d1b36 --- /dev/null +++ b/web/src/components/ui/command.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + + + + {children} + + + + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx index 677d05f..78d3d19 100644 --- a/web/src/components/ui/input.tsx +++ b/web/src/components/ui/input.tsx @@ -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 {} @@ -17,9 +16,9 @@ const Input = React.forwardRef( ref={ref} {...props} /> - ) + ); } -) -Input.displayName = "Input" +); +Input.displayName = "Input"; -export { Input } +export { Input }; diff --git a/web/src/components/ui/popover.tsx b/web/src/components/ui/popover.tsx index a0ec48b..ef29e37 100644 --- a/web/src/components/ui/popover.tsx +++ b/web/src/components/ui/popover.tsx @@ -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, @@ -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} /> -)) -PopoverContent.displayName = PopoverPrimitive.Content.displayName +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent } +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/web/src/db/schema.ts b/web/src/db/schema.ts index 694b999..8b7b884 100644 --- a/web/src/db/schema.ts +++ b/web/src/db/schema.ts @@ -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(), + models: jsonb("models").$type(), 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", { diff --git a/web/src/server/addMachineSchema.ts b/web/src/server/addMachineSchema.ts index 9d36624..639def4 100644 --- a/web/src/server/addMachineSchema.ts +++ b/web/src/server/addMachineSchema.ts @@ -17,4 +17,5 @@ export const addCustomMachineSchema = insertCustomMachineSchema.pick({ name: true, type: true, snapshot: true, + models: true, }); diff --git a/web/src/server/curdDeploments.ts b/web/src/server/curdDeploments.ts index 74dc3f2..ff0e89c 100644 --- a/web/src/server/curdDeploments.ts +++ b/web/src/server/curdDeploments.ts @@ -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; +} diff --git a/web/src/server/curdMachine.ts b/web/src/server/curdMachine.ts index 48e886a..3dec0aa 100644 --- a/web/src/server/curdMachine.ts +++ b/web/src/server/curdMachine.ts @@ -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 & { + 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) => { 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, + 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,