diff --git a/web-plugin/index.js b/web-plugin/index.js index 47e71d3..c960293 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -9,6 +9,62 @@ const ext = { init(app) { addButton(); + + const queryParams = new URLSearchParams(window.location.search); + const workflow_version_id = queryParams.get("workflow_version_id"); + const auth_token = queryParams.get("auth_token"); + const org_display = queryParams.get("org_display"); + const origin = queryParams.get("origin"); + if (!workflow_version_id) { + console.error("No workflow_version_id provided in query parameters."); + } else { + const data = getData(); + let endpoint = data.endpoint; + let apiKey = data.apiKey; + + // If there is auth token override it + if (auth_token) { + apiKey = auth_token; + endpoint = origin; + saveData({ + displayName: org_display, + endpoint: origin, + apiKey: auth_token, + displayName: org_display, + }); + } + + loadingDialog.showLoading( + "Loading workflow from " + org_display, + "Please wait...", + ); + fetch(endpoint + "/api/workflow-version/" + workflow_version_id, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + apiKey, + }, + }) + .then(async (res) => { + const data = await res.json(); + const { workflow, error } = data; + if (error) { + infoDialog.showMessage("Unable to load this workflow", error); + return; + } + /** @type {LGraph} */ + app.loadGraphData(workflow); + }) + .catch((e) => infoDialog.showMessage("Error", e.message)) + .finally(() => { + loadingDialog.close(); + window.history.replaceState( + {}, + document.title, + window.location.pathname, + ); + }); + } }, registerCustomNodes() { @@ -191,7 +247,7 @@ function addButton() { endpoint = endpoint.slice(0, -1); } - const apiRoute = endpoint + "/api/upload"; + const apiRoute = endpoint + "/api/workflow"; // const userId = apiKey try { const body = { @@ -280,14 +336,16 @@ export class InfoDialog extends ComfyDialog { this.element.classList.add("comfy-normal-modal"); this.element.style.paddingBottom = "20px"; } + + button = undefined; + createButtons() { - return [ - $el("button", { - type: "button", - textContent: "Close", - onclick: () => this.close(), - }), - ]; + this.button = $el("button", { + type: "button", + textContent: "Close", + onclick: () => this.close(), + }); + return [this.button]; } close() { @@ -317,6 +375,58 @@ export class InfoDialog extends ComfyDialog { `); } + + loadingIcon = ``; + + showLoading(title, message) { + this.show(` +
+

${title} ${this.loadingIcon}

+ +
+ `); + } +} + +export class LoadingDialog extends ComfyDialog { + constructor() { + super(); + this.element.classList.add("comfy-normal-modal"); + // this.element.style.paddingBottom = "20px"; + } + + createButtons() { + return []; + } + + close() { + this.element.style.display = "none"; + } + + show(html) { + this.textElement.style["white-space"] = "normal"; + this.textElement.style.color = "white"; + this.textElement.style.marginTop = "0px"; + if (typeof html === "string") { + this.textElement.innerHTML = html; + } else { + this.textElement.replaceChildren(html); + } + this.element.style.display = "flex"; + this.element.style.zIndex = 1001; + } + + loadingIcon = ``; + + showLoading(title, message) { + this.show(` +
+

${title} ${this.loadingIcon}

+
+ `); + } } export class InputDialog extends InfoDialog { @@ -444,9 +554,15 @@ export class ConfirmDialog extends InfoDialog { } export const inputDialog = new InputDialog(); +export const loadingDialog = new LoadingDialog(); export const infoDialog = new InfoDialog(); export const confirmDialog = new ConfirmDialog(); +/** + * Retrieves deployment data from local storage or defaults. + * @param {string} [environment] - The environment to get the data for. + * @returns {{endpoint: string, apiKey: string, displayName: string, environment?: string}} The deployment data. + */ function getData(environment) { const deployOption = environment || localStorage.getItem("comfy_deploy_env") || "cloud"; @@ -469,6 +585,17 @@ function getData(environment) { }; } +/** + * Retrieves deployment data from local storage or defaults. + * @param {{endpoint: string, apiKey: string, displayName: string, environment?: string}} [data] - The environment to get the data for. + */ +function saveData(data) { + localStorage.setItem( + "comfy_deploy_env_data_" + data.environment, + JSON.stringify(data), + ); +} + export class ConfigDialog extends ComfyDialog { container = null; poll = null; @@ -527,15 +654,12 @@ export class ConfigDialog extends ComfyDialog { const endpoint = this.container.querySelector("#endpoint").value; const apiKey = api_key ?? this.container.querySelector("#apiKey").value; - const data = { + saveData({ endpoint, apiKey, displayName, - }; - localStorage.setItem( - "comfy_deploy_env_data_" + deployOption, - JSON.stringify(data), - ); + environment: deployOption, + }); this.close(); } diff --git a/web/src/app/(app)/api/[[...routes]]/route.ts b/web/src/app/(app)/api/[[...routes]]/route.ts index e8c7cdd..8e27050 100644 --- a/web/src/app/(app)/api/[[...routes]]/route.ts +++ b/web/src/app/(app)/api/[[...routes]]/route.ts @@ -8,7 +8,8 @@ import { handle } from "hono/vercel"; import { app } from "../../../../routes/app"; import { registerWorkflowUploadRoute } from "@/routes/registerWorkflowUploadRoute"; import { registerGetAuthResponse } from "@/routes/registerGetAuthResponse"; - +import { registerGetWorkflowRoute } from "@/routes/registerGetWorkflow"; +import { cors } from "hono/cors"; export const dynamic = "force-dynamic"; export const maxDuration = 300; // 5 minutes @@ -18,17 +19,24 @@ declare module "hono" { } } -async function checkAuth(c: Context, next: Next) { +async function checkAuth(c: Context, next: Next, headers?: HeadersInit) { const token = c.req.raw.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token" const userData = token ? parseJWT(token) : undefined; if (!userData || token === undefined) { - return c.text("Invalid or expired token", 401); + return c.text("Invalid or expired token", { + status: 401, + headers: headers, + }); } // If the key has expiration, this is a temporary key and not in our db, so we can skip checking if (userData.exp === undefined) { const revokedKey = await isKeyRevoked(token); - if (revokedKey) return c.text("Revoked token", 401); + if (revokedKey) + return c.text("Revoked token", { + status: 401, + headers: headers, + }); } c.set("apiKeyTokenData", userData); @@ -36,9 +44,29 @@ async function checkAuth(c: Context, next: Next) { await next(); } +async function checkAuthCORS(c: Context, next: Next) { + return checkAuth(c, next, { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", + }); +} + app.use("/run", checkAuth); app.use("/upload-url", checkAuth); -app.use("/upload-workflow", checkAuth); + +const corsHandler = cors({ + origin: "*", + allowHeaders: ["Authorization", "Content-Type"], + allowMethods: ["POST", "GET", "OPTIONS"], + exposeHeaders: ["Content-Length"], + maxAge: 600, + credentials: true, +}); + +// CORS Check +app.use("/workflow", corsHandler, checkAuth); +app.use("/workflow-version/*", corsHandler, checkAuth); // create run endpoint registerCreateRunRoute(app); @@ -47,9 +75,12 @@ registerGetOutputRoute(app); // file upload endpoint registerUploadRoute(app); -registerWorkflowUploadRoute(app); +// Anon registerGetAuthResponse(app); +registerWorkflowUploadRoute(app); +registerGetWorkflowRoute(app); + // The OpenAPI documentation will be available at /doc app.doc("/doc", { openapi: "3.0.0", @@ -76,3 +107,4 @@ const handler = handle(app); export const GET = handler; export const POST = handler; +export const OPTIONS = handler; diff --git a/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx b/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx index 620b2b4..778d4b2 100644 --- a/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx +++ b/web/src/app/(app)/workflows/[workflow_id]/@workflow/page.tsx @@ -2,12 +2,13 @@ import { CreateShareButton } from "@/components/CreateShareButton"; import { MachinesWSMain } from "@/components/MachinesWS"; import { VersionDetails } from "@/components/VersionDetails"; import { - CopyWorkflowVersion, - CreateDeploymentButton, - MachineSelect, - RunWorkflowButton, - VersionSelect, - ViewWorkflowDetailsButton, + CopyWorkflowVersion, + CreateDeploymentButton, + MachineSelect, + OpenEditButton, + RunWorkflowButton, + VersionSelect, + ViewWorkflowDetailsButton, } from "@/components/VersionSelect"; import { Card, @@ -48,6 +49,7 @@ export default async function Page({ + diff --git a/web/src/app/(app)/workflows/page.tsx b/web/src/app/(app)/workflows/page.tsx index d1499d2..afdd310 100644 --- a/web/src/app/(app)/workflows/page.tsx +++ b/web/src/app/(app)/workflows/page.tsx @@ -1,5 +1,5 @@ import { setInitialUserData } from "../../../lib/setInitialUserData"; -import { getAllUserWorkflow } from "../../../server/getAllUserWorkflow"; +import { getAllUserWorkflow } from "../../../server/crudWorkflow"; import { WorkflowList } from "@/components/WorkflowList"; import { db } from "@/db/db"; import { usersTable } from "@/db/schema"; diff --git a/web/src/components/DeploymentDisplay.tsx b/web/src/components/DeploymentDisplay.tsx index 87be9a2..257545f 100644 --- a/web/src/components/DeploymentDisplay.tsx +++ b/web/src/components/DeploymentDisplay.tsx @@ -1,11 +1,11 @@ import { CodeBlock } from "@/components/CodeBlock"; import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { TableRow } from "@/components/ui/table"; @@ -83,145 +83,145 @@ const run = await client.getRun(run_id); `; export function DeploymentDisplay({ - deployment, - domain, + deployment, + domain, }: { - deployment: Awaited>[0]; - domain: string; + deployment: Awaited>[0]; + domain: string; }) { - const workflowInput = getInputsFromWorkflow(deployment.version); + const workflowInput = getInputsFromWorkflow(deployment.version); - if (deployment.environment === "public-share") { - return ; - } + if (deployment.environment === "public-share") { + return ; + } - return ( - - - - - - - - - - {deployment.environment} Deployment - - Code for your deployment client - - - - - Server Client - NodeJS Fetch - CURL - - -
- Copy and paste the ComfyDeployClient form  - - here - -
- - Create a run via deployment id - 0 - ? jsClientCreateRunTemplate - : jsClientCreateRunNoInputsTemplate, - deployment, - domain, - workflowInput, - )} - /> - Check the status of the run, and retrieve the outputs - -
- - Trigger the workflow - - Check the status of the run, and retrieve the outputs - - - - - - -
-
-
-
- ); + return ( + + + + + + + + + + {deployment.environment} Deployment + + Code for your deployment client + + + + + Server Client + NodeJS Fetch + CURL + + +
+ Copy and paste the ComfyDeployClient form  + + here + +
+ + Create a run via deployment id + 0 + ? jsClientCreateRunTemplate + : jsClientCreateRunNoInputsTemplate, + deployment, + domain, + workflowInput, + )} + /> + Check the status of the run, and retrieve the outputs + +
+ + Trigger the workflow + + Check the status of the run, and retrieve the outputs + + + + + + +
+
+
+
+ ); } function formatCode( - codeTemplate: string, - deployment: Awaited>[0], - domain: string, - inputs?: ReturnType, - inputsTabs?: number, + codeTemplate: string, + deployment: Awaited>[0], + domain: string, + inputs?: ReturnType, + inputsTabs?: number, ) { - 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")}`, - ); - } else { - codeTemplate = codeTemplate.replace( - ` + 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")}`, + ); + } else { + codeTemplate = codeTemplate.replace( + ` inputs: {}`, - "", - ); - } - return codeTemplate - .replace("", `${domain ?? "http://localhost:3000"}/api/run`) - .replace("", deployment.id) - .replace("", domain ?? "http://localhost:3000"); + "", + ); + } + return codeTemplate + .replace("", `${domain ?? "http://localhost:3000"}/api/run`) + .replace("", deployment.id) + .replace("", domain ?? "http://localhost:3000"); } diff --git a/web/src/components/VersionSelect.tsx b/web/src/components/VersionSelect.tsx index 96edf18..1ac739b 100644 --- a/web/src/components/VersionSelect.tsx +++ b/web/src/components/VersionSelect.tsx @@ -41,7 +41,14 @@ 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, ExternalLink, Info, MoreVertical, Play } from "lucide-react"; +import { + Copy, + Edit, + ExternalLink, + Info, + MoreVertical, + Play, +} from "lucide-react"; import { parseAsInteger, useQueryState } from "next-usequerystate"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; @@ -51,6 +58,8 @@ 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"; export function VersionSelect({ workflow, @@ -116,13 +125,13 @@ export function MachineSelect({ } export function useSelectedMachine( - machines: Awaited>, + machines: Awaited>, ) { - const a = useQueryState("machine", { - defaultValue: machines?.[0]?.id ?? "", - }); + const a = useQueryState("machine", { + defaultValue: machines?.[0]?.id ?? "", + }); - return a; + return a; } type PublicRunStore = { @@ -219,7 +228,7 @@ export function RunWorkflowButton({ const schema = useMemo(() => { const workflow_version = getWorkflowVersionFromVersionIndex( workflow, - version + version, ); if (!workflow_version) return null; @@ -233,7 +242,7 @@ export function RunWorkflowButton({ const val = Object.keys(values).length > 0 ? values : undefined; const workflow_version_id = workflow?.versions.find( - (x) => x.version === version + (x) => x.version === version, )?.id; console.log(workflow_version_id); if (!workflow_version_id) return; @@ -248,7 +257,7 @@ export function RunWorkflowButton({ machine_id: machine, inputs: val, runOrigin: "manual", - }) + }), ); // console.log(res.json()); setIsLoading(false); @@ -317,7 +326,7 @@ export function CreateDeploymentButton({ const [isLoading, setIsLoading] = useState(false); const workflow_version_id = workflow?.versions.find( - (x) => x.version === version + (x) => x.version === version, )?.id; return ( @@ -337,8 +346,8 @@ export function CreateDeploymentButton({ workflow.id, workflow_version_id, machine, - "production" - ) + "production", + ), ); setIsLoading(false); }} @@ -355,8 +364,8 @@ export function CreateDeploymentButton({ workflow.id, workflow_version_id, machine, - "staging" - ) + "staging", + ), ); setIsLoading(false); }} @@ -368,6 +377,49 @@ export function CreateDeploymentButton({ ); } +export function OpenEditButton({ + workflow, + machines, +}: { + workflow: Awaited>; + machines: Awaited>; +}) { + const [version] = useQueryState("version", { + defaultValue: workflow?.versions[0].version ?? 1, + ...parseAsInteger, + }); + const [machine] = useSelectedMachine(machines); + const workflow_version_id = workflow?.versions.find( + (x) => x.version == version, + )?.id; + const [isLoading, setIsLoading] = useState(false); + + return ( + workflow_version_id && + machine && ( + + ) + ); +} + export function CopyWorkflowVersion({ workflow, }: { @@ -378,7 +430,7 @@ export function CopyWorkflowVersion({ ...parseAsInteger, }); const workflow_version = workflow?.versions.find( - (x) => x.version === version + (x) => x.version === version, ); return ( @@ -402,7 +454,7 @@ export function CopyWorkflowVersion({ }); navigator.clipboard.writeText( - JSON.stringify(workflow_version?.workflow) + JSON.stringify(workflow_version?.workflow), ); toast("Copied to clipboard"); }} @@ -412,7 +464,7 @@ export function CopyWorkflowVersion({ { navigator.clipboard.writeText( - JSON.stringify(workflow_version?.workflow_api) + JSON.stringify(workflow_version?.workflow_api), ); toast("Copied to clipboard"); }} @@ -426,7 +478,7 @@ export function CopyWorkflowVersion({ export function getWorkflowVersionFromVersionIndex( workflow: Awaited>, - version: number + version: number, ) { const workflow_version = workflow?.versions.find((x) => x.version == version); @@ -452,7 +504,7 @@ export function ViewWorkflowDetailsButton({ isLoading: isNodesIndexLoading, } = useSWR( "https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/extension-node-map.json", - fetcher + fetcher, ); const groupedByAuxName = useMemo(() => { @@ -462,7 +514,7 @@ export function ViewWorkflowDetailsButton({ const workflow_version = getWorkflowVersionFromVersionIndex( workflow, - version + version, ); const api = workflow_version?.workflow_api; @@ -473,7 +525,7 @@ export function ViewWorkflowDetailsButton({ .map(([_, value]) => { const classType = value.class_type; const classTypeData = Object.entries(data).find(([_, nodeArray]) => - nodeArray[0].includes(classType) + nodeArray[0].includes(classType), ); return classTypeData ? { node: value, classTypeData } : null; }) @@ -503,7 +555,7 @@ export function ViewWorkflowDetailsButton({ node: z.infer[]; url: string; } - > + >, ); // console.log(groupedByAuxName); @@ -544,7 +596,8 @@ export function ViewWorkflowDetailsButton({ {key} ({ user: one(usersTable, { fields: [workflowTable.user_id], @@ -79,6 +81,7 @@ export const workflowVersionTable = dbSchema.table("workflow_versions", { created_at: timestamp("created_at").defaultNow().notNull(), updated_at: timestamp("updated_at").defaultNow().notNull(), }); +export const workflowVersionSchema = createSelectSchema(workflowVersionTable); export const workflowVersionRelations = relations( workflowVersionTable, diff --git a/web/src/routes/registerGetWorkflow.ts b/web/src/routes/registerGetWorkflow.ts new file mode 100644 index 0000000..be17571 --- /dev/null +++ b/web/src/routes/registerGetWorkflow.ts @@ -0,0 +1,86 @@ +import { workflowVersionSchema } from "@/db/schema"; +import type { App } from "@/routes/app"; +import { authError } from "@/routes/authError"; +import { getWorkflowVersion } from "@/server/crudWorkflow"; +import { z, createRoute } from "@hono/zod-openapi"; + +const route = createRoute({ + method: "get", + path: "/workflow-version/:version_id", + tags: ["comfyui"], + summary: "Get comfyui workflow", + description: "Use this to retrieve comfyui workflow by id", + request: { + params: z.object({ + version_id: z.string(), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: workflowVersionSchema, + }, + }, + description: "Retrieve the output", + }, + 500: { + content: { + "application/json": { + schema: z.object({ + error: z.string(), + }), + }, + }, + description: "Error when uploading the workflow", + }, + ...authError, + }, +}); + +export const registerGetWorkflowRoute = (app: App) => { + return app.openapi(route, async (c) => { + const { version_id } = c.req.valid("param"); + const apiUser = c.get("apiKeyTokenData")!; + + if (!apiUser.user_id) + return c.json( + { + error: "Invalid user_id", + }, + { + status: 500, + }, + ); + + try { + const workflow_version = await getWorkflowVersion(apiUser, version_id); + if (workflow_version) { + return c.json(workflow_version, { + status: 200, + }); + } else { + return c.json( + { + error: "No version found", + }, + { + status: 500, + }, + ); + } + } catch (error: unknown) { + const errorMessage = + error instanceof Error ? error.message : "Unknown error"; + return c.json( + { + error: errorMessage, + }, + { + statusText: "Invalid request", + status: 500, + }, + ); + } + }); +}; diff --git a/web/src/routes/registerWorkflowUploadRoute.ts b/web/src/routes/registerWorkflowUploadRoute.ts index b6fe96d..b851ae2 100644 --- a/web/src/routes/registerWorkflowUploadRoute.ts +++ b/web/src/routes/registerWorkflowUploadRoute.ts @@ -1,4 +1,10 @@ -import { snapshotType, workflowAPIType, workflowType } from "@/db/schema"; +import { db } from "@/db/db"; +import { + snapshotType, + workflowAPIType, + workflowTable, + workflowType, +} from "@/db/schema"; import type { App } from "@/routes/app"; import { authError } from "@/routes/authError"; import { @@ -6,10 +12,11 @@ import { createNewWorkflowVersion, } from "@/server/createNewWorkflow"; import { z, createRoute } from "@hono/zod-openapi"; +import { and, eq } from "drizzle-orm"; const route = createRoute({ method: "post", - path: "/upload-workflow", + path: "/workflow", tags: ["comfyui"], summary: "Upload workflow from ComfyUI", description: @@ -106,6 +113,30 @@ export const registerWorkflowUploadRoute = (app: App) => { workflow_id = _workflow_id; version = _version; } else if (workflow_id) { + const workflow = await db + .select() + .from(workflowTable) + .where( + and( + eq(workflowTable.id, workflow_id), + eq(workflowTable.user_id, user_id), + eq(workflowTable.org_id, org_id), + ), + ); + + if (workflow.length === 0) { + return c.json( + { + error: "Invalid workflow_id", + }, + { + status: 500, + statusText: "Invalid workflow_id", + headers: corsHeaders, + }, + ); + } + // Case 2 update workflow const { version: _version } = await createNewWorkflowVersion({ workflow_id: workflow_id, diff --git a/web/src/server/getAllUserWorkflow.tsx b/web/src/server/crudWorkflow.tsx similarity index 61% rename from web/src/server/getAllUserWorkflow.tsx rename to web/src/server/crudWorkflow.tsx index b6400f4..32fb56b 100644 --- a/web/src/server/getAllUserWorkflow.tsx +++ b/web/src/server/crudWorkflow.tsx @@ -4,8 +4,10 @@ import { workflowTable, workflowVersionTable, } from "@/db/schema"; +import { APIKeyUserType } from "@/server/APIKeyBodyRequest"; import { auth } from "@clerk/nextjs"; import { and, desc, eq, isNull } from "drizzle-orm"; +import { redirect } from "next/navigation"; export async function getAllUserWorkflow() { const { userId, orgId } = await auth(); @@ -51,3 +53,29 @@ export async function getAllUserWorkflow() { return workflow; } + +export async function getWorkflowVersion( + apiUser: APIKeyUserType, + version_id: string, +) { + const { org_id, user_id } = apiUser; + + if (!user_id) { + throw new Error("No user id"); + } + + const parentWorkflow = await db.query.workflowTable.findFirst({ + where: + org_id != undefined + ? eq(workflowTable.org_id, org_id) + : and(eq(workflowTable.user_id, user_id), isNull(workflowTable.org_id)), + }); + + if (!parentWorkflow) { + throw new Error("No workflow found"); + } + + return db.query.workflowVersionTable.findFirst({ + where: eq(workflowVersionTable.id, version_id), + }); +} diff --git a/web/src/server/curdDeploments.ts b/web/src/server/curdDeploments.ts index 9ea5190..a3ef2b0 100644 --- a/web/src/server/curdDeploments.ts +++ b/web/src/server/curdDeploments.ts @@ -16,270 +16,271 @@ import "server-only"; import { validate as isValidUUID } from "uuid"; import type { z } from "zod"; export async function createDeployments( - workflow_id: string, - version_id: string, - machine_id: string, - environment: DeploymentType["environment"], + workflow_id: string, + version_id: string, + machine_id: string, + environment: DeploymentType["environment"], ) { - const { userId, orgId } = auth(); - if (!userId) throw new Error("No user id"); + const { userId, orgId } = auth(); + if (!userId) throw new Error("No user id"); - if (!machine_id) { - throw new Error("No machine id provided"); - } + if (!machine_id) { + throw new Error("No machine id provided"); + } - // Same environment and same workflow - const existingDeployment = await db.query.deploymentsTable.findFirst({ - where: and( - eq(deploymentsTable.workflow_id, workflow_id), - eq(deploymentsTable.environment, environment), - ), - }); + // Same environment and same workflow + const existingDeployment = await db.query.deploymentsTable.findFirst({ + where: and( + eq(deploymentsTable.workflow_id, workflow_id), + eq(deploymentsTable.environment, environment), + ), + }); - if (existingDeployment) { - await db - .update(deploymentsTable) - .set({ - workflow_id, - workflow_version_id: version_id, - machine_id, - org_id: orgId, - }) - .where(eq(deploymentsTable.id, existingDeployment.id)); - } else { - const workflow = await db.query.workflowTable.findFirst({ - where: eq(workflowTable.id, workflow_id), - with: { - user: { - columns: { - name: true, - }, - }, - }, - }); + if (existingDeployment) { + await db + .update(deploymentsTable) + .set({ + workflow_id, + workflow_version_id: version_id, + machine_id, + org_id: orgId, + }) + .where(eq(deploymentsTable.id, existingDeployment.id)); + } else { + const workflow = await db.query.workflowTable.findFirst({ + where: eq(workflowTable.id, workflow_id), + with: { + user: { + columns: { + name: true, + }, + }, + }, + }); - if (!workflow) throw new Error("No workflow found"); + if (!workflow) throw new Error("No workflow found"); - const userName = workflow.org_id - ? await clerkClient.organizations - .getOrganization({ - organizationId: workflow.org_id, - }) - .then((x) => x.name) - : workflow.user.name; + const userName = workflow.org_id + ? await clerkClient.organizations + .getOrganization({ + organizationId: workflow.org_id, + }) + .then((x) => x.name) + : workflow.user.name; - await db.insert(deploymentsTable).values({ - user_id: userId, - workflow_id, - workflow_version_id: version_id, - machine_id, - environment, - org_id: orgId, - share_slug: slugify(`${userName} ${workflow.name}`), - }); - } - revalidatePath(`/${workflow_id}`); - return { - message: `Successfully created deployment for ${environment}`, - }; + await db.insert(deploymentsTable).values({ + user_id: userId, + workflow_id, + workflow_version_id: version_id, + machine_id, + environment, + org_id: orgId, + share_slug: slugify(`${userName} ${workflow.name}`), + }); + } + revalidatePath(`/${workflow_id}`); + return { + message: `Successfully created deployment for ${environment}`, + }; } export async function findAllDeployments() { - const { userId, orgId } = auth(); - if (!userId) throw new Error("No user id"); + const { userId, orgId } = auth(); + if (!userId) throw new Error("No user id"); - const deployments = await db.query.workflowTable.findMany({ - where: and( - orgId - ? eq(workflowTable.org_id, orgId) - : and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)), - ), - columns: { - name: true, - }, - with: { - deployments: { - columns: { - environment: true, - }, - with: { - version: { - columns: { - id: true, - snapshot: true, - }, - }, - }, - }, - }, - }); + const deployments = await db.query.workflowTable.findMany({ + where: and( + orgId + ? eq(workflowTable.org_id, orgId) + : and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)), + ), + columns: { + name: true, + }, + with: { + deployments: { + columns: { + environment: true, + }, + with: { + version: { + columns: { + id: true, + snapshot: true, + }, + }, + }, + }, + }, + }); - return deployments; + return deployments; } export async function findSharedDeployment(workflow_id: string) { - const deploymentData = await db.query.deploymentsTable.findFirst({ - where: and( - eq(deploymentsTable.environment, "public-share"), - isValidUUID(workflow_id) - ? eq(deploymentsTable.id, workflow_id) - : eq(deploymentsTable.share_slug, workflow_id), - ), - with: { - user: true, - machine: true, - workflow: { - columns: { - name: true, - org_id: true, - user_id: true, - }, - }, - version: true, - }, - }); + const deploymentData = await db.query.deploymentsTable.findFirst({ + where: and( + eq(deploymentsTable.environment, "public-share"), + isValidUUID(workflow_id) + ? eq(deploymentsTable.id, workflow_id) + : eq(deploymentsTable.share_slug, workflow_id), + ), + with: { + user: true, + machine: true, + workflow: { + columns: { + name: true, + org_id: true, + user_id: true, + }, + }, + version: true, + }, + }); - return deploymentData; + return deploymentData; } export const removePublicShareDeployment = withServerPromise( - async (deployment_id: string) => { - const [removed] = await db - .delete(deploymentsTable) - .where( - and( - eq(deploymentsTable.environment, "public-share"), - eq(deploymentsTable.id, deployment_id), - ), - ).returning(); - + async (deployment_id: string) => { + const [removed] = await db + .delete(deploymentsTable) + .where( + and( + eq(deploymentsTable.environment, "public-share"), + eq(deploymentsTable.id, deployment_id), + ), + ) + .returning(); + // revalidatePath( // `/workflows/${removed.workflow_id}` // ) - }, + }, ); export const cloneWorkflow = withServerPromise( - async (deployment_id: string) => { - const deployment = await db.query.deploymentsTable.findFirst({ - where: and( - eq(deploymentsTable.environment, "public-share"), - eq(deploymentsTable.id, deployment_id), - ), - with: { - version: true, - workflow: true, - }, - }); + async (deployment_id: string) => { + const deployment = await db.query.deploymentsTable.findFirst({ + where: and( + eq(deploymentsTable.environment, "public-share"), + eq(deploymentsTable.id, deployment_id), + ), + with: { + version: true, + workflow: true, + }, + }); - if (!deployment) throw new Error("No deployment found"); + if (!deployment) throw new Error("No deployment found"); - const { userId, orgId } = auth(); + const { userId, orgId } = auth(); - if (!userId) throw new Error("No user id"); + if (!userId) throw new Error("No user id"); - await createNewWorkflow({ - user_id: userId, - org_id: orgId, - workflow_name: `${deployment.workflow.name} (Cloned)`, - workflowData: { - workflow: deployment.version.workflow, - workflow_api: deployment?.version.workflow_api, - snapshot: deployment?.version.snapshot, - }, - }); + await createNewWorkflow({ + user_id: userId, + org_id: orgId, + workflow_name: `${deployment.workflow.name} (Cloned)`, + workflowData: { + workflow: deployment.version.workflow, + workflow_api: deployment?.version.workflow_api, + snapshot: deployment?.version.snapshot, + }, + }); - redirect(`/workflows/${deployment.workflow.id}`); + redirect(`/workflows/${deployment.workflow.id}`); - return { - message: "Successfully cloned workflow", - }; - }, + return { + message: "Successfully cloned workflow", + }; + }, ); export const cloneMachine = withServerPromise(async (deployment_id: string) => { - const deployment = await db.query.deploymentsTable.findFirst({ - where: and( - eq(deploymentsTable.environment, "public-share"), - eq(deploymentsTable.id, deployment_id), - ), - with: { - machine: true, - }, - }); + const deployment = await db.query.deploymentsTable.findFirst({ + where: and( + eq(deploymentsTable.environment, "public-share"), + eq(deploymentsTable.id, deployment_id), + ), + with: { + machine: true, + }, + }); - if (!deployment) throw new Error("No deployment found"); - if (deployment.machine.type !== "comfy-deploy-serverless") - throw new Error("Can only clone comfy-deploy-serverlesss"); + if (!deployment) throw new Error("No deployment found"); + if (deployment.machine.type !== "comfy-deploy-serverless") + throw new Error("Can only clone comfy-deploy-serverlesss"); - const { userId, orgId } = auth(); + const { userId, orgId } = auth(); - if (!userId) throw new Error("No user id"); + if (!userId) throw new Error("No user id"); - await addCustomMachine({ - gpu: deployment.machine.gpu, - models: deployment.machine.models, - snapshot: deployment.machine.snapshot, - name: `${deployment.machine.name} (Cloned)`, - type: "comfy-deploy-serverless", - }); + await addCustomMachine({ + gpu: deployment.machine.gpu, + models: deployment.machine.models, + snapshot: deployment.machine.snapshot, + name: `${deployment.machine.name} (Cloned)`, + type: "comfy-deploy-serverless", + }); - return { - message: "Successfully cloned workflow", - }; + return { + message: "Successfully cloned workflow", + }; }); export async function findUserShareDeployment(share_id: string) { - const { userId, orgId } = auth(); + const { userId, orgId } = auth(); - if (!userId) throw new Error("No user id"); + if (!userId) throw new Error("No user id"); - const [deployment] = await db - .select() - .from(deploymentsTable) - .where( - and( - isValidUUID(share_id) - ? eq(deploymentsTable.id, share_id) - : eq(deploymentsTable.share_slug, share_id), - eq(deploymentsTable.environment, "public-share"), - orgId - ? eq(deploymentsTable.org_id, orgId) - : and( - eq(deploymentsTable.user_id, userId), - isNull(deploymentsTable.org_id), - ), - ), - ); + const [deployment] = await db + .select() + .from(deploymentsTable) + .where( + and( + isValidUUID(share_id) + ? eq(deploymentsTable.id, share_id) + : eq(deploymentsTable.share_slug, share_id), + eq(deploymentsTable.environment, "public-share"), + orgId + ? eq(deploymentsTable.org_id, orgId) + : and( + eq(deploymentsTable.user_id, userId), + isNull(deploymentsTable.org_id), + ), + ), + ); - if (!deployment) throw new Error("No deployment found"); + if (!deployment) throw new Error("No deployment found"); - return deployment; + return deployment; } export const updateSharePageInfo = withServerPromise( - async ({ - id, - ...data - }: z.infer & { - id: string; - }) => { - const { userId } = auth(); - if (!userId) return { error: "No user id" }; + async ({ + id, + ...data + }: z.infer & { + id: string; + }) => { + const { userId } = auth(); + if (!userId) return { error: "No user id" }; - console.log(data); + console.log(data); - const [deployment] = await db - .update(deploymentsTable) - .set(data) - .where( - and( - eq(deploymentsTable.environment, "public-share"), - eq(deploymentsTable.id, id), - ), - ) - .returning(); + const [deployment] = await db + .update(deploymentsTable) + .set(data) + .where( + and( + eq(deploymentsTable.environment, "public-share"), + eq(deploymentsTable.id, id), + ), + ) + .returning(); - return { message: "Info Updated" }; - }, + return { message: "Info Updated" }; + }, ); diff --git a/web/src/server/editWorkflowOnMachine.tsx b/web/src/server/editWorkflowOnMachine.tsx new file mode 100644 index 0000000..ff7bb5a --- /dev/null +++ b/web/src/server/editWorkflowOnMachine.tsx @@ -0,0 +1,43 @@ +"use server"; + +import { getMachineById } from "@/server/curdMachine"; +import { auth } from "@clerk/nextjs"; +import jwt from "jsonwebtoken"; +import { getOrgOrUserDisplayName } from "@/server/getOrgOrUserDisplayName"; +import { withServerPromise } from "@/server/withServerPromise"; +import "server-only"; +import { headers } from "next/headers"; + +export const editWorkflowOnMachine = withServerPromise( + async (workflow_version_id: string, machine_id: string) => { + const { userId, orgId } = auth(); + + const headersList = headers(); + const host = headersList.get("host") || ""; + const protocol = headersList.get("x-forwarded-proto") || ""; + const domain = `${protocol}://${host}`; + + if (!userId) { + throw new Error("No user id"); + } + + const machine = await getMachineById(machine_id); + + const expireTime = "1w"; + const token = jwt.sign( + { user_id: userId, org_id: orgId }, + process.env.JWT_SECRET!, + { + expiresIn: expireTime, + }, + ); + + const userName = await getOrgOrUserDisplayName(orgId, userId); + + return `${ + machine.endpoint + }?workflow_version_id=${workflow_version_id}&auth_token=${token}&org_display=${encodeURIComponent( + userName, + )}&origin=${encodeURIComponent(domain)}`; + }, +);