feat: add opening directly from comfy deploy -> machines

This commit is contained in:
BennyKok 2024-01-23 09:33:53 +08:00
parent c0450b58d5
commit 52d6e07eeb
12 changed files with 808 additions and 405 deletions

View File

@ -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 {
</div>
`);
}
loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) {
this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center;">${title} ${this.loadingIcon}</h3>
<label>
${message}
</label>
</div>
`);
}
}
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 = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
showLoading(title, message) {
this.show(`
<div style="width: 400px; display: flex; gap: 18px; flex-direction: column; overflow: unset">
<h3 style="margin: 0px; display: flex; align-items: center; justify-content: center; gap: 4px;">${title} ${this.loadingIcon}</h3>
</div>
`);
}
}
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();
}

View File

@ -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;

View File

@ -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({
<CreateShareButton workflow={workflow} machines={machines} />
<CopyWorkflowVersion workflow={workflow} />
<ViewWorkflowDetailsButton workflow={workflow} />
<OpenEditButton workflow={workflow} machines={machines} />
</div>
<VersionDetails workflow={workflow} />

View File

@ -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";

View File

@ -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<ReturnType<typeof findAllDeployments>>[0];
domain: string;
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
domain: string;
}) {
const workflowInput = getInputsFromWorkflow(deployment.version);
const workflowInput = getInputsFromWorkflow(deployment.version);
if (deployment.environment === "public-share") {
return <SharePageDeploymentRow deployment={deployment} />;
}
if (deployment.environment === "public-share") {
return <SharePageDeploymentRow deployment={deployment} />;
}
return (
<Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow>
<DeploymentRow deployment={deployment} />
</TableRow>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle className="capitalize">
{deployment.environment} Deployment
</DialogTitle>
<DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[600px] pr-4">
<Tabs defaultValue="client" className="w-full gap-2 text-sm">
<TabsList className="grid w-fit grid-cols-3 mb-2">
<TabsTrigger value="client">Server Client</TabsTrigger>
<TabsTrigger value="js">NodeJS Fetch</TabsTrigger>
<TabsTrigger value="curl">CURL</TabsTrigger>
</TabsList>
<TabsContent className="flex flex-col gap-2 !mt-0" value="client">
<div>
Copy and paste the ComfyDeployClient form&nbsp;
<a
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
className="text-blue-500 hover:underline"
target="_blank"
rel="noreferrer"
>
here
</a>
</div>
<CodeBlock
lang="js"
code={formatCode(
domain == "https://www.comfydeploy.com"
? jsClientSetupTemplateHostedVersion
: jsClientSetupTemplate,
deployment,
domain,
workflowInput,
)}
/>
Create a run via deployment id
<CodeBlock
lang="js"
code={formatCode(
workflowInput && workflowInput.length > 0
? jsClientCreateRunTemplate
: jsClientCreateRunNoInputsTemplate,
deployment,
domain,
workflowInput,
)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(
clientTemplate_checkStatus,
deployment,
domain,
)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-0" value="js">
Trigger the workflow
<CodeBlock
lang="js"
code={formatCode(jsTemplate, deployment, domain, workflowInput)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-2" value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment, domain)}
/>
<CodeBlock
lang="bash"
code={formatCode(curlTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
</Tabs>
</ScrollArea>
</DialogContent>
</Dialog>
);
return (
<Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow>
<DeploymentRow deployment={deployment} />
</TableRow>
</DialogTrigger>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle className="capitalize">
{deployment.environment} Deployment
</DialogTitle>
<DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-[600px] pr-4">
<Tabs defaultValue="client" className="w-full gap-2 text-sm">
<TabsList className="grid w-fit grid-cols-3 mb-2">
<TabsTrigger value="client">Server Client</TabsTrigger>
<TabsTrigger value="js">NodeJS Fetch</TabsTrigger>
<TabsTrigger value="curl">CURL</TabsTrigger>
</TabsList>
<TabsContent className="flex flex-col gap-2 !mt-0" value="client">
<div>
Copy and paste the ComfyDeployClient form&nbsp;
<a
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
className="text-blue-500 hover:underline"
target="_blank"
rel="noreferrer"
>
here
</a>
</div>
<CodeBlock
lang="js"
code={formatCode(
domain == "https://www.comfydeploy.com"
? jsClientSetupTemplateHostedVersion
: jsClientSetupTemplate,
deployment,
domain,
workflowInput,
)}
/>
Create a run via deployment id
<CodeBlock
lang="js"
code={formatCode(
workflowInput && workflowInput.length > 0
? jsClientCreateRunTemplate
: jsClientCreateRunNoInputsTemplate,
deployment,
domain,
workflowInput,
)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(
clientTemplate_checkStatus,
deployment,
domain,
)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-0" value="js">
Trigger the workflow
<CodeBlock
lang="js"
code={formatCode(jsTemplate, deployment, domain, workflowInput)}
/>
Check the status of the run, and retrieve the outputs
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
<TabsContent className="flex flex-col gap-2 !mt-2" value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment, domain)}
/>
<CodeBlock
lang="bash"
code={formatCode(curlTemplate_checkStatus, deployment, domain)}
/>
</TabsContent>
</Tabs>
</ScrollArea>
</DialogContent>
</Dialog>
);
}
function formatCode(
codeTemplate: string,
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0],
domain: string,
inputs?: ReturnType<typeof getInputsFromWorkflow>,
inputsTabs?: number,
codeTemplate: string,
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0],
domain: string,
inputs?: ReturnType<typeof getInputsFromWorkflow>,
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("<URL>", `${domain ?? "http://localhost:3000"}/api/run`)
.replace("<ID>", deployment.id)
.replace("<URLONLY>", domain ?? "http://localhost:3000");
"",
);
}
return codeTemplate
.replace("<URL>", `${domain ?? "http://localhost:3000"}/api/run`)
.replace("<ID>", deployment.id)
.replace("<URLONLY>", domain ?? "http://localhost:3000");
}

