Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5e2f36cd | ||
|
|
0b0015fbd1 | ||
|
|
abb4a4798c | ||
|
|
95238469d6 | ||
|
|
86f14b5bce | ||
|
|
84802f5c4e | ||
|
|
68b4d93639 | ||
|
|
5d349153d2 | ||
|
|
ea675fcd4c | ||
|
|
4c715b815a | ||
|
|
bef6ce35de | ||
|
|
d8951df35f | ||
|
|
38fea1e79f | ||
|
|
66ad3ab4c2 | ||
|
|
488d2aee8c | ||
|
|
c4628f6e4c | ||
|
|
9ba349d36a | ||
|
|
08ab93127e | ||
|
|
de641f0acf | ||
|
|
d4d7e98487 | ||
|
|
967a77d3a3 | ||
|
|
a4cd5db360 | ||
|
|
576a6744a4 | ||
|
|
3e5ff7702e | ||
|
|
009589630d | ||
|
|
8eb2ce3e10 | ||
|
|
e3a1d24304 | ||
|
|
2d033570f4 | ||
|
|
7ae25aa162 | ||
|
|
4b37de9ec5 | ||
|
|
b0d1bcc303 | ||
|
|
2193dd287d | ||
|
|
81bde40aeb | ||
|
|
317f699c46 | ||
|
|
0d1bb2aaf4 |
@@ -13,4 +13,4 @@ RUN mkdir builds
|
||||
# CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "80", "--lifespan", "on"]
|
||||
CMD ["python", "src/main.py"]
|
||||
# If running behind a proxy like Nginx or Traefik add --proxy-headers
|
||||
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]
|
||||
# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80", "--proxy-headers"]
|
||||
|
||||
@@ -19,7 +19,7 @@ import requests
|
||||
from urllib.parse import parse_qs
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.types import ASGIApp, Scope, Receive, Send
|
||||
|
||||
import modal
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
@@ -180,6 +180,8 @@ class Item(BaseModel):
|
||||
models: List[Model]
|
||||
callback_url: str
|
||||
model_volume_name: str
|
||||
run_timeout: Optional[int] = Field(default=60 * 5)
|
||||
idle_timeout: Optional[int] = Field(default=60)
|
||||
gpu: GPUType = Field(default=GPUType.T4)
|
||||
|
||||
@field_validator('gpu')
|
||||
@@ -234,6 +236,14 @@ class UploadType(str, Enum):
|
||||
checkpoint = "checkpoint"
|
||||
lora = "lora"
|
||||
embedding = "embedding"
|
||||
clip = "clip"
|
||||
clip_vision = "clip_vision"
|
||||
configs = "configs"
|
||||
controlnet = "controlnet"
|
||||
upscale_models = "upscale_models"
|
||||
vae = "vae"
|
||||
ipadapter = "ipadapter"
|
||||
other = "other"
|
||||
|
||||
class UploadBody(BaseModel):
|
||||
download_url: str
|
||||
@@ -249,8 +259,46 @@ UPLOAD_TYPE_DIR_MAP = {
|
||||
UploadType.checkpoint: "checkpoints",
|
||||
UploadType.lora: "loras",
|
||||
UploadType.embedding: "embeddings",
|
||||
UploadType.clip: "clip",
|
||||
UploadType.clip_vision: "clip_vision",
|
||||
UploadType.configs: "configs",
|
||||
UploadType.controlnet: "controlnet",
|
||||
UploadType.upscale_models: "upscale_models",
|
||||
UploadType.vae: "vae",
|
||||
UploadType.ipadapter: "ipadapter",
|
||||
UploadType.other: "",
|
||||
}
|
||||
|
||||
class DeleteBody(BaseModel):
|
||||
volume_name: str
|
||||
path: str
|
||||
file_name: str
|
||||
|
||||
|
||||
@app.post("/delete-volume-model")
|
||||
async def delete_model(body: DeleteBody):
|
||||
global last_activity_time
|
||||
last_activity_time = time.time()
|
||||
logger.info(f"Extended inactivity time to {global_timeout}")
|
||||
|
||||
full_path = f"{body.path.rstrip('/')}/{body.file_name}"
|
||||
|
||||
rm_process = await asyncio.subprocess.create_subprocess_exec("modal", "volume", "rm", body.volume_name, full_path,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,)
|
||||
await rm_process.wait()
|
||||
|
||||
logger.info(f"Successfully deleted: {full_path} from volume: {body.volume_name}")
|
||||
stdout, stderr = await rm_process.communicate()
|
||||
if stdout:
|
||||
logger.info(f"cp_process stdout: {stdout.decode()}")
|
||||
if stderr:
|
||||
logger.info(f"cp_process stderr: {stderr.decode()}")
|
||||
|
||||
if rm_process.returncode == 0:
|
||||
return JSONResponse(status_code=200, content={"status":f"Successfully deleted {full_path} from volume {body.volume_name}"})
|
||||
else:
|
||||
return JSONResponse(status_code=500, content={"status": "error", "error": stderr.decode()})
|
||||
|
||||
@app.post("/upload-volume")
|
||||
async def upload_model(body: UploadBody):
|
||||
@@ -265,12 +313,16 @@ async def upload_model(body: UploadBody):
|
||||
|
||||
|
||||
async def upload_logic(body: UploadBody):
|
||||
folder_path = f"/app/builds/{body.volume_id}"
|
||||
folder_path = f"/app/builds/{body.volume_id}-{uuid4()}"
|
||||
|
||||
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume-builder", folder_path)
|
||||
cp_process = await asyncio.subprocess.create_subprocess_exec("cp", "-r", "/app/src/volume_builder", folder_path)
|
||||
await cp_process.wait()
|
||||
|
||||
upload_path = UPLOAD_TYPE_DIR_MAP[body.upload_type]
|
||||
if upload_path == "":
|
||||
# TODO: deal with custom paths
|
||||
pass
|
||||
|
||||
config = {
|
||||
"volume_names": {
|
||||
body.volume_name: {"download_url": body.download_url, "folder_path": upload_path}
|
||||
@@ -284,16 +336,22 @@ async def upload_logic(body: UploadBody):
|
||||
"volume_id": body.volume_id,
|
||||
"folder_path": upload_path,
|
||||
},
|
||||
"civitai_api_key": os.environ.get('CIVITAI_API_KEY')
|
||||
"civitai_api_key": os.environ.get('CIVITAI_API_KEY'),
|
||||
"app_name": f"vol_name_{uuid4()}"
|
||||
}
|
||||
with open(f"{folder_path}/config.py", "w") as f:
|
||||
f.write("config = " + json.dumps(config))
|
||||
|
||||
await asyncio.subprocess.create_subprocess_shell(
|
||||
f"modal run app.py",
|
||||
process = await asyncio.subprocess.create_subprocess_shell(
|
||||
f"python runner.py",
|
||||
cwd=folder_path,
|
||||
env={**os.environ, "COLUMNS": "10000"}
|
||||
)
|
||||
await process.wait()
|
||||
|
||||
# import modal
|
||||
# modal.deploy_stub(stub)
|
||||
# stub["download_model"].web_url
|
||||
|
||||
@app.post("/create")
|
||||
async def create_machine(item: Item):
|
||||
@@ -391,7 +449,9 @@ async def build_logic(item: Item):
|
||||
"gpu": item.gpu,
|
||||
"public_model_volume": public_model_volume_name,
|
||||
"private_model_volume": item.model_volume_name,
|
||||
"pip": list(pip_modules)
|
||||
"pip": list(pip_modules),
|
||||
"run_timeout": item.run_timeout,
|
||||
"idle_timeout": item.idle_timeout,
|
||||
}
|
||||
with open(f"{folder_path}/config.py", "w") as f:
|
||||
f.write("config = " + json.dumps(config))
|
||||
|
||||
@@ -9,6 +9,8 @@ from fastapi import FastAPI, Request, HTTPException
|
||||
from fastapi.responses import HTMLResponse
|
||||
from volume_setup import volumes
|
||||
from datetime import datetime
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
# deploy_test = False
|
||||
|
||||
import os
|
||||
@@ -36,7 +38,9 @@ if not deploy_test:
|
||||
# dockerfile_image = Image.from_dockerfile(f"{current_directory}/Dockerfile", context_mount=Mount.from_local_dir(f"{current_directory}/data", remote_path="/data"))
|
||||
|
||||
dockerfile_image = (
|
||||
modal.Image.debian_slim()
|
||||
modal.Image.debian_slim(
|
||||
python_version="3.11.1"
|
||||
)
|
||||
.apt_install("git", "wget")
|
||||
.pip_install(
|
||||
"git+https://github.com/modal-labs/asgiproxy.git", "httpx", "tqdm"
|
||||
@@ -49,12 +53,12 @@ if not deploy_test:
|
||||
|
||||
# Install comfyui manager
|
||||
"cd /comfyui/custom_nodes && git clone https://github.com/ltdrdata/ComfyUI-Manager.git",
|
||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && git reset --hard 9c86f62b912f4625fe2b929c7fc61deb9d16f6d3",
|
||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && git reset --hard 8dd801435bb75aa1d24b7e382bac070a4c18bc51",
|
||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && pip install -r requirements.txt",
|
||||
"cd /comfyui/custom_nodes/ComfyUI-Manager && mkdir startup-scripts",
|
||||
)
|
||||
.run_commands(f"cat /comfyui/server.py")
|
||||
.run_commands(f"ls /comfyui/app")
|
||||
# .run_commands(f"cat /comfyui/server.py")
|
||||
# .run_commands(f"ls /comfyui/app")
|
||||
# .run_commands(
|
||||
# # Install comfy deploy
|
||||
# "cd /comfyui/custom_nodes && git clone https://github.com/BennyKok/comfyui-deploy.git",
|
||||
@@ -82,7 +86,7 @@ if not deploy_test:
|
||||
# Time to wait between API check attempts in milliseconds
|
||||
COMFY_API_AVAILABLE_INTERVAL_MS = 50
|
||||
# Maximum number of API check attempts
|
||||
COMFY_API_AVAILABLE_MAX_RETRIES = 500
|
||||
COMFY_API_AVAILABLE_MAX_RETRIES = 1000
|
||||
# Time to wait between poll attempts in milliseconds
|
||||
COMFY_POLLING_INTERVAL_MS = 250
|
||||
# Maximum number of poll attempts
|
||||
@@ -91,48 +95,34 @@ COMFY_POLLING_MAX_RETRIES = 1000
|
||||
COMFY_HOST = "127.0.0.1:8188"
|
||||
|
||||
|
||||
def check_server(url, retries=50, delay=500):
|
||||
import requests
|
||||
import time
|
||||
"""
|
||||
Check if a server is reachable via HTTP GET request
|
||||
|
||||
Args:
|
||||
- url (str): The URL to check
|
||||
- retries (int, optional): The number of times to attempt connecting to the server. Default is 50
|
||||
- delay (int, optional): The time in milliseconds to wait between retries. Default is 500
|
||||
|
||||
Returns:
|
||||
bool: True if the server is reachable within the given number of retries, otherwise False
|
||||
"""
|
||||
|
||||
for i in range(retries):
|
||||
async def check_server(url, retries=50, delay=500):
|
||||
import aiohttp
|
||||
# for i in range(retries):
|
||||
while True:
|
||||
try:
|
||||
response = requests.get(url)
|
||||
|
||||
# If the response status code is 200, the server is up and running
|
||||
if response.status_code == 200:
|
||||
print(f"runpod-worker-comfy - API is reachable")
|
||||
return True
|
||||
except requests.RequestException as e:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url) as response:
|
||||
# If the response status code is 200, the server is up and running
|
||||
if response.status == 200:
|
||||
print(f"comfy-modal - API is reachable")
|
||||
return True
|
||||
except Exception as e:
|
||||
# If an exception occurs, the server may not be ready
|
||||
pass
|
||||
|
||||
# print(f"runpod-worker-comfy - trying")
|
||||
|
||||
# Wait for the specified delay before retrying
|
||||
time.sleep(delay / 1000)
|
||||
await asyncio.sleep(delay / 1000)
|
||||
|
||||
print(
|
||||
f"runpod-worker-comfy - Failed to connect to server at {url} after {retries} attempts."
|
||||
f"comfy-modal - Failed to connect to server at {url} after {retries} attempts."
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def check_status(prompt_id):
|
||||
req = urllib.request.Request(
|
||||
f"http://{COMFY_HOST}/comfyui-deploy/check-status?prompt_id={prompt_id}")
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
async def check_status(prompt_id):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(f"http://{COMFY_HOST}/comfyui-deploy/check-status?prompt_id={prompt_id}") as response:
|
||||
return await response.json()
|
||||
|
||||
|
||||
class Input(BaseModel):
|
||||
@@ -142,12 +132,12 @@ class Input(BaseModel):
|
||||
file_upload_endpoint: str
|
||||
|
||||
|
||||
def queue_workflow_comfy_deploy(data: Input):
|
||||
async def queue_workflow_comfy_deploy(data: Input):
|
||||
data_str = data.json()
|
||||
data_bytes = data_str.encode('utf-8')
|
||||
req = urllib.request.Request(
|
||||
f"http://{COMFY_HOST}/comfyui-deploy/run", data=data_bytes)
|
||||
return json.loads(urllib.request.urlopen(req).read())
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(f"http://{COMFY_HOST}/comfyui-deploy/run", data=data_bytes) as response:
|
||||
return await response.json()
|
||||
|
||||
|
||||
class RequestInput(BaseModel):
|
||||
@@ -158,85 +148,226 @@ image = Image.debian_slim()
|
||||
|
||||
target_image = image if deploy_test else dockerfile_image
|
||||
|
||||
@stub.cls(image=target_image, gpu=config["gpu"] ,volumes=volumes, timeout=60 * 10, container_idle_timeout=60)
|
||||
run_timeout = config["run_timeout"]
|
||||
idle_timeout = config["idle_timeout"]
|
||||
|
||||
import asyncio
|
||||
|
||||
@stub.cls(
|
||||
image=target_image,
|
||||
gpu=config["gpu"] ,
|
||||
volumes=volumes,
|
||||
timeout=(config["run_timeout"] + 20),
|
||||
container_idle_timeout=config["idle_timeout"],
|
||||
allow_concurrent_inputs=1,
|
||||
)
|
||||
class ComfyDeployRunner:
|
||||
|
||||
machine_logs = []
|
||||
|
||||
async def read_stream(self, stream, isStderr):
|
||||
import time
|
||||
while True:
|
||||
try:
|
||||
line = await stream.readline()
|
||||
if line:
|
||||
l = line.decode('utf-8').strip()
|
||||
|
||||
if l == "":
|
||||
continue
|
||||
|
||||
if not isStderr:
|
||||
print(l, flush=True)
|
||||
self.machine_logs.append({
|
||||
"logs": l,
|
||||
"timestamp": time.time()
|
||||
})
|
||||
|
||||
else:
|
||||
# is error
|
||||
# logger.error(l)
|
||||
print(l, flush=True)
|
||||
self.machine_logs.append({
|
||||
"logs": l,
|
||||
"timestamp": time.time()
|
||||
})
|
||||
else:
|
||||
break
|
||||
except asyncio.CancelledError:
|
||||
# Handle the cancellation here if needed
|
||||
break # Break out of the loop on cancellation
|
||||
|
||||
@enter()
|
||||
def setup(self):
|
||||
async def setup(self):
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# Make sure that the ComfyUI API is available
|
||||
print(f"comfy-modal - check server")
|
||||
|
||||
command = ["python", "main.py",
|
||||
"--disable-auto-launch", "--disable-metadata"]
|
||||
|
||||
self.server_process = subprocess.Popen(command, cwd="/comfyui")
|
||||
|
||||
check_server(
|
||||
f"http://{COMFY_HOST}",
|
||||
COMFY_API_AVAILABLE_MAX_RETRIES,
|
||||
COMFY_API_AVAILABLE_INTERVAL_MS,
|
||||
self.server_process = await asyncio.subprocess.create_subprocess_shell(
|
||||
f"python main.py --disable-auto-launch --disable-metadata",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd="/comfyui",
|
||||
# env={**os.environ, "COLUMNS": "10000"}
|
||||
)
|
||||
|
||||
@exit()
|
||||
def cleanup(self, exc_type, exc_value, traceback):
|
||||
self.server_process.terminate()
|
||||
async def cleanup(self, exc_type, exc_value, traceback):
|
||||
print(f"comfy-modal - cleanup", exc_type, exc_value, traceback)
|
||||
# Get the current event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
# Check if the event loop is closed
|
||||
if loop.is_closed():
|
||||
print("The event loop is closed.")
|
||||
else:
|
||||
try:
|
||||
self.server_process.terminate()
|
||||
await self.server_process.wait()
|
||||
except Exception as e:
|
||||
print("Issues when cleaning up", e)
|
||||
print("The event loop is open.")
|
||||
|
||||
@method()
|
||||
def run(self, input: Input):
|
||||
data = json.dumps({
|
||||
"run_id": input.prompt_id,
|
||||
"status": "started",
|
||||
"time": datetime.now().isoformat()
|
||||
}).encode('utf-8')
|
||||
req = urllib.request.Request(input.status_endpoint, data=data, method='POST')
|
||||
urllib.request.urlopen(req)
|
||||
|
||||
job_input = input
|
||||
async def run(self, input: Input):
|
||||
import signal
|
||||
import time
|
||||
import aiohttp
|
||||
|
||||
stdout_task = asyncio.create_task(
|
||||
self.read_stream(self.server_process.stdout, False))
|
||||
stderr_task = asyncio.create_task(
|
||||
self.read_stream(self.server_process.stderr, True))
|
||||
|
||||
try:
|
||||
queued_workflow = queue_workflow_comfy_deploy(job_input) # queue_workflow(workflow)
|
||||
prompt_id = queued_workflow["prompt_id"]
|
||||
print(f"comfy-modal - queued workflow with ID {prompt_id}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
return {"error": f"Error queuing workflow: {str(e)}"}
|
||||
class TimeoutError(Exception):
|
||||
pass
|
||||
|
||||
# Poll for completion
|
||||
print(f"comfy-modal - wait until image generation is complete")
|
||||
retries = 0
|
||||
status = ""
|
||||
try:
|
||||
print("getting request")
|
||||
while retries < COMFY_POLLING_MAX_RETRIES:
|
||||
status_result = check_status(prompt_id=prompt_id)
|
||||
# history = get_history(prompt_id)
|
||||
def timeout_handler(signum, frame):
|
||||
data = json.dumps({
|
||||
"run_id": input.prompt_id,
|
||||
"status": "timeout",
|
||||
"time": datetime.now().isoformat()
|
||||
}).encode('utf-8')
|
||||
req = urllib.request.Request(input.status_endpoint, data=data, method='POST')
|
||||
urllib.request.urlopen(req)
|
||||
raise TimeoutError("Operation timed out")
|
||||
|
||||
signal.signal(signal.SIGALRM, timeout_handler)
|
||||
|
||||
# Exit the loop if we have found the history
|
||||
# if prompt_id in history and history[prompt_id].get("outputs"):
|
||||
# break
|
||||
try:
|
||||
signal.alarm(run_timeout)
|
||||
|
||||
# Exit the loop if we have found the status both success or failed
|
||||
if 'status' in status_result and (status_result['status'] == 'success' or status_result['status'] == 'failed'):
|
||||
status = status_result['status']
|
||||
print(status)
|
||||
break
|
||||
else:
|
||||
# Wait before trying again
|
||||
time.sleep(COMFY_POLLING_INTERVAL_MS / 1000)
|
||||
retries += 1
|
||||
else:
|
||||
return {"error": "Max retries reached while waiting for image generation"}
|
||||
except Exception as e:
|
||||
return {"error": f"Error waiting for image generation: {str(e)}"}
|
||||
ok = await check_server(
|
||||
f"http://{COMFY_HOST}",
|
||||
COMFY_API_AVAILABLE_MAX_RETRIES,
|
||||
COMFY_API_AVAILABLE_INTERVAL_MS,
|
||||
)
|
||||
|
||||
print(f"comfy-modal - Finished, turning off")
|
||||
if not ok:
|
||||
raise Exception("ComfyUI API is not available")
|
||||
# Set an alarm for some seconds in the future
|
||||
|
||||
result = {"status": status}
|
||||
data = json.dumps({
|
||||
"run_id": input.prompt_id,
|
||||
"status": "started",
|
||||
"time": datetime.now().isoformat()
|
||||
}).encode('utf-8')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(input.status_endpoint, data=data) as response:
|
||||
pass
|
||||
|
||||
job_input = input
|
||||
|
||||
try:
|
||||
queued_workflow = await queue_workflow_comfy_deploy(job_input) # queue_workflow(workflow)
|
||||
prompt_id = queued_workflow["prompt_id"]
|
||||
print(f"comfy-modal - queued workflow with ID {prompt_id}")
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(traceback.format_exc())
|
||||
return {"error": f"Error queuing workflow: {str(e)}"}
|
||||
|
||||
# Poll for completion
|
||||
print(f"comfy-modal - wait until image generation is complete")
|
||||
retries = 0
|
||||
status = ""
|
||||
try:
|
||||
print("getting request")
|
||||
# while retries < COMFY_POLLING_MAX_RETRIES:
|
||||
while True:
|
||||
status_result = await check_status(prompt_id=prompt_id)
|
||||
if 'status' in status_result and (status_result['status'] == 'success' or status_result['status'] == 'failed'):
|
||||
status = status_result['status']
|
||||
print(status)
|
||||
break
|
||||
else:
|
||||
# Wait before trying again
|
||||
await asyncio.sleep(COMFY_POLLING_INTERVAL_MS / 1000)
|
||||
retries += 1
|
||||
else:
|
||||
return {"error": "Max retries reached while waiting for image generation"}
|
||||
except Exception as e:
|
||||
return {"error": f"Error waiting for image generation: {str(e)}"}
|
||||
|
||||
print(f"comfy-modal - Finished, turning off")
|
||||
|
||||
result = {"status": status}
|
||||
|
||||
except TimeoutError:
|
||||
print("Operation timed out")
|
||||
return {"status": "failed"}
|
||||
except Exception as e:
|
||||
print(f"Unexpected error occurred: {str(e)}")
|
||||
data = json.dumps({
|
||||
"run_id": input.prompt_id,
|
||||
"status": "failed",
|
||||
"time": datetime.now().isoformat()
|
||||
}).encode('utf-8')
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(input.status_endpoint, data=data) as response:
|
||||
print("response", response)
|
||||
self.machine_logs.append({
|
||||
"logs": str(e),
|
||||
"timestamp": time.time()
|
||||
})
|
||||
finally:
|
||||
signal.alarm(0)
|
||||
|
||||
print("uploading log_data")
|
||||
data = json.dumps({
|
||||
"run_id": input.prompt_id,
|
||||
"time": datetime.now().isoformat(),
|
||||
"log_data": self.machine_logs
|
||||
}).encode('utf-8')
|
||||
print("my logs", len(self.machine_logs))
|
||||
# Clear logs
|
||||
timeout = aiohttp.ClientTimeout(total=60) # 60 seconds total timeout
|
||||
# Use HTTP/1.1 explicitly and increase the connection pool size
|
||||
connector = TCPConnector(limit=100, force_close=True, enable_cleanup_closed=True)
|
||||
|
||||
async with aiohttp.ClientSession(timeout=timeout, connector=connector) as session:
|
||||
try:
|
||||
async with session.post(input.status_endpoint, data=data) as response:
|
||||
print("response", response)
|
||||
# Process your response here
|
||||
except asyncio.TimeoutError:
|
||||
print("Request timed out")
|
||||
except Exception as e:
|
||||
print(f"An error occurred: {e}")
|
||||
print("uploaded log_data")
|
||||
# print(data)
|
||||
self.machine_logs = []
|
||||
finally:
|
||||
stdout_task.cancel()
|
||||
stderr_task.cancel()
|
||||
await stdout_task
|
||||
await stderr_task
|
||||
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@web_app.post("/run")
|
||||
@@ -252,10 +383,12 @@ async def post_run(request_input: RequestInput):
|
||||
urllib.request.urlopen(req)
|
||||
|
||||
model = ComfyDeployRunner()
|
||||
call = model.run.spawn(request_input.input)
|
||||
call = await model.run.spawn.aio(request_input.input)
|
||||
|
||||
print("call", call)
|
||||
|
||||
# call = run.spawn()
|
||||
return {"call_id": call.object_id}
|
||||
return {"call_id": None}
|
||||
|
||||
return {"call_id": None}
|
||||
|
||||
|
||||
@@ -4,5 +4,7 @@ config = {
|
||||
"gpu": "T4",
|
||||
"public_model_volume": "model-store",
|
||||
"private_model_volume": "private-model-store",
|
||||
"pip": []
|
||||
"pip": [],
|
||||
"run_timeout": 60 * 5,
|
||||
"idle_timeout": 60
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ public:
|
||||
loras: loras
|
||||
upscale_models: upscale_models
|
||||
vae: vae
|
||||
ipadapter: ipadapter
|
||||
|
||||
|
||||
private:
|
||||
base_path: /private_models/
|
||||
@@ -21,3 +23,4 @@ private:
|
||||
loras: loras
|
||||
upscale_models: upscale_models
|
||||
vae: vae
|
||||
ipadapter: ipadapter
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"comfyui": "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
||||
"git_custom_nodes": {
|
||||
"https://github.com/BennyKok/comfyui-deploy.git": {
|
||||
"hash": "a838cb7ad425e5652c3931fbafdc886b53c48a22",
|
||||
"hash": "df46e3a0e5ad93fa71f5d216997e376af33b2a6d",
|
||||
"disabled": false
|
||||
}
|
||||
},
|
||||
|
||||
+13
-11
@@ -1,10 +1,18 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
import modal
|
||||
from config import config
|
||||
import os
|
||||
import subprocess
|
||||
from pprint import pprint
|
||||
|
||||
stub = modal.Stub()
|
||||
stub = modal.Stub(config["app_name"])
|
||||
vol_name_to_links = config["volume_names"]
|
||||
vol_name_to_path = config["volume_paths"]
|
||||
callback_url = config["callback_url"]
|
||||
callback_body = config["callback_body"]
|
||||
civitai_key = config["civitai_api_key"]
|
||||
web_app = FastAPI()
|
||||
|
||||
# Volume names may only contain alphanumeric characters, dashes, periods, and underscores, and must be less than 64 characters in length.
|
||||
def is_valid_name(name: str) -> bool:
|
||||
@@ -21,12 +29,6 @@ def create_volumes(volume_names, paths):
|
||||
|
||||
return path_to_vol
|
||||
|
||||
vol_name_to_links = config["volume_names"]
|
||||
vol_name_to_path = config["volume_paths"]
|
||||
callback_url = config["callback_url"]
|
||||
callback_body = config["callback_body"]
|
||||
civitai_key = config["civitai_api_key"]
|
||||
|
||||
volumes = create_volumes(vol_name_to_links, vol_name_to_path)
|
||||
image = (
|
||||
modal.Image.debian_slim().apt_install("wget").pip_install("requests")
|
||||
@@ -45,7 +47,7 @@ def download_model(volume_name, download_config):
|
||||
modified_download_url = download_url + ("&" if "?" in download_url else "?") + "token=" + civitai_key # civitai requires auth
|
||||
print('downloading', modified_download_url)
|
||||
|
||||
subprocess.run(["wget", modified_download_url , "--content-disposition", "-P", model_store_path])
|
||||
subprocess.run(["wget", modified_download_url , "--content-disposition", "-P", model_store_path, "-nv"])
|
||||
subprocess.run(["ls", "-la", volume_base_path])
|
||||
subprocess.run(["ls", "-la", model_store_path])
|
||||
volumes[volume_base_path].commit()
|
||||
@@ -56,11 +58,12 @@ def download_model(volume_name, download_config):
|
||||
print(f"finished! sending to {callback_url}")
|
||||
pprint({**status, **callback_body})
|
||||
|
||||
@stub.local_entrypoint()
|
||||
@stub.function(image=image)
|
||||
# @modal.asgi_app()
|
||||
def simple_download():
|
||||
import requests
|
||||
try:
|
||||
list(download_model.starmap([(vol_name, link) for vol_name,link in vol_name_to_links.items()]))
|
||||
list(download_model.starmap([(vol_name, download_conf) for vol_name,download_conf in vol_name_to_links.items()]))
|
||||
except modal.exception.FunctionTimeoutError as e:
|
||||
status = {"status": "failed", "error_logs": f"{str(e)}", "timeout": timeout}
|
||||
requests.post(callback_url, json={**status, **callback_body})
|
||||
@@ -71,4 +74,3 @@ def simple_download():
|
||||
requests.post(callback_url, json={**status, **callback_body})
|
||||
print(f"finished! sending to {callback_url}")
|
||||
pprint({**status, **callback_body})
|
||||
|
||||
+1
@@ -15,4 +15,5 @@ config = {
|
||||
"folder_path": "checkpoints",
|
||||
},
|
||||
"civitai_api_key": "",
|
||||
"app_name": "",
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import modal
|
||||
import requests
|
||||
from app import stub
|
||||
from config import config
|
||||
|
||||
modal.runner.deploy_stub(stub)
|
||||
print("deployed stub")
|
||||
# web_url = stub["simple_download"].web_url
|
||||
f = modal.Function.lookup(config['app_name'], "simple_download")
|
||||
f.spawn()
|
||||
# print(f"web_url: {web_url}")
|
||||
# requests.post(web_url)
|
||||
+65
-15
@@ -26,6 +26,8 @@ import threading
|
||||
api = None
|
||||
api_task = None
|
||||
prompt_metadata = {}
|
||||
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
|
||||
cd_enable_run_log = os.environ.get('CD_ENABLE_RUN_LOG', 'false').lower() == 'true'
|
||||
|
||||
def post_prompt(json_data):
|
||||
prompt_server = server.PromptServer.instance
|
||||
@@ -157,7 +159,9 @@ async def websocket_handler(request):
|
||||
try:
|
||||
# Send initial state to the new client
|
||||
await send("status", { 'sid': sid }, sid)
|
||||
await send_first_time_log(sid)
|
||||
|
||||
if cd_enable_log:
|
||||
await send_first_time_log(sid)
|
||||
|
||||
async for msg in ws:
|
||||
if msg.type == aiohttp.WSMsgType.ERROR:
|
||||
@@ -236,7 +240,12 @@ class Status(Enum):
|
||||
FAILED = "failed"
|
||||
UPLOADING = "uploading"
|
||||
|
||||
# Global variable to keep track of the last read line number
|
||||
last_read_line_number = 0
|
||||
|
||||
def update_run(prompt_id, status: Status):
|
||||
global last_read_line_number
|
||||
|
||||
if prompt_id not in prompt_metadata:
|
||||
return
|
||||
|
||||
@@ -251,16 +260,50 @@ def update_run(prompt_id, status: Status):
|
||||
"run_id": prompt_id,
|
||||
"status": status.value,
|
||||
}
|
||||
prompt_metadata[prompt_id]['status'] = status
|
||||
print(f"Status: {status.value}")
|
||||
|
||||
try:
|
||||
requests.post(status_endpoint, json=body)
|
||||
|
||||
if cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED):
|
||||
try:
|
||||
with open(comfyui_file_path, 'r') as log_file:
|
||||
# log_data = log_file.read()
|
||||
# Move to the last read line
|
||||
all_log_data = log_file.read() # Read all log data
|
||||
print("All log data before skipping:", all_log_data) # Log all data before skipping
|
||||
log_file.seek(0) # Reset file pointer to the beginning
|
||||
|
||||
for _ in range(last_read_line_number):
|
||||
next(log_file)
|
||||
log_data = log_file.read()
|
||||
# Update the last read line number
|
||||
last_read_line_number += log_data.count('\n')
|
||||
print("last_read_line_number", last_read_line_number)
|
||||
print("log_data", log_data)
|
||||
print("log_data.count(n)", log_data.count('\n'))
|
||||
|
||||
body = {
|
||||
"run_id": prompt_id,
|
||||
"log_data": [
|
||||
{
|
||||
"logs": log_data,
|
||||
# "timestamp": time.time(),
|
||||
}
|
||||
]
|
||||
}
|
||||
requests.post(status_endpoint, json=body)
|
||||
except Exception as log_error:
|
||||
print(f"Error reading log file: {log_error}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_type = type(e).__name__
|
||||
stack_trace = traceback.format_exc().strip()
|
||||
print(f"Error occurred while updating run: {e} {stack_trace}")
|
||||
|
||||
finally:
|
||||
prompt_metadata[prompt_id]['status'] = status
|
||||
|
||||
|
||||
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
|
||||
"""
|
||||
@@ -387,20 +430,25 @@ async def update_file_status(prompt_id, data, uploading, have_error=False, node_
|
||||
"prompt_id": prompt_id,
|
||||
})
|
||||
|
||||
async def handle_upload(prompt_id, data, key, content_type_key, default_content_type):
|
||||
items = data.get(key, [])
|
||||
for item in items:
|
||||
await upload_file(
|
||||
prompt_id,
|
||||
item.get("filename"),
|
||||
subfolder=item.get("subfolder"),
|
||||
type=item.get("type"),
|
||||
content_type=item.get(content_type_key, default_content_type)
|
||||
)
|
||||
|
||||
|
||||
# Upload files in the background
|
||||
async def upload_in_background(prompt_id, data, node_id=None, have_upload=True):
|
||||
try:
|
||||
images = data.get('images', [])
|
||||
for image in images:
|
||||
await upload_file(prompt_id, image.get("filename"), subfolder=image.get("subfolder"), type=image.get("type"), content_type=image.get("content_type", "image/png"))
|
||||
|
||||
files = data.get('files', [])
|
||||
for file in files:
|
||||
await upload_file(prompt_id, file.get("filename"), subfolder=file.get("subfolder"), type=file.get("type"), content_type=file.get("content_type", "image/png"))
|
||||
|
||||
gifs = data.get('gifs', [])
|
||||
for gif in gifs:
|
||||
await upload_file(prompt_id, gif.get("filename"), subfolder=gif.get("subfolder"), type=gif.get("type"), content_type=gif.get("format", "image/gif"))
|
||||
await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
|
||||
await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
|
||||
# This will also be mp4
|
||||
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
|
||||
|
||||
if have_upload:
|
||||
await update_file_status(prompt_id, data, False, node_id=node_id)
|
||||
@@ -441,6 +489,7 @@ prompt_server.send_json = send_json_override.__get__(prompt_server, server.Promp
|
||||
root_path = os.path.dirname(os.path.abspath(__file__))
|
||||
two_dirs_up = os.path.dirname(os.path.dirname(root_path))
|
||||
log_file_path = os.path.join(two_dirs_up, 'comfy-deploy.log')
|
||||
comfyui_file_path = os.path.join(two_dirs_up, 'comfyui.log')
|
||||
|
||||
last_read_line = 0
|
||||
|
||||
@@ -480,4 +529,5 @@ def run_in_new_thread(coroutine):
|
||||
t.start()
|
||||
asyncio.run_coroutine_threadsafe(coroutine, new_loop)
|
||||
|
||||
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
||||
if cd_enable_log:
|
||||
run_in_new_thread(watch_file_changes(log_file_path, send_logs_to_websocket))
|
||||
|
||||
+43
-32
@@ -7,45 +7,56 @@ import threading
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
||||
# Running with export CD_ENABLE_LOG=true; python main.py
|
||||
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
# Check for 'cd-enable-log' flag in input arguments
|
||||
# cd_enable_log = '--cd-enable-log' in sys.argv
|
||||
cd_enable_log = os.environ.get('CD_ENABLE_LOG', 'false').lower() == 'true'
|
||||
|
||||
class StreamToLogger():
|
||||
def __init__(self, log_level):
|
||||
self.log_level = log_level
|
||||
def setup():
|
||||
handler = RotatingFileHandler('comfy-deploy.log', maxBytes=500000, backupCount=5)
|
||||
|
||||
def write(self, buf):
|
||||
if (self.log_level == logging.INFO):
|
||||
original_stdout.write(buf)
|
||||
original_stdout.flush()
|
||||
elif (self.log_level == logging.ERROR):
|
||||
original_stderr.write(buf)
|
||||
original_stderr.flush()
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
|
||||
for line in buf.rstrip().splitlines():
|
||||
handler.handle(
|
||||
logging.LogRecord(
|
||||
name="comfy-deploy",
|
||||
level=self.log_level,
|
||||
pathname="prestartup_script.py",
|
||||
lineno=1,
|
||||
msg=line.rstrip(),
|
||||
args=None,
|
||||
exc_info=None
|
||||
class StreamToLogger():
|
||||
def __init__(self, log_level):
|
||||
self.log_level = log_level
|
||||
|
||||
def write(self, buf):
|
||||
if (self.log_level == logging.INFO):
|
||||
original_stdout.write(buf)
|
||||
original_stdout.flush()
|
||||
elif (self.log_level == logging.ERROR):
|
||||
original_stderr.write(buf)
|
||||
original_stderr.flush()
|
||||
|
||||
for line in buf.rstrip().splitlines():
|
||||
handler.handle(
|
||||
logging.LogRecord(
|
||||
name="comfy-deploy",
|
||||
level=self.log_level,
|
||||
pathname="prestartup_script.py",
|
||||
lineno=1,
|
||||
msg=line.rstrip(),
|
||||
args=None,
|
||||
exc_info=None
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def flush(self):
|
||||
if (self.log_level == logging.INFO):
|
||||
original_stdout.flush()
|
||||
elif (self.log_level == logging.ERROR):
|
||||
original_stderr.flush()
|
||||
def flush(self):
|
||||
if (self.log_level == logging.INFO):
|
||||
original_stdout.flush()
|
||||
elif (self.log_level == logging.ERROR):
|
||||
original_stderr.flush()
|
||||
|
||||
# Redirect stdout and stderr to the logger
|
||||
sys.stdout = StreamToLogger(logging.INFO)
|
||||
sys.stderr = StreamToLogger(logging.ERROR)
|
||||
# Redirect stdout and stderr to the logger
|
||||
sys.stdout = StreamToLogger(logging.INFO)
|
||||
sys.stderr = StreamToLogger(logging.ERROR)
|
||||
|
||||
if cd_enable_log:
|
||||
print("** Comfy Deploy logging enabled")
|
||||
setup()
|
||||
|
||||
try:
|
||||
# Get the absolute path of the script's directory
|
||||
|
||||
+21
-7
@@ -50,11 +50,22 @@ const ext = {
|
||||
})
|
||||
.then(async (res) => {
|
||||
const data = await res.json();
|
||||
const { workflow, error } = data;
|
||||
const { workflow, workflow_id, error } = data;
|
||||
if (error) {
|
||||
infoDialog.showMessage("Unable to load this workflow", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Adding a delay to wait for the intial graph to load
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
workflow?.nodes.forEach((x) => {
|
||||
if (x?.type === "ComfyDeploy") {
|
||||
x.widgets_values[1] = workflow_id;
|
||||
// x.widgets_values[2] = workflow_version.version;
|
||||
}
|
||||
});
|
||||
|
||||
/** @type {LGraph} */
|
||||
app.loadGraphData(workflow);
|
||||
})
|
||||
@@ -682,16 +693,19 @@ export class ConfigDialog extends ComfyDialog {
|
||||
</label>
|
||||
<label style="color: white; width: 100%;">
|
||||
Endpoint:
|
||||
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${data.endpoint
|
||||
}">
|
||||
<input id="endpoint" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="text" value="${
|
||||
data.endpoint
|
||||
}">
|
||||
</label>
|
||||
<label style="color: white;">
|
||||
API Key: ${data.displayName ?? ""}
|
||||
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${data.apiKey
|
||||
}">
|
||||
<input id="apiKey" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;" type="password" value="${
|
||||
data.apiKey
|
||||
}">
|
||||
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
|
||||
${data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
|
||||
}
|
||||
${
|
||||
data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
|
||||
}
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TYPE "workflow_run_status" ADD VALUE 'timeout';--> statement-breakpoint
|
||||
ALTER TABLE "comfyui_deploy"."workflow_runs" ADD COLUMN "run_log" text;
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "comfyui_deploy"."workflow_runs" DROP COLUMN "run_log";
|
||||
ALTER TABLE "comfyui_deploy"."workflow_runs" ADD COLUMN "run_log" jsonb;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -337,6 +337,20 @@
|
||||
"when": 1706384528895,
|
||||
"tag": "0047_gifted_starbolt",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 48,
|
||||
"version": "5",
|
||||
"when": 1706600255919,
|
||||
"tag": "0048_dear_korath",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 49,
|
||||
"version": "5",
|
||||
"when": 1706631744127,
|
||||
"tag": "0049_sweet_hex",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
||||
import { db } from "@/db/db";
|
||||
import {
|
||||
WorkflowRunStatusSchema,
|
||||
userUsageTable,
|
||||
workflowRunOutputs,
|
||||
workflowRunsTable,
|
||||
workflowTable,
|
||||
} from "@/db/schema";
|
||||
import { getCurrentPlan } from "@/server/getCurrentPlan";
|
||||
import { stripe } from "@/server/stripe";
|
||||
@@ -18,6 +16,7 @@ const Request = z.object({
|
||||
status: WorkflowRunStatusSchema.optional(),
|
||||
time: z.coerce.date().optional(),
|
||||
output_data: z.any().optional(),
|
||||
log_data: z.any().optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
@@ -26,7 +25,26 @@ export async function POST(request: Request) {
|
||||
|
||||
if (!data || error) return error;
|
||||
|
||||
const { run_id, status, time, output_data } = data;
|
||||
const { run_id, status, time, output_data, log_data } = data;
|
||||
|
||||
if (log_data) {
|
||||
// It successfully started, update the started_at time
|
||||
await db
|
||||
.update(workflowRunsTable)
|
||||
.set({
|
||||
run_log: log_data,
|
||||
})
|
||||
.where(eq(workflowRunsTable.id, run_id));
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "success",
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (status == "started" && time != undefined) {
|
||||
// It successfully started, update the started_at time
|
||||
@@ -48,6 +66,9 @@ export async function POST(request: Request) {
|
||||
.where(eq(workflowRunsTable.id, run_id));
|
||||
}
|
||||
|
||||
const ended =
|
||||
status === "success" || status === "failed" || status === "timeout";
|
||||
|
||||
if (output_data) {
|
||||
const workflow_run_output = await db.insert(workflowRunOutputs).values({
|
||||
run_id: run_id,
|
||||
@@ -58,8 +79,7 @@ export async function POST(request: Request) {
|
||||
.update(workflowRunsTable)
|
||||
.set({
|
||||
status: status,
|
||||
ended_at:
|
||||
status === "success" || status === "failed" ? new Date() : null,
|
||||
ended_at: ended ? new Date() : null,
|
||||
})
|
||||
.where(eq(workflowRunsTable.id, run_id))
|
||||
.returning();
|
||||
@@ -67,10 +87,7 @@ export async function POST(request: Request) {
|
||||
// Need to filter out only comfy deploy serverless
|
||||
// Also multiply with the gpu selection
|
||||
if (workflow_run.machine_type == "comfy-deploy-serverless") {
|
||||
if (
|
||||
(status === "success" || status === "failed") &&
|
||||
workflow_run.user_id
|
||||
) {
|
||||
if (ended && workflow_run.user_id) {
|
||||
const sub = await getCurrentPlan({
|
||||
user_id: workflow_run.user_id,
|
||||
org_id: workflow_run.org_id,
|
||||
@@ -91,12 +108,16 @@ export async function POST(request: Request) {
|
||||
durationInSec *= 4;
|
||||
break;
|
||||
}
|
||||
await stripe.subscriptionItems.createUsageRecord(
|
||||
sub.subscription_item_api_id,
|
||||
{
|
||||
quantity: durationInSec,
|
||||
},
|
||||
);
|
||||
try {
|
||||
await stripe.subscriptionItems.createUsageRecord(
|
||||
sub.subscription_item_api_id,
|
||||
{
|
||||
quantity: durationInSec,
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ export default async function Page({
|
||||
/>
|
||||
)}
|
||||
{machine.status !== "building" && machine.build_log && (
|
||||
<LogsViewer logs={JSON.parse(machine.build_log)} />
|
||||
<LogsViewer
|
||||
logs={JSON.parse(machine.build_log)}
|
||||
className="h-full max-h-[600px]"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -16,11 +16,19 @@ const freeTierSeconds = 30000;
|
||||
export default async function Home() {
|
||||
const sub = await getCurrentPlanWithAuth();
|
||||
|
||||
const data = sub?.subscription_item_api_id
|
||||
? await stripe.subscriptionItems.listUsageRecordSummaries(
|
||||
sub?.subscription_item_api_id,
|
||||
)
|
||||
: null;
|
||||
let data: Awaited<
|
||||
ReturnType<typeof stripe.subscriptionItems.listUsageRecordSummaries>
|
||||
> | null = null;
|
||||
|
||||
try {
|
||||
data = sub?.subscription_item_api_id
|
||||
? await stripe.subscriptionItems.listUsageRecordSummaries(
|
||||
sub?.subscription_item_api_id,
|
||||
)
|
||||
: null;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex items-center justify-center">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LoadingWrapper } from "@/components/LoadingWrapper";
|
||||
import { DeploymentsTable } from "@/components/RunsTable";
|
||||
import { DeploymentsTable } from "@/components/DeploymentsTable";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default async function Page({
|
||||
|
||||
@@ -13,12 +13,12 @@ export default async function Page({
|
||||
const workflow_id = params.workflow_id;
|
||||
|
||||
return (
|
||||
<Card className="w-full h-fit min-w-0">
|
||||
<CardHeader className="relative">
|
||||
<Card className="w-full h-fit min-w-0 relative">
|
||||
<CardHeader>
|
||||
<CardTitle>Run</CardTitle>
|
||||
<div className="absolute right-6 top-6">
|
||||
{/* <div className="absolute right-6 top-6">
|
||||
<RouteRefresher interval={5000} autoRefresh={false} />
|
||||
</div>
|
||||
</div> */}
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
OpenEditButton,
|
||||
RunWorkflowButton,
|
||||
VersionSelect,
|
||||
ViewWorkflowDetailsButton,
|
||||
} from "@/components/VersionSelect";
|
||||
import { ViewWorkflowDetailsButton } from "@/components/ViewWorkflowDetailsButton";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
|
||||
@@ -13,10 +13,7 @@ export async function CodeBlock(props: {
|
||||
|
||||
return (
|
||||
<div className="relative w-full text-sm">
|
||||
{/* max-w-[calc(32rem-1.5rem-1.5rem)] */}
|
||||
{/* <div className=""> */}
|
||||
<p
|
||||
// tabIndex={1}
|
||||
className="[&>pre]:p-4 rounded-lg max-h-96 overflow-auto w-full"
|
||||
style={{
|
||||
overflowWrap: "break-word",
|
||||
@@ -28,7 +25,6 @@ export async function CodeBlock(props: {
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
{/* </div> */}
|
||||
<CopyButton className="absolute right-2 top-2" text={props.code} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { CopyButton } from "@/components/CopyButton";
|
||||
import type { StringLiteralUnion } from "shikiji";
|
||||
import useSWR from "swr";
|
||||
import { highlight } from "../server/highlight";
|
||||
|
||||
export function CodeBlockClient({
|
||||
code,
|
||||
lang,
|
||||
}: {
|
||||
code: string;
|
||||
lang: StringLiteralUnion<string>;
|
||||
}) {
|
||||
const { data } = useSWR(code, async () => {
|
||||
return highlight(code.trim(), lang);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative w-full text-sm">
|
||||
{data && (
|
||||
<p
|
||||
className="[&>pre]:p-4 rounded-lg max-h-96 overflow-auto w-full"
|
||||
style={{
|
||||
overflowWrap: "break-word",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: data,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<CopyButton className="absolute right-2 top-2" text={code} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,12 @@ import { toast } from "sonner";
|
||||
|
||||
export function CopyButton({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
text: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
@@ -21,7 +23,7 @@ export function CopyButton({
|
||||
}}
|
||||
className={cn(" p-2 min-h-0 aspect-square", className)}
|
||||
>
|
||||
<Copy size={14} />
|
||||
{children} <Copy size={14} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,13 +69,16 @@ const client = new ComfyDeployClient({
|
||||
`;
|
||||
|
||||
const jsClientCreateRunTemplate = `
|
||||
const { run_id } = await client.run("<ID>", {
|
||||
const { run_id } = await client.run({
|
||||
deployment_id: "<ID>",
|
||||
inputs: {}
|
||||
});
|
||||
`;
|
||||
|
||||
const jsClientCreateRunNoInputsTemplate = `
|
||||
const { run_id } = await client.run("<ID>");
|
||||
const { run_id } = await client.run({
|
||||
deployment_id: "<ID>"
|
||||
});
|
||||
`;
|
||||
|
||||
const clientTemplate_checkStatus = `
|
||||
@@ -119,10 +122,7 @@ export function DeploymentDisplay({
|
||||
<TabsContent className="flex flex-col gap-2 !mt-0" value="client">
|
||||
<div>
|
||||
Install the node comfydeploy SDK
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={`npm i comfydeploy`}
|
||||
/>
|
||||
<CodeBlock lang="bash" code={`npm i comfydeploy`} />
|
||||
Initialize your client
|
||||
</div>
|
||||
<CodeBlock
|
||||
@@ -197,20 +197,18 @@ function formatCode(
|
||||
if (inputs && inputs.length > 0) {
|
||||
codeTemplate = codeTemplate.replace(
|
||||
"inputs: {}",
|
||||
`inputs: ${
|
||||
JSON.stringify(
|
||||
Object.fromEntries(
|
||||
inputs.map((x) => {
|
||||
return [x?.input_id, ""];
|
||||
}),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
.split("\n")
|
||||
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
|
||||
.join("\n")
|
||||
}`,
|
||||
`inputs: ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
inputs.map((x) => {
|
||||
return [x?.input_id, ""];
|
||||
}),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
.split("\n")
|
||||
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
|
||||
.join("\n")}`,
|
||||
);
|
||||
} else {
|
||||
codeTemplate = codeTemplate.replace(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { headers } from "next/headers";
|
||||
import { findAllDeployments } from "../server/findAllRuns";
|
||||
import { DeploymentDisplay } from "./DeploymentDisplay";
|
||||
|
||||
export async function DeploymentsTable(props: { workflow_id: string }) {
|
||||
const allRuns = await findAllDeployments(props.workflow_id);
|
||||
|
||||
const headersList = headers();
|
||||
const host = headersList.get("host") || "";
|
||||
const protocol = headersList.get("x-forwarded-proto") || "";
|
||||
const domain = `${protocol}://${host}`;
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-fit w-full">
|
||||
<Table className="">
|
||||
<TableCaption>A list of your deployments</TableCaption>
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
<TableRow>
|
||||
<TableHead className=" w-[100px]">Environment</TableHead>
|
||||
<TableHead className=" w-[100px]">Version</TableHead>
|
||||
<TableHead className="">Machine</TableHead>
|
||||
<TableHead className=" text-right">Updated At</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allRuns.map((run) => (
|
||||
<DeploymentDisplay deployment={run} key={run.id} domain={domain} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+139
-100
@@ -23,6 +23,33 @@ import * as React from "react";
|
||||
import { useState } from "react";
|
||||
import type { UnknownKeysParam, ZodObject, ZodRawShape, z } from "zod";
|
||||
|
||||
type ContextType = [Partial<any>, React.Dispatch<React.SetStateAction<any>>];
|
||||
const AutoFormValueContext = React.createContext<ContextType | null>(null);
|
||||
|
||||
function AutoFormValueProvider<Z extends ZodObject<any, any>>({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
value: ContextType;
|
||||
}) {
|
||||
return (
|
||||
<AutoFormValueContext.Provider value={value}>
|
||||
{children}
|
||||
</AutoFormValueContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useAutoFormValueContext<Z extends ZodObject<any, any>>() {
|
||||
const context = React.useContext(AutoFormValueContext);
|
||||
|
||||
// if (!context) {
|
||||
// throw new Error("useInsertModal must be used within a InsertModalProvider");
|
||||
// }
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export function InsertModal<
|
||||
K extends ZodRawShape,
|
||||
Y extends UnknownKeysParam,
|
||||
@@ -41,65 +68,73 @@ export function InsertModal<
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
const [values, setValues] = useState<Partial<z.infer<Z>>>({});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{/* <DialogTrigger disabled={props.disabled}> */}
|
||||
{props.tooltip ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className={props.disabled ? "opacity-50" : ""}
|
||||
onClick={() => {
|
||||
if (props.disabled) return;
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.buttonTitle ?? props.title}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{props.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
<AutoFormValueProvider value={[values, setValues]}>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{/* <DialogTrigger disabled={props.disabled}> */}
|
||||
{props.tooltip ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className={props.disabled ? "opacity-50" : ""}
|
||||
onClick={() => {
|
||||
if (props.disabled) return;
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.buttonTitle ?? props.title}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{props.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.title}
|
||||
</Button>
|
||||
)}
|
||||
{/* </DialogTrigger> */}
|
||||
<DialogContent
|
||||
className={cn("sm:max-w-[425px]", props.dialogClassName)}
|
||||
>
|
||||
{props.title}
|
||||
</Button>
|
||||
)}
|
||||
{/* </DialogTrigger> */}
|
||||
<DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* <ScrollArea> */}
|
||||
<AutoForm
|
||||
fieldConfig={props.fieldConfig}
|
||||
formSchema={props.formSchema}
|
||||
onSubmit={async (data) => {
|
||||
setIsLoading(true);
|
||||
await callServerPromise(props.serverAction(data));
|
||||
setIsLoading(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<AutoFormSubmit>
|
||||
Save Changes
|
||||
{isLoading && <LoadingIcon />}
|
||||
</AutoFormSubmit>
|
||||
</div>
|
||||
</AutoForm>
|
||||
{/* </ScrollArea> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* <ScrollArea> */}
|
||||
<AutoForm
|
||||
values={values}
|
||||
onValuesChange={setValues}
|
||||
fieldConfig={props.fieldConfig}
|
||||
formSchema={props.formSchema}
|
||||
onSubmit={async (data) => {
|
||||
setIsLoading(true);
|
||||
await callServerPromise(props.serverAction(data));
|
||||
setIsLoading(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<AutoFormSubmit>
|
||||
Save Changes
|
||||
{isLoading && <LoadingIcon />}
|
||||
</AutoFormSubmit>
|
||||
</div>
|
||||
</AutoForm>
|
||||
{/* </ScrollArea> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AutoFormValueProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,49 +173,53 @@ export function UpdateModal<
|
||||
}, [props.data]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{props.trigger ?? (
|
||||
<DialogTrigger
|
||||
className="appearance-none hover:cursor-pointer"
|
||||
asChild
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
<AutoFormValueProvider value={[values, setValues]}>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{props.trigger ?? (
|
||||
<DialogTrigger
|
||||
className="appearance-none hover:cursor-pointer"
|
||||
asChild
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{props.trigger}
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent
|
||||
className={cn("sm:max-w-[425px]", props.dialogClassName)}
|
||||
>
|
||||
{props.trigger}
|
||||
</DialogTrigger>
|
||||
)}
|
||||
<DialogContent className={cn("sm:max-w-[425px]", props.dialogClassName)}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AutoForm
|
||||
values={values}
|
||||
onValuesChange={setValues}
|
||||
fieldConfig={props.fieldConfig}
|
||||
formSchema={props.formSchema}
|
||||
onSubmit={async (data) => {
|
||||
setIsLoading(true);
|
||||
await callServerPromise(
|
||||
props.serverAction({
|
||||
...data,
|
||||
id: props.data.id,
|
||||
}),
|
||||
);
|
||||
setIsLoading(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-end flex-wrap gap-2">
|
||||
{props.extraButtons}
|
||||
<AutoFormSubmit>
|
||||
Save Changes
|
||||
{isLoading && <LoadingIcon />}
|
||||
</AutoFormSubmit>
|
||||
</div>
|
||||
</AutoForm>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<AutoForm
|
||||
values={values}
|
||||
onValuesChange={setValues}
|
||||
fieldConfig={props.fieldConfig}
|
||||
formSchema={props.formSchema}
|
||||
onSubmit={async (data) => {
|
||||
setIsLoading(true);
|
||||
await callServerPromise(
|
||||
props.serverAction({
|
||||
...data,
|
||||
id: props.data.id,
|
||||
}),
|
||||
);
|
||||
setIsLoading(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-end flex-wrap gap-2">
|
||||
{props.extraButtons}
|
||||
<AutoFormSubmit>
|
||||
Save Changes
|
||||
{isLoading && <LoadingIcon />}
|
||||
</AutoFormSubmit>
|
||||
</div>
|
||||
</AutoForm>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</AutoFormValueProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function LiveStatus({
|
||||
(state) =>
|
||||
state.data
|
||||
.filter((x) => x.id === run.id)
|
||||
.sort((a, b) => b.timestamp - a.timestamp)?.[0]
|
||||
.sort((a, b) => b.timestamp - a.timestamp)?.[0],
|
||||
);
|
||||
|
||||
let status = run.status;
|
||||
@@ -51,7 +51,9 @@ export function LiveStatus({
|
||||
<>
|
||||
<TableCell>
|
||||
{data && status != "success"
|
||||
? `${data.json.event} - ${data.json.data.node}`
|
||||
? `${data.json.event}${
|
||||
data.json.data.node ? " - " + data.json.data.node : ""
|
||||
}`
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell className="truncate text-right">
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export type LogsType = {
|
||||
machine_id?: string;
|
||||
logs: string;
|
||||
timestamp: number;
|
||||
timestamp?: number;
|
||||
}[];
|
||||
|
||||
export function LogsViewer({ logs }: { logs: LogsType }) {
|
||||
export function LogsViewer({
|
||||
logs,
|
||||
hideTimestamp,
|
||||
className,
|
||||
stickToBottom = true,
|
||||
}: {
|
||||
logs: LogsType;
|
||||
hideTimestamp?: boolean;
|
||||
className?: string;
|
||||
stickToBottom?: boolean;
|
||||
}) {
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stickToBottom) return;
|
||||
|
||||
// console.log(logs.length, container.current);
|
||||
if (container.current) {
|
||||
const scrollHeight = container.current.scrollHeight;
|
||||
@@ -21,11 +35,12 @@ export function LogsViewer({ logs }: { logs: LogsType }) {
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}, [logs.length]);
|
||||
}, [logs.length, stickToBottom]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(ref) => {
|
||||
if (!stickToBottom) return;
|
||||
if (!container.current && ref) {
|
||||
const scrollHeight = ref.scrollHeight;
|
||||
|
||||
@@ -36,10 +51,31 @@ export function LogsViewer({ logs }: { logs: LogsType }) {
|
||||
}
|
||||
container.current = ref;
|
||||
}}
|
||||
className="flex flex-col text-xs p-2 overflow-y-scroll max-h-[400px] whitespace-break-spaces"
|
||||
className={cn(
|
||||
"h-full w-full flex flex-col text-xs p-2 overflow-y-scroll whitespace-break-spaces",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{logs.map((x, i) => (
|
||||
<div key={i}>{x.logs}</div>
|
||||
<div
|
||||
key={i}
|
||||
className="hover:bg-gray-100 flex flex-row items-center gap-2"
|
||||
onClick={() => {
|
||||
toast.success("Copied to clipboard");
|
||||
navigator.clipboard.writeText(x.logs);
|
||||
}}
|
||||
>
|
||||
{!hideTimestamp && x.timestamp != undefined && (
|
||||
<>
|
||||
<span className="w-[150px] flex-shrink-0">
|
||||
{new Date(x.timestamp * 1000).toLocaleString()}
|
||||
</span>
|
||||
<div className="h-full w-[1px] bg-stone-400 flex-shrink-0"></div>
|
||||
</>
|
||||
)}
|
||||
{/* Display timestamp */}
|
||||
<div>{x.logs}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function MachineBuildLog({
|
||||
@@ -41,7 +41,7 @@ export function MachineBuildLog({
|
||||
reconnectAttempts: 20,
|
||||
reconnectInterval: 1000,
|
||||
queryParams: query,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const connectionStatus = getConnectionStatus(readyState);
|
||||
@@ -57,59 +57,70 @@ export function MachineBuildLog({
|
||||
setLogs((logs) => [...(logs ?? []), message.data]);
|
||||
} else if (message?.event === "FINISHED") {
|
||||
setFinished(true);
|
||||
setStatus(message.data.status)
|
||||
setStatus(message.data.status);
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
const router = useRouter()
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{connectionStatus}
|
||||
<LogsViewer logs={logs} />
|
||||
<LogsViewer logs={logs} className="h-full max-h-[600px]" />
|
||||
|
||||
<AlertDialog open={finished}>
|
||||
<AlertDialogContent>
|
||||
{
|
||||
status == "succuss" ? (
|
||||
<>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Machine Built</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Your machine is built, you can now integrate your API, or directly run to check this machines.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction onClick={() => {
|
||||
router.push("/workflows")
|
||||
}}>See Workflows</AlertDialogAction>
|
||||
<AlertDialogAction onClick={() => {
|
||||
router.push("/machines")
|
||||
}}>See All Machines</AlertDialogAction>
|
||||
</AlertDialogFooter></>
|
||||
) : (
|
||||
<>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Machine Failed</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Something went wrong with the machine build, please check the log.
|
||||
Possible cause could be conflits with custom nodes, build got stuck, timeout, or too many custom nodes installed.
|
||||
Please attempt a rebuild or remove some of the custom nodes.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>See logs</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => {
|
||||
router.push("/machines")
|
||||
}}>Back to machines</AlertDialogAction>
|
||||
</AlertDialogFooter></>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
{status == "succuss" ? (
|
||||
<>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Machine Built</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Your machine is built, you can now integrate your API, or
|
||||
directly run to check this machines.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
router.push("/workflows");
|
||||
}}
|
||||
>
|
||||
See Workflows
|
||||
</AlertDialogAction>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
router.push("/machines");
|
||||
}}
|
||||
>
|
||||
See All Machines
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Machine Failed</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Something went wrong with the machine build, please check the
|
||||
log. Possible cause could be conflits with custom nodes, build
|
||||
got stuck, timeout, or too many custom nodes installed. Please
|
||||
attempt a rebuild or remove some of the custom nodes.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>See logs</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
router.push("/machines");
|
||||
}}
|
||||
>
|
||||
Back to machines
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</>
|
||||
)}
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ type State = {
|
||||
json: {
|
||||
event: string;
|
||||
data: any;
|
||||
}
|
||||
},
|
||||
) => void;
|
||||
};
|
||||
|
||||
@@ -82,7 +82,7 @@ function MachineWS({
|
||||
const logs = useStore((x) =>
|
||||
x.logs
|
||||
.filter((p) => p.machine_id === machine.id)
|
||||
.sort((a, b) => a.timestamp - b.timestamp)
|
||||
.sort((a, b) => a.timestamp - b.timestamp),
|
||||
);
|
||||
const [sid, setSid] = useState("");
|
||||
|
||||
@@ -96,7 +96,7 @@ function MachineWS({
|
||||
// queryParams: {
|
||||
// clientId: sid,
|
||||
// },
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const connectionStatus = getConnectionStatus(readyState);
|
||||
@@ -135,7 +135,9 @@ function MachineWS({
|
||||
You can view your run's outputs here
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<LogsViewer logs={logs} />
|
||||
<div className="h-[400px]">
|
||||
<LogsViewer logs={logs} hideTimestamp />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,13 @@ import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { InsertModal } from "./InsertModal";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -30,9 +37,9 @@ import {
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { ArrowUpDown } from "lucide-react";
|
||||
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { addModel } from "@/server/curdModel";
|
||||
import { addModel, deleteModel } from "@/server/curdModel";
|
||||
import { downloadUrlModelSchema } from "@/server/addCivitaiModelSchema";
|
||||
import { modelEnumType } from "@/db/schema";
|
||||
|
||||
@@ -192,10 +199,16 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
||||
lora: "green",
|
||||
embedding: "violet",
|
||||
vae: "teal",
|
||||
clip: "default",
|
||||
clip_vision: "default",
|
||||
configs: "default",
|
||||
controlnet: "default",
|
||||
upscale_models: "default",
|
||||
ipadapter: "default",
|
||||
};
|
||||
|
||||
function getBadgeColor(modelType: modelEnumType) {
|
||||
return model_type_map[modelType] || "default";
|
||||
return model_type_map[modelType]
|
||||
}
|
||||
|
||||
const color = getBadgeColor(row.original.model_type);
|
||||
@@ -225,35 +238,35 @@ export const columns: ColumnDef<ModelItemList>[] = [
|
||||
),
|
||||
},
|
||||
// TODO: deletion and editing for future sprint
|
||||
// {
|
||||
// id: "actions",
|
||||
// enableHiding: false,
|
||||
// cell: ({ row }) => {
|
||||
// const checkpoint = row.original;
|
||||
//
|
||||
// return (
|
||||
// <DropdownMenu>
|
||||
// <DropdownMenuTrigger asChild>
|
||||
// <Button variant="ghost" className="h-8 w-8 p-0">
|
||||
// <span className="sr-only">Open menu</span>
|
||||
// <MoreHorizontal className="h-4 w-4" />
|
||||
// </Button>
|
||||
// </DropdownMenuTrigger>
|
||||
// <DropdownMenuContent align="end">
|
||||
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
// <DropdownMenuItem
|
||||
// className="text-destructive"
|
||||
// onClick={() => {
|
||||
// deleteWorkflow(checkpoint.id);
|
||||
// }}
|
||||
// >
|
||||
// Delete Workflow
|
||||
// </DropdownMenuItem>
|
||||
// </DropdownMenuContent>
|
||||
// </DropdownMenu>
|
||||
// );
|
||||
// },
|
||||
// },
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const model = row.original;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
className="text-destructive"
|
||||
onClick={() => {
|
||||
deleteModel(model.id);
|
||||
}}
|
||||
>
|
||||
Delete Model
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export function ModelList({ data }: { data: ModelItemList[] }) {
|
||||
|
||||
@@ -1,27 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import useSWR from "swr";
|
||||
import { DownloadButton } from "./DownloadButton";
|
||||
import { getFileDownloadUrl } from "@/server/getFileDownloadUrl";
|
||||
|
||||
export async function OutputRender(props: {
|
||||
export function OutputRender(props: {
|
||||
run_id: string;
|
||||
filename: string;
|
||||
}) {
|
||||
const { data: url } = useSWR(
|
||||
"run-outputs+" + props.run_id + props.filename,
|
||||
async () => {
|
||||
return await getFileDownloadUrl(
|
||||
`outputs/runs/${props.run_id}/${props.filename}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!url) return <></>;
|
||||
|
||||
if (props.filename.endsWith(".mp4") || props.filename.endsWith(".webm")) {
|
||||
return (
|
||||
<video controls autoPlay className="w-[400px]">
|
||||
<source src={url} type="video/mp4" />
|
||||
<source src={url} type="video/webm" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
props.filename.endsWith(".png") ||
|
||||
props.filename.endsWith(".gif") ||
|
||||
props.filename.endsWith(".jpg") ||
|
||||
props.filename.endsWith(".jpeg")
|
||||
) {
|
||||
const url = await getFileDownloadUrl(
|
||||
`outputs/runs/${props.run_id}/${props.filename}`
|
||||
);
|
||||
|
||||
return <img className="max-w-[200px]" alt={props.filename} src={url} />;
|
||||
} else {
|
||||
const url = await getFileDownloadUrl(
|
||||
`outputs/runs/${props.run_id}/${props.filename}`
|
||||
);
|
||||
// console.log(url);
|
||||
|
||||
return <DownloadButton filename={props.filename} href={url} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { RunInputs } from "@/components/RunInputs";
|
||||
import { RunOutputs } from "@/components/RunOutputs";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -17,10 +19,10 @@ import {
|
||||
} from "@/components/ui/tooltip";
|
||||
import { getDuration, getRelativeTime } from "@/lib/getRelativeTime";
|
||||
import { type findAllRuns } from "@/server/findAllRuns";
|
||||
import { Suspense } from "react";
|
||||
import { LiveStatus } from "./LiveStatus";
|
||||
import { LoadingWrapper } from "@/components/LoadingWrapper";
|
||||
|
||||
export async function RunDisplay({
|
||||
export function RunDisplay({
|
||||
run,
|
||||
}: {
|
||||
run: Awaited<ReturnType<typeof findAllRuns>>[0];
|
||||
@@ -73,9 +75,9 @@ export async function RunDisplay({
|
||||
</DialogHeader>
|
||||
<div className="max-h-96 overflow-y-scroll">
|
||||
<RunInputs run={run} />
|
||||
<Suspense>
|
||||
<RunOutputs run_id={run.id} />
|
||||
</Suspense>
|
||||
<LoadingWrapper tag="output">
|
||||
<RunOutputs run={run} />
|
||||
</LoadingWrapper>
|
||||
</div>
|
||||
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
||||
</DialogContent>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import type { findAllRuns } from "@/server/findAllRuns";
|
||||
|
||||
export async function RunInputs({
|
||||
export function RunInputs({
|
||||
run,
|
||||
}: {
|
||||
run: Awaited<ReturnType<typeof findAllRuns>>[0];
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { OutputRender } from "./OutputRender";
|
||||
import { CodeBlock } from "@/components/CodeBlock";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -8,10 +19,25 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { findAllRuns } from "@/server/findAllRuns";
|
||||
import { getRunsOutput } from "@/server/getRunsOutput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { LogsViewer } from "@/components/LogsViewer";
|
||||
import { CopyButton } from "@/components/CopyButton";
|
||||
import useSWR from "swr";
|
||||
import { CodeBlockClient } from "@/components/CodeBlockClient";
|
||||
|
||||
export function RunOutputs({
|
||||
run,
|
||||
}: { run: Awaited<ReturnType<typeof findAllRuns>>[0] }) {
|
||||
const { data, isValidating, error } = useSWR(
|
||||
"run-outputs+" + run.id,
|
||||
async () => {
|
||||
return await getRunsOutput(run.id);
|
||||
},
|
||||
);
|
||||
|
||||
export async function RunOutputs({ run_id }: { run_id: string }) {
|
||||
const outputs = await getRunsOutput(run_id);
|
||||
return (
|
||||
<Table className="table-fixed">
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
@@ -21,7 +47,43 @@ export async function RunOutputs({ run_id }: { run_id: string }) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{outputs?.map((run) => {
|
||||
<TableRow key={run.id}>
|
||||
<TableCell className="break-words">Run log</TableCell>
|
||||
<TableCell>
|
||||
{run.run_log ? (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="secondary" className="w-fit">
|
||||
View Log <ExternalLink size={14} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[1000px] h-full max-h-[600px] grid grid-rows-[auto,1fr,auto]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Run Log</DialogTitle>
|
||||
</DialogHeader>
|
||||
<LogsViewer logs={run.run_log} stickToBottom={false} />
|
||||
<DialogFooter>
|
||||
<CopyButton
|
||||
className="w-fit aspect-auto p-4"
|
||||
text={JSON.stringify(run.run_log)}
|
||||
>
|
||||
Copy
|
||||
</CopyButton>
|
||||
<DialogClose>
|
||||
<Button type="button" variant="secondary">
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : (
|
||||
"No log available"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
{data?.map((run) => {
|
||||
const fileName =
|
||||
run.data.images?.[0].filename ||
|
||||
run.data.files?.[0].filename ||
|
||||
@@ -32,7 +94,7 @@ export async function RunOutputs({ run_id }: { run_id: string }) {
|
||||
<TableRow key={run.id}>
|
||||
<TableCell>Output</TableCell>
|
||||
<TableCell className="">
|
||||
<CodeBlock
|
||||
<CodeBlockClient
|
||||
code={JSON.stringify(run.data, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
@@ -45,7 +107,7 @@ export async function RunOutputs({ run_id }: { run_id: string }) {
|
||||
<TableRow key={run.id}>
|
||||
<TableCell className="break-words">{fileName}</TableCell>
|
||||
<TableCell>
|
||||
<OutputRender run_id={run_id} filename={fileName} />
|
||||
<OutputRender run_id={run.run_id} filename={fileName} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,94 +9,79 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { parseAsInteger } from "next-usequerystate";
|
||||
import { headers } from "next/headers";
|
||||
import {
|
||||
findAllDeployments,
|
||||
findAllRunsWithCounts,
|
||||
findAllRunsWithCounts,
|
||||
getAllRunstableContent,
|
||||
} from "../server/findAllRuns";
|
||||
import { DeploymentDisplay } from "./DeploymentDisplay";
|
||||
import { PaginationControl } from "./PaginationControl";
|
||||
import { RunDisplay } from "./RunDisplay";
|
||||
import useSWR from "swr";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
|
||||
const itemPerPage = 6;
|
||||
const pageParser = parseAsInteger.withDefault(1);
|
||||
|
||||
export async function RunsTable(props: {
|
||||
export function RunsTable(props: {
|
||||
workflow_id: string;
|
||||
searchParams: { [key: string]: string | string[] | undefined };
|
||||
searchParams: { [key: string]: any };
|
||||
}) {
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
const page = pageParser.parseServerSide(
|
||||
props.searchParams?.page ?? undefined
|
||||
const page = pageParser.parse(props.searchParams?.page ?? undefined) ?? 1;
|
||||
const { data, error, isLoading, isValidating } = useSWR(
|
||||
"runs+" + page,
|
||||
async () => {
|
||||
const data = await findAllRunsWithCounts({
|
||||
workflow_id: props.workflow_id,
|
||||
limit: itemPerPage,
|
||||
offset: (page - 1) * itemPerPage,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
{
|
||||
// suspense: false,
|
||||
refreshInterval: 5000,
|
||||
},
|
||||
);
|
||||
const { allRuns, total } = await findAllRunsWithCounts({
|
||||
workflow_id: props.workflow_id,
|
||||
limit: itemPerPage,
|
||||
offset: (page - 1) * itemPerPage,
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<div className="overflow-auto h-fit w-full">
|
||||
<Table className="">
|
||||
{allRuns.length === 0 && (
|
||||
<TableCaption>A list of your recent runs.</TableCaption>
|
||||
)}
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
<TableRow>
|
||||
<TableHead className="truncate">Number</TableHead>
|
||||
<TableHead className="truncate">Machine</TableHead>
|
||||
<TableHead className="truncate">Time</TableHead>
|
||||
<TableHead className="truncate">Version</TableHead>
|
||||
<TableHead className="truncate">Origin</TableHead>
|
||||
<TableHead className="truncate">Duration</TableHead>
|
||||
<TableHead className="truncate">Live Status</TableHead>
|
||||
<TableHead className="text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allRuns.map((run) => (
|
||||
<RunDisplay run={run} key={run.id} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{Math.ceil(total / itemPerPage) > 0 && (
|
||||
<PaginationControl
|
||||
totalPage={Math.ceil(total / itemPerPage)}
|
||||
currentPage={page}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export async function DeploymentsTable(props: { workflow_id: string }) {
|
||||
const allRuns = await findAllDeployments(props.workflow_id);
|
||||
|
||||
const headersList = headers();
|
||||
const host = headersList.get("host") || "";
|
||||
const protocol = headersList.get("x-forwarded-proto") || "";
|
||||
const domain = `${protocol}://${host}`;
|
||||
// await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
|
||||
return (
|
||||
<div className="overflow-auto h-fit w-full">
|
||||
<Table className="">
|
||||
<TableCaption>A list of your deployments</TableCaption>
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
<TableRow>
|
||||
<TableHead className=" w-[100px]">Environment</TableHead>
|
||||
<TableHead className=" w-[100px]">Version</TableHead>
|
||||
<TableHead className="">Machine</TableHead>
|
||||
<TableHead className=" text-right">Updated At</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allRuns.map((run) => (
|
||||
<DeploymentDisplay deployment={run} key={run.id} domain={domain} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div>
|
||||
{isValidating ? (
|
||||
<div className="absolute right-8 top-8">
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="overflow-auto h-fit w-full">
|
||||
<Table className="">
|
||||
{/* {data?.allRuns.length === 0 && (
|
||||
<TableCaption>A list of your recent runs.</TableCaption>
|
||||
)} */}
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
<TableRow>
|
||||
<TableHead className="truncate">Number</TableHead>
|
||||
<TableHead className="truncate">Machine</TableHead>
|
||||
<TableHead className="truncate">Time</TableHead>
|
||||
<TableHead className="truncate">Version</TableHead>
|
||||
<TableHead className="truncate">Origin</TableHead>
|
||||
<TableHead className="truncate">Duration</TableHead>
|
||||
<TableHead className="truncate">Live Status</TableHead>
|
||||
<TableHead className="text-right">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.allRuns.map((run) => (
|
||||
<RunDisplay run={run} key={run.id} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{data && Math.ceil(data.total / itemPerPage) > 0 && (
|
||||
<PaginationControl
|
||||
totalPage={Math.ceil(data.total / itemPerPage)}
|
||||
currentPage={page}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ export function StatusBadge({
|
||||
);
|
||||
case "success":
|
||||
return <Badge variant="success">{status}</Badge>;
|
||||
case "timeout":
|
||||
return <Badge variant="amber">{status}</Badge>;
|
||||
case "failed":
|
||||
return <Badge variant="destructive">{status}</Badge>;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -28,36 +27,19 @@ import {
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { showcaseMediaNullable, workflowAPINodeType } from "@/db/schema";
|
||||
import type { showcaseMediaNullable } from "@/db/schema";
|
||||
import { checkStatus, createRun } from "@/server/createRun";
|
||||
import { createDeployments } from "@/server/curdDeploments";
|
||||
import type { getMachines } from "@/server/curdMachine";
|
||||
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
|
||||
import {
|
||||
Copy,
|
||||
Edit,
|
||||
ExternalLink,
|
||||
Info,
|
||||
MoreVertical,
|
||||
Play,
|
||||
} from "lucide-react";
|
||||
import { Copy, Edit, MoreVertical, Play } from "lucide-react";
|
||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import type { z } from "zod";
|
||||
import { create } from "zustand";
|
||||
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
|
||||
import { callServerPromise } from "./callServerPromise";
|
||||
import fetcher from "./fetcher";
|
||||
import { ButtonAction } from "@/components/ButtonActionLoader";
|
||||
import { editWorkflowOnMachine } from "@/server/editWorkflowOnMachine";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
@@ -125,29 +107,48 @@ export function MachineSelect({
|
||||
);
|
||||
}
|
||||
|
||||
type SelectedMachineStore = {
|
||||
selectedMachine: string | undefined;
|
||||
setSelectedMachine: (machine: string) => void;
|
||||
};
|
||||
|
||||
export const selectedMachineStore = create<SelectedMachineStore>((set) => ({
|
||||
selectedMachine: undefined,
|
||||
setSelectedMachine: (machine) => set(() => ({ selectedMachine: machine })),
|
||||
}));
|
||||
|
||||
export function useSelectedMachine(
|
||||
machines: Awaited<ReturnType<typeof getMachines>>,
|
||||
): [string, (v: string) => void] {
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { selectedMachine, setSelectedMachine } = selectedMachineStore();
|
||||
return [selectedMachine ?? machines?.[0]?.id ?? "", setSelectedMachine];
|
||||
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
// const searchParams = useSearchParams();
|
||||
// const pathname = usePathname();
|
||||
// const router = useRouter();
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams],
|
||||
);
|
||||
// const createQueryString = useCallback(
|
||||
// (name: string, value: string) => {
|
||||
// const params = new URLSearchParams(searchParams.toString());
|
||||
// params.set(name, value);
|
||||
|
||||
return [
|
||||
searchParams.get("machine") ?? machines?.[0]?.id ?? "",
|
||||
(v: string) => {
|
||||
router.push(pathname + "?" + createQueryString("machine", v));
|
||||
},
|
||||
];
|
||||
// return params.toString();
|
||||
// },
|
||||
// [searchParams],
|
||||
// );
|
||||
|
||||
// return [
|
||||
// searchParams.get("machine") ?? machines?.[0]?.id ?? "",
|
||||
// (v: string) => {
|
||||
// // window.history.pushState(
|
||||
// // "new url",
|
||||
// // "",
|
||||
// // pathname + "?" + createQueryString("machine", v),
|
||||
// // );
|
||||
// // router.push(pathname + "?" + createQueryString("machine", v));
|
||||
// router.replace(pathname + "?" + createQueryString("machine", v));
|
||||
// },
|
||||
// ];
|
||||
}
|
||||
|
||||
type PublicRunStore = {
|
||||
@@ -500,150 +501,3 @@ export function getWorkflowVersionFromVersionIndex(
|
||||
|
||||
return workflow_version;
|
||||
}
|
||||
|
||||
export function ViewWorkflowDetailsButton({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>;
|
||||
}) {
|
||||
const [version] = useQueryState("version", {
|
||||
defaultValue: workflow?.versions[0].version ?? 1,
|
||||
...parseAsInteger,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isLoading: isNodesIndexLoading,
|
||||
} = useSWR(
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json",
|
||||
fetcher,
|
||||
);
|
||||
|
||||
const groupedByAuxName = useMemo(() => {
|
||||
if (!data) return null;
|
||||
|
||||
// console.log(data);
|
||||
|
||||
const workflow_version = getWorkflowVersionFromVersionIndex(
|
||||
workflow,
|
||||
version,
|
||||
);
|
||||
|
||||
const api = workflow_version?.workflow_api;
|
||||
|
||||
if (!api) return null;
|
||||
|
||||
const crossCheckedApi = Object.entries(api)
|
||||
.map(([_, value]) => {
|
||||
const classType = value.class_type;
|
||||
const classTypeData = Object.entries(data).find(([_, nodeArray]) =>
|
||||
nodeArray[0].includes(classType),
|
||||
);
|
||||
return classTypeData ? { node: value, classTypeData } : null;
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
// console.log(crossCheckedApi);
|
||||
|
||||
const groupedByAuxName = crossCheckedApi.reduce(
|
||||
(acc, data) => {
|
||||
if (!data) return acc;
|
||||
|
||||
const { node, classTypeData } = data;
|
||||
const auxName = classTypeData[1][1].title_aux;
|
||||
// console.log(auxName);
|
||||
if (!acc[auxName]) {
|
||||
acc[auxName] = {
|
||||
url: classTypeData[0],
|
||||
node: [],
|
||||
};
|
||||
}
|
||||
acc[auxName].node.push(node);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
node: z.infer<typeof workflowAPINodeType>[];
|
||||
url: string;
|
||||
}
|
||||
>,
|
||||
);
|
||||
|
||||
// console.log(groupedByAuxName);
|
||||
|
||||
return groupedByAuxName;
|
||||
}, [version, data]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
||||
<Button className="gap-2" variant="outline">
|
||||
Details <Info size={14} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Workflow Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
View your custom nodes, models, external files used in this workflow
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-auto max-h-[400px] w-full">
|
||||
<Table>
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">File</TableHead>
|
||||
<TableHead className="">Output</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedByAuxName &&
|
||||
Object.entries(groupedByAuxName).map(([key, group]) => {
|
||||
// const filePath
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="break-words">
|
||||
<a
|
||||
href={group.url}
|
||||
target="_blank"
|
||||
className="hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{key}
|
||||
<ExternalLink
|
||||
className="inline-block ml-1"
|
||||
size={12}
|
||||
/>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="flex flex-wrap gap-2">
|
||||
{group.node.map((x) => (
|
||||
<Badge key={x.class_type} variant="outline">
|
||||
{x.class_type}
|
||||
</Badge>
|
||||
))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button className="w-fit" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"use client";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { workflowAPINodeType } from "@/db/schema";
|
||||
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
|
||||
import { ExternalLink, Info } from "lucide-react";
|
||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
||||
import { useMemo } from "react";
|
||||
import useSWR from "swr";
|
||||
import type { z } from "zod";
|
||||
import fetcher from "./fetcher";
|
||||
import { getWorkflowVersionFromVersionIndex } from "./VersionSelect";
|
||||
|
||||
export function ViewWorkflowDetailsButton({
|
||||
workflow,
|
||||
}: {
|
||||
workflow: Awaited<ReturnType<typeof findFirstTableWithVersion>>;
|
||||
}) {
|
||||
const [version] = useQueryState("version", {
|
||||
defaultValue: workflow?.versions[0].version ?? 1,
|
||||
...parseAsInteger,
|
||||
});
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
isLoading: isNodesIndexLoading,
|
||||
} = useSWR(
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json",
|
||||
fetcher,
|
||||
);
|
||||
|
||||
const groupedByAuxName = useMemo(() => {
|
||||
if (!data) return null;
|
||||
|
||||
const workflow_version = getWorkflowVersionFromVersionIndex(
|
||||
workflow,
|
||||
version,
|
||||
);
|
||||
|
||||
const api = workflow_version?.workflow_api;
|
||||
|
||||
if (!api) return null;
|
||||
|
||||
const crossCheckedApi = Object.entries(api)
|
||||
.map(([_, value]) => {
|
||||
const classType = value.class_type;
|
||||
const classTypeData = Object.entries(data).find(([_, nodeArray]) =>
|
||||
nodeArray[0].includes(classType),
|
||||
);
|
||||
return classTypeData ? { node: value, classTypeData } : null;
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
// console.log(crossCheckedApi);
|
||||
const groupedByAuxName = crossCheckedApi.reduce(
|
||||
(acc, data) => {
|
||||
if (!data) return acc;
|
||||
|
||||
const { node, classTypeData } = data;
|
||||
const auxName = classTypeData[1][1].title_aux;
|
||||
// console.log(auxName);
|
||||
if (!acc[auxName]) {
|
||||
acc[auxName] = {
|
||||
url: classTypeData[0],
|
||||
node: [],
|
||||
};
|
||||
}
|
||||
acc[auxName].node.push(node);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<
|
||||
string,
|
||||
{
|
||||
node: z.infer<typeof workflowAPINodeType>[];
|
||||
url: string;
|
||||
}
|
||||
>,
|
||||
);
|
||||
|
||||
// console.log(groupedByAuxName);
|
||||
return groupedByAuxName;
|
||||
}, [version, data]);
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
||||
<Button className="gap-2" variant="outline">
|
||||
Details <Info size={14} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Workflow Details</DialogTitle>
|
||||
<DialogDescription>
|
||||
View your custom nodes, models, external files used in this workflow
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="overflow-auto max-h-[400px] w-full">
|
||||
<Table>
|
||||
<TableHeader className="bg-background top-0 sticky">
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">File</TableHead>
|
||||
<TableHead className="">Output</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{groupedByAuxName &&
|
||||
Object.entries(groupedByAuxName).map(([key, group]) => {
|
||||
// const filePath
|
||||
return (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="break-words">
|
||||
<a
|
||||
href={group.url}
|
||||
target="_blank"
|
||||
className="hover:underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{key}
|
||||
<ExternalLink
|
||||
className="inline-block ml-1"
|
||||
size={12}
|
||||
/>
|
||||
</a>
|
||||
</TableCell>
|
||||
<TableCell className="flex flex-wrap gap-2">
|
||||
{group.node.map((x) => (
|
||||
<Badge key={x.class_type} variant="outline">
|
||||
{x.class_type}
|
||||
</Badge>
|
||||
))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<DialogClose asChild>
|
||||
<Button className="w-fit">Close</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
{/* </div> */}
|
||||
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -53,9 +53,8 @@ import {
|
||||
Plus,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import { z } from "zod";
|
||||
import { getBranchInfo } from "./getBranchInfo";
|
||||
|
||||
export function SnapshotPickerView({
|
||||
field,
|
||||
@@ -131,7 +130,7 @@ export function SnapshotPickerView({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
disabled={key.endsWith("comfyui-deploy.git")}
|
||||
disabled={key.endsWith("comfyui-deploy")}
|
||||
// className="opacity-50"
|
||||
onClick={() => {
|
||||
const newNodeList = {
|
||||
@@ -148,6 +147,29 @@ export function SnapshotPickerView({
|
||||
>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
// className="opacity-50"
|
||||
onClick={async () => {
|
||||
const newNodeList = {
|
||||
...field.value.git_custom_nodes,
|
||||
};
|
||||
|
||||
const branchInfo = await getBranchInfo(key);
|
||||
|
||||
if (!branchInfo) return;
|
||||
|
||||
newNodeList[key].hash = branchInfo?.commit.sha;
|
||||
|
||||
const nodeList = newNodeList;
|
||||
const newValue = {
|
||||
...field.value,
|
||||
git_custom_nodes: nodeList,
|
||||
};
|
||||
field.onChange(newValue);
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Card>
|
||||
@@ -268,24 +290,6 @@ type CustomNodeList = {
|
||||
}[];
|
||||
};
|
||||
|
||||
const RepoSchema = z.object({
|
||||
default_branch: z.string(),
|
||||
});
|
||||
|
||||
const BranchInfoSchema = z.object({
|
||||
commit: z.object({
|
||||
sha: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
function extractRepoName(repoUrl: string) {
|
||||
const url = new URL(repoUrl);
|
||||
const pathParts = url.pathname.split("/");
|
||||
const repoName = pathParts[2].replace(".git", "");
|
||||
const author = pathParts[1];
|
||||
return `${author}/${repoName}`;
|
||||
}
|
||||
|
||||
function CustomNodesSelector({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
@@ -362,43 +366,7 @@ function CustomNodesSelector({
|
||||
delete newNodeList[currentValue];
|
||||
nodeList = newNodeList;
|
||||
} else {
|
||||
const repoName = extractRepoName(currentValue);
|
||||
const id = toast.loading(`Fetching repo info...`);
|
||||
const repo = await fetch(
|
||||
`https://api.github.com/repos/${repoName}`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
console.log(x);
|
||||
return x;
|
||||
})
|
||||
.then((x) => RepoSchema.parse(x))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error(
|
||||
`Failed to fetch repo info ${e.message}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!repo) return;
|
||||
const branch = repo.default_branch;
|
||||
const branchInfo = await fetch(
|
||||
`https://api.github.com/repos/${repoName}/branches/${branch}`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => BranchInfoSchema.parse(x))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error(
|
||||
`Failed to fetch branch info ${e.message}`,
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
toast.dismiss(id);
|
||||
const branchInfo = await getBranchInfo(currentValue);
|
||||
|
||||
if (!branchInfo) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const RepoSchema = z.object({
|
||||
default_branch: z.string(),
|
||||
});
|
||||
const BranchInfoSchema = z.object({
|
||||
commit: z.object({
|
||||
sha: z.string(),
|
||||
}),
|
||||
});
|
||||
function extractRepoName(repoUrl: string) {
|
||||
const url = new URL(repoUrl);
|
||||
const pathParts = url.pathname.split("/");
|
||||
const repoName = pathParts[2].replace(".git", "");
|
||||
const author = pathParts[1];
|
||||
return `${author}/${repoName}`;
|
||||
}
|
||||
export async function getBranchInfo(gitUrl: string) {
|
||||
const repoName = extractRepoName(gitUrl);
|
||||
const id = toast.loading(`Fetching repo info...`);
|
||||
const repo = await fetch(`https://api.github.com/repos/${repoName}`)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
console.log(x);
|
||||
return x;
|
||||
})
|
||||
.then((x) => RepoSchema.parse(x))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error(`Failed to fetch repo info ${e.message}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!repo) return;
|
||||
const branch = repo.default_branch;
|
||||
const branchInfo = await fetch(
|
||||
`https://api.github.com/repos/${repoName}/branches/${branch}`,
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => BranchInfoSchema.parse(x))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
toast.dismiss(id);
|
||||
toast.error(`Failed to fetch branch info ${e.message}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
toast.dismiss(id);
|
||||
return branchInfo;
|
||||
}
|
||||
+18
-7
@@ -1,3 +1,4 @@
|
||||
import { LogsType } from "@/components/LogsViewer";
|
||||
import { CivitaiModelResponse } from "@/types/civitai";
|
||||
import { type InferSelectModel, relations } from "drizzle-orm";
|
||||
import {
|
||||
@@ -6,10 +7,10 @@ import {
|
||||
jsonb,
|
||||
pgEnum,
|
||||
pgSchema,
|
||||
real,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
real,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { TypeOf, z } from "zod";
|
||||
@@ -104,6 +105,7 @@ export const workflowRunStatus = pgEnum("workflow_run_status", [
|
||||
"failed",
|
||||
"started",
|
||||
"queued",
|
||||
"timeout",
|
||||
]);
|
||||
|
||||
export const deploymentEnvironment = pgEnum("deployment_environment", [
|
||||
@@ -148,8 +150,9 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
|
||||
onDelete: "set null",
|
||||
},
|
||||
),
|
||||
workflow_inputs:
|
||||
jsonb("workflow_inputs").$type<Record<string, string | number>>(),
|
||||
workflow_inputs: jsonb("workflow_inputs").$type<
|
||||
Record<string, string | number>
|
||||
>(),
|
||||
workflow_id: uuid("workflow_id")
|
||||
.notNull()
|
||||
.references(() => workflowTable.id, {
|
||||
@@ -172,6 +175,7 @@ export const workflowRunsTable = dbSchema.table("workflow_runs", {
|
||||
machine_type: machinesType("machine_type"),
|
||||
user_id: text("user_id"),
|
||||
org_id: text("org_id"),
|
||||
run_log: jsonb("run_log").$type<LogsType>(),
|
||||
});
|
||||
|
||||
export const workflowRunRelations = relations(
|
||||
@@ -295,8 +299,9 @@ export const deploymentsTable = dbSchema.table("deployments", {
|
||||
.references(() => machinesTable.id),
|
||||
share_slug: text("share_slug").unique(),
|
||||
description: text("description"),
|
||||
showcase_media:
|
||||
jsonb("showcase_media").$type<z.infer<typeof showcaseMedia>>(),
|
||||
showcase_media: jsonb("showcase_media").$type<
|
||||
z.infer<typeof showcaseMedia>
|
||||
>(),
|
||||
environment: deploymentEnvironment("environment").notNull(),
|
||||
created_at: timestamp("created_at").defaultNow().notNull(),
|
||||
updated_at: timestamp("updated_at").defaultNow().notNull(),
|
||||
@@ -385,13 +390,19 @@ export const modelUploadType = pgEnum("model_upload_type", [
|
||||
"other",
|
||||
]);
|
||||
|
||||
// https://www.answeroverflow.com/m/1125106227387584552
|
||||
// https://www.answeroverflow.com/m/1125106227387584552
|
||||
export const modelTypes = [
|
||||
"checkpoint",
|
||||
"lora",
|
||||
"embedding",
|
||||
"vae",
|
||||
] as const
|
||||
"clip",
|
||||
"clip_vision",
|
||||
"configs",
|
||||
"controlnet",
|
||||
"upscale_models",
|
||||
"ipadapter",
|
||||
] as const;
|
||||
export const modelType = pgEnum("model_type", modelTypes);
|
||||
export type modelEnumType = (typeof modelTypes)[number];
|
||||
|
||||
|
||||
@@ -31,6 +31,16 @@ const getOutputRoute = createRoute({
|
||||
input_image: "https://somestatic.png",
|
||||
},
|
||||
}),
|
||||
run_log: (schema) =>
|
||||
schema.run_log.openapi({
|
||||
type: "object",
|
||||
example: [
|
||||
{
|
||||
logs: "some logs",
|
||||
timestamp: 1706631877.3831277,
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
@@ -81,7 +91,7 @@ export const registerGetOutputRoute = (app: App) => {
|
||||
code: 400,
|
||||
message: "Workflow not found",
|
||||
},
|
||||
400
|
||||
400,
|
||||
);
|
||||
|
||||
return c.json(run, 200);
|
||||
@@ -94,7 +104,7 @@ export const registerGetOutputRoute = (app: App) => {
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -16,26 +16,14 @@ export const insertCustomMachineSchema = createInsertSchema(machinesTable, {
|
||||
schema.snapshot.default({
|
||||
comfyui: "d0165d819afe76bd4e6bdd710eb5f3e571b6a804",
|
||||
git_custom_nodes: {
|
||||
"https://github.com/BennyKok/comfyui-deploy.git": {
|
||||
hash: "a838cb7ad425e5652c3931fbafdc886b53c48a22",
|
||||
"https://github.com/bennykok/comfyui-deploy": {
|
||||
hash: "df46e3a0e5ad93fa71f5d216997e376af33b2a6d",
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
file_custom_nodes: [],
|
||||
}),
|
||||
models: (schema) =>
|
||||
schema.models.default([
|
||||
{
|
||||
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",
|
||||
},
|
||||
]),
|
||||
models: (schema) => schema.models.default([]),
|
||||
});
|
||||
|
||||
export const addCustomMachineSchema = insertCustomMachineSchema.pick({
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { withServerPromise } from "./withServerPromise";
|
||||
import { db } from "@/db/db";
|
||||
import type { z } from "zod";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { headers } from "next/headers";
|
||||
import { downloadUrlModelSchema } from "./addCivitaiModelSchema";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
@@ -210,6 +211,47 @@ export const addModelDownloadUrl = withServerPromise(
|
||||
},
|
||||
);
|
||||
|
||||
export const deleteModel = withServerPromise(
|
||||
async (modelId: string) => {
|
||||
const model = await db.query.modelTable.findFirst({
|
||||
where: eq(modelTable.id, modelId),
|
||||
});
|
||||
|
||||
// If the model does not exist, throw an error or return a message
|
||||
if (!model) {
|
||||
throw new Error("Model not found");
|
||||
// Or return { error: "Model not found" }; if you prefer to handle it without throwing
|
||||
}
|
||||
|
||||
const volumes = await retrieveModelVolumes();
|
||||
if (
|
||||
model.status === "success" && !!model.folder_path && !!model.model_name
|
||||
) {
|
||||
const result = await fetch(
|
||||
`${process.env.MODAL_BUILDER_URL!}/delete-volume-model`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
volume_name: volumes[0].volume_name,
|
||||
path: model.folder_path,
|
||||
file_name: model.model_name,
|
||||
}),
|
||||
},
|
||||
);
|
||||
if (!result.ok) {
|
||||
const error_log = await result.text();
|
||||
throw new Error(`Error: ${result.statusText} ${error_log}`);
|
||||
}
|
||||
}
|
||||
await db.delete(modelTable).where(eq(modelTable.id, modelId));
|
||||
revalidatePath("/storage");
|
||||
return { message: "Model Deleted" };
|
||||
},
|
||||
);
|
||||
|
||||
export const getCivitaiModelRes = async (civitaiUrl: string) => {
|
||||
const { url, modelVersionId } = getUrl(civitaiUrl);
|
||||
const civitaiModelRes = await fetch(url)
|
||||
@@ -301,8 +343,8 @@ export const addCivitaiModel = withServerPromise(
|
||||
model_name: selectedModelVersion.files[0].name,
|
||||
civitai_id: civitaiModelRes.id.toString(),
|
||||
civitai_version_id: selectedModelVersionId,
|
||||
civitai_url: data.url, // TODO: need to confirm
|
||||
civitai_download_url: selectedModelVersion.files[0].downloadUrl,
|
||||
civitai_url: data.url,
|
||||
civitai_download_url: selectedModelVersion.files[0].downloadUrl, // there is an issue when a model hoster might put multiple different types of files i.e. their training data.
|
||||
civitai_model_response: civitaiModelRes,
|
||||
user_volume_id: volumes[0].id,
|
||||
model_type,
|
||||
@@ -312,6 +354,7 @@ export const addCivitaiModel = withServerPromise(
|
||||
const b = a[0];
|
||||
|
||||
await uploadModel(data, b, volumes[0]);
|
||||
revalidatePath("/storage");
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { RunDisplay } from "@/components/RunDisplay";
|
||||
import { db } from "@/db/db";
|
||||
import { deploymentsTable, workflowRunsTable } from "@/db/schema";
|
||||
import { count, desc, eq, sql } from "drizzle-orm";
|
||||
@@ -56,6 +57,15 @@ export async function findAllRuns({
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllRunstableContent(props: RunsSearchTypes) {
|
||||
const data = await findAllRunsWithCounts(props);
|
||||
|
||||
return {
|
||||
table: data?.allRuns.map((run) => <RunDisplay run={run} key={run.id} />),
|
||||
total: data?.total,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findAllRunsWithCounts(props: RunsSearchTypes) {
|
||||
const a = await db
|
||||
.select({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { db } from "@/db/db";
|
||||
import { and, desc, eq, isNull, or } from "drizzle-orm";
|
||||
import { and, desc, eq, isNull, ne, or } from "drizzle-orm";
|
||||
import { subscriptionStatusTable } from "@/db/schema";
|
||||
import { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
@@ -28,6 +28,7 @@ export async function getCurrentPlan({ user_id, org_id }: APIKeyUserType) {
|
||||
isNull(subscriptionStatusTable.org_id),
|
||||
eq(subscriptionStatusTable.org_id, ""),
|
||||
),
|
||||
ne(subscriptionStatusTable.status, "deleted"),
|
||||
),
|
||||
orderBy: desc(subscriptionStatusTable.created_at),
|
||||
});
|
||||
|
||||
@@ -49,24 +49,26 @@ export async function getRunsData(run_id: string, user?: APIKeyUserType) {
|
||||
for (let i = 0; i < data.outputs.length; i++) {
|
||||
const output = data.outputs[i];
|
||||
|
||||
if (output.data?.images !== undefined) {
|
||||
for (let j = 0; j < output.data?.images.length; j++) {
|
||||
const element = output.data?.images[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
||||
);
|
||||
}
|
||||
} else if (output.data?.files !== undefined) {
|
||||
for (let j = 0; j < output.data?.files.length; j++) {
|
||||
const element = output.data?.files[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (output.data?.images !== undefined)
|
||||
replaceUrls(output.data?.images, data.id);
|
||||
|
||||
if (output.data?.files !== undefined)
|
||||
replaceUrls(output.data?.files, data.id);
|
||||
|
||||
if (output.data?.gifs !== undefined)
|
||||
replaceUrls(output.data?.gifs, data.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function replaceUrls(dataType: any[], dataId: string) {
|
||||
for (let j = 0; j < dataType.length; j++) {
|
||||
const element = dataType[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${dataId}/${element.filename}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
"use server";
|
||||
|
||||
import { RunOutputs } from "@/components/RunOutputs";
|
||||
import { db } from "@/db/db";
|
||||
import { workflowRunOutputs } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function getRunsOutputDisplay(run_id: string) {
|
||||
return <RunOutputs run_id={run_id} />;
|
||||
}
|
||||
|
||||
export async function getRunsOutput(run_id: string) {
|
||||
// throw new Error("Not implemented");
|
||||
return await db
|
||||
.select()
|
||||
.from(workflowRunOutputs)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use server";
|
||||
|
||||
import type { StringLiteralUnion } from "shikiji";
|
||||
import { getHighlighter } from "shikiji";
|
||||
|
||||
export async function highlight(
|
||||
code: string,
|
||||
lang: StringLiteralUnion<string>,
|
||||
) {
|
||||
const highlighter = await getHighlighter({
|
||||
themes: ["one-dark-pro"],
|
||||
langs: [lang],
|
||||
});
|
||||
return highlighter.codeToHtml(code.trim(), {
|
||||
lang: lang,
|
||||
theme: "one-dark-pro",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user