feat: add millon js, add models picker dialog, update builder
This commit is contained in:
parent
3c4bce630e
commit
01a9c1a1d6
@ -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,33 +220,40 @@ 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 = []
|
||||
if item.machine_id not in machine_logs_cache:
|
||||
machine_logs_cache[item.machine_id] = []
|
||||
|
||||
# Stream the output
|
||||
# Read output
|
||||
machine_logs = machine_logs_cache[item.machine_id]
|
||||
|
||||
async def read_stream(stream, isStderr):
|
||||
while True:
|
||||
line = await process.stdout.readline()
|
||||
error = await process.stderr.readline()
|
||||
if not line and not error:
|
||||
break
|
||||
line = await stream.readline()
|
||||
if line:
|
||||
l = line.decode('utf-8').strip()
|
||||
e = error.decode('utf-8').strip()
|
||||
|
||||
if l != "":
|
||||
if l == "":
|
||||
continue
|
||||
|
||||
if not isStderr:
|
||||
logger.info(l)
|
||||
machine_logs_cache.append({
|
||||
machine_logs.append({
|
||||
"logs": l,
|
||||
"timestamp": time.time()
|
||||
})
|
||||
@ -230,7 +274,7 @@ async def build_logic(item: Item):
|
||||
url = l
|
||||
|
||||
if url:
|
||||
machine_logs_cache.append({
|
||||
machine_logs.append({
|
||||
"logs": f"App image built, url: {url}",
|
||||
"timestamp": time.time()
|
||||
})
|
||||
@ -241,10 +285,14 @@ async def build_logic(item: Item):
|
||||
"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",
|
||||
}}))
|
||||
|
||||
if e != "":
|
||||
logger.info(e)
|
||||
machine_logs_cache.append({
|
||||
else:
|
||||
# is error
|
||||
logger.error(l)
|
||||
machine_logs.append({
|
||||
"logs": e,
|
||||
"timestamp": time.time()
|
||||
})
|
||||
@ -255,7 +303,16 @@ async def build_logic(item: Item):
|
||||
"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,28 +330,38 @@ 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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1 +1 @@
|
||||
config = {"name": "my-app", "deploy_test": "True"}
|
||||
config = {"name": "my-app", "deploy_test": "True", "gpu": "T4"}
|
@ -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()
|
||||
|
12
builder/modal-builder/src/template/data/models.json
Normal file
12
builder/modal-builder/src/template/data/models.json
Normal 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"
|
||||
}
|
||||
]
|
BIN
web/bun.lockb
BIN
web/bun.lockb
Binary file not shown.
1
web/drizzle/0023_fair_ikaris.sql
Normal file
1
web/drizzle/0023_fair_ikaris.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE "comfyui_deploy"."machines" ADD COLUMN "models" jsonb;
|
716
web/drizzle/meta/0023_snapshot.json
Normal file
716
web/drizzle/meta/0023_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
@ -162,6 +162,13 @@
|
||||
"when": 1704453649633,
|
||||
"tag": "0022_petite_bishop",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 23,
|
||||
"version": "5",
|
||||
"when": 1704540132567,
|
||||
"tag": "0023_fair_ikaris",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
@ -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 } }
|
||||
);
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
|
@ -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,6 +91,7 @@ export function DeploymentDisplay({
|
||||
</DialogTitle>
|
||||
<DialogDescription>Code for your deployment client</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[600px]">
|
||||
<Tabs defaultValue="js" className="w-full">
|
||||
<TabsList className="grid w-fit grid-cols-2">
|
||||
<TabsTrigger value="js">js</TabsTrigger>
|
||||
@ -118,6 +120,7 @@ export function DeploymentDisplay({
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -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,6 +202,33 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
{machine.type === "comfy-deploy-serverless" ? (
|
||||
<UpdateModal
|
||||
data={machine}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title="Edit"
|
||||
description="Edit machines"
|
||||
serverAction={updateCustomMachine}
|
||||
formSchema={addCustomMachineSchema}
|
||||
fieldConfig={{
|
||||
type: {
|
||||
fieldType: "fallback",
|
||||
inputProps: {
|
||||
disabled: true,
|
||||
showLabel: false,
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
fieldType: "snapshot",
|
||||
},
|
||||
models: {
|
||||
fieldType: "models",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UpdateModal
|
||||
data={machine}
|
||||
open={open}
|
||||
@ -211,6 +245,7 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
|
@ -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>
|
||||
|
157
web/src/components/custom-form/ModelPickerView.tsx
Normal file
157
web/src/components/custom-form/ModelPickerView.tsx
Normal 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>
|
||||
);
|
||||
}
|
125
web/src/components/custom-form/SnapshotPickerView.tsx
Normal file
125
web/src/components/custom-form/SnapshotPickerView.tsx
Normal 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>
|
||||
);
|
||||
}
|
39
web/src/components/custom-form/model-picker.tsx
Normal file
39
web/src/components/custom-form/model-picker.tsx
Normal 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>
|
||||
);
|
||||
}
|
39
web/src/components/custom-form/snapshot-picker.tsx
Normal file
39
web/src/components/custom-form/snapshot-picker.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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,
|
||||
|
@ -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)}
|
||||
>
|
||||
<ScrollArea>
|
||||
<div className="max-h-[400px] px-1 py-1 w-full">
|
||||
<AutoFormObject
|
||||
schema={objectFormSchema}
|
||||
form={form}
|
||||
fieldConfig={fieldConfig}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{children}
|
||||
</form>
|
||||
|
@ -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;
|
||||
|
154
web/src/components/ui/command.tsx
Normal file
154
web/src/components/ui/command.tsx
Normal 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,
|
||||
};
|
@ -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 };
|
||||
|
@ -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 };
|
||||
|
@ -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", {
|
||||
|
@ -17,4 +17,5 @@ export const addCustomMachineSchema = insertCustomMachineSchema.pick({
|
||||
name: true,
|
||||
type: true,
|
||||
snapshot: true,
|
||||
models: true,
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,12 +142,21 @@ export const addCustomMachine = withServerPromise(
|
||||
|
||||
const b = a[0];
|
||||
|
||||
// const origin = new URL(request.url).origin;
|
||||
await buildMachine(data, b);
|
||||
redirect(`/machines/${b.id}`);
|
||||
// revalidatePath("/machines");
|
||||
return { message: "Machine Building" };
|
||||
}
|
||||
);
|
||||
|
||||
async function buildMachine(
|
||||
data: z.infer<typeof addCustomMachineSchema>,
|
||||
b: MachineType
|
||||
) {
|
||||
const headersList = headers();
|
||||
|
||||
const domain = headersList.get("x-forwarded-host") || "";
|
||||
const 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");
|
||||
@ -108,8 +171,10 @@ export const addCustomMachine = withServerPromise(
|
||||
body: JSON.stringify({
|
||||
machine_id: b.id,
|
||||
name: b.id,
|
||||
snapshot: JSON.parse(data.snapshot as string),
|
||||
snapshot: data.snapshot, //JSON.parse( as string),
|
||||
callback_url: `${protocol}://${domain}/api/machine-built`,
|
||||
models: data.models, //JSON.parse(data.models as string),
|
||||
gpu: "T4",
|
||||
}),
|
||||
});
|
||||
|
||||
@ -125,13 +190,7 @@ export const addCustomMachine = withServerPromise(
|
||||
.where(eq(machinesTable.id, b.id));
|
||||
throw new Error(`Error: ${result.statusText} ${error_log}`);
|
||||
}
|
||||
|
||||
redirect(`/machines/${b.id}`);
|
||||
|
||||
// revalidatePath("/machines");
|
||||
return { message: "Machine Building" };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const updateMachine = withServerPromise(
|
||||
async ({
|
||||
|
Loading…
x
Reference in New Issue
Block a user