View File

@ -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<ReturnType<typeof getMachines>>,
machines: Awaited<ReturnType<typeof getMachines>>,
) {
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 (
<DropdownMenu>
@ -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<ReturnType<typeof findFirstTableWithVersion>>;
machines: Awaited<ReturnType<typeof getMachines>>;
}) {
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 && (
<Button
className="gap-2"
onClick={async () => {
setIsLoading(true);
const url = await callServerPromise(
editWorkflowOnMachine(workflow_version_id, machine),
);
if (url && typeof url !== "object") {
window.open(url, "_blank");
} else if (url && typeof url === "object" && url.error) {
console.error(url.error);
}
setIsLoading(false);
}}
// asChild
variant="outline"
>
Edit {isLoading ? <LoadingIcon /> : <Edit size={14} />}
</Button>
)
);
}
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 (
<DropdownMenu>
@ -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({
<DropdownMenuItem
onClick={async () => {
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<ReturnType<typeof findFirstTableWithVersion>>,
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<typeof workflowAPINodeType>[];
url: string;
}
>
>,
);
// console.log(groupedByAuxName);
@ -544,7 +596,8 @@ export function ViewWorkflowDetailsButton({
<a
href={group.url}
target="_blank"
className="hover:underline" rel="noreferrer"
className="hover:underline"
rel="noreferrer"
>
{key}
<ExternalLink

View File

@ -9,7 +9,7 @@ import {
timestamp,
uuid,
} from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
export const dbSchema = pgSchema("comfyui_deploy");
@ -35,6 +35,8 @@ export const workflowTable = dbSchema.table("workflows", {
updated_at: timestamp("updated_at").defaultNow().notNull(),
});
export const workflowSchema = createSelectSchema(workflowTable);
export const workflowRelations = relations(workflowTable, ({ many, one }) => ({
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,

View File

@ -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,
},
);
}
});
};

View File

@ -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,

View File

@ -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),
});
}

View File

@ -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<typeof publicShareDeployment> & {
id: string;
}) => {
const { userId } = auth();
if (!userId) return { error: "No user id" };
async ({
id,
...data
}: z.infer<typeof publicShareDeployment> & {
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" };
},
);

View File

@ -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)}`;
},
);