feat: share page slug

This commit is contained in:
BennyKok 2024-01-21 11:40:48 +08:00
parent ca1b05fff5
commit 6de7bf3f20
13 changed files with 1453 additions and 615 deletions

View File

@ -0,0 +1,2 @@
ALTER TABLE "comfyui_deploy"."deployments" ADD COLUMN "share_slug" text;--> statement-breakpoint
ALTER TABLE "comfyui_deploy"."deployments" ADD CONSTRAINT "deployments_share_slug_unique" UNIQUE("share_slug");

View File

@ -0,0 +1,776 @@
{
"id": "1425ee00-66fb-4541-8da7-19b217944545",
"prevId": "1ca4fdb7-c0c4-4c39-8b47-f40282293da0",
"version": "5",
"dialect": "pg",
"tables": {
"api_keys": {
"name": "api_keys",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"revoked": {
"name": "revoked",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"api_keys_user_id_users_id_fk": {
"name": "api_keys_user_id_users_id_fk",
"tableFrom": "api_keys",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"api_keys_key_unique": {
"name": "api_keys_key_unique",
"nullsNotDistinct": false,
"columns": [
"key"
]
}
}
},
"deployments": {
"name": "deployments",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"share_slug": {
"name": "share_slug",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"showcase_media": {
"name": "showcase_media",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"environment": {
"name": "environment",
"type": "deployment_environment",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"deployments_user_id_users_id_fk": {
"name": "deployments_user_id_users_id_fk",
"tableFrom": "deployments",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_workflow_version_id_workflow_versions_id_fk": {
"name": "deployments_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "deployments",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"deployments_workflow_id_workflows_id_fk": {
"name": "deployments_workflow_id_workflows_id_fk",
"tableFrom": "deployments",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"deployments_machine_id_machines_id_fk": {
"name": "deployments_machine_id_machines_id_fk",
"tableFrom": "deployments",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"deployments_share_slug_unique": {
"name": "deployments_share_slug_unique",
"nullsNotDistinct": false,
"columns": [
"share_slug"
]
}
}
},
"machines": {
"name": "machines",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"endpoint": {
"name": "endpoint",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"disabled": {
"name": "disabled",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"auth_token": {
"name": "auth_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"type": {
"name": "type",
"type": "machine_type",
"primaryKey": false,
"notNull": true,
"default": "'classic'"
},
"status": {
"name": "status",
"type": "machine_status",
"primaryKey": false,
"notNull": true,
"default": "'ready'"
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"models": {
"name": "models",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"gpu": {
"name": "gpu",
"type": "machine_gpu",
"primaryKey": false,
"notNull": false
},
"build_machine_instance_id": {
"name": "build_machine_instance_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"build_log": {
"name": "build_log",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"machines_user_id_users_id_fk": {
"name": "machines_user_id_users_id_fk",
"tableFrom": "machines",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"users": {
"name": "users",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"username": {
"name": "username",
"type": "text",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_run_outputs": {
"name": "workflow_run_outputs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"run_id": {
"name": "run_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_run_outputs_run_id_workflow_runs_id_fk": {
"name": "workflow_run_outputs_run_id_workflow_runs_id_fk",
"tableFrom": "workflow_run_outputs",
"tableTo": "workflow_runs",
"columnsFrom": [
"run_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_runs": {
"name": "workflow_runs",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow_version_id": {
"name": "workflow_version_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"workflow_inputs": {
"name": "workflow_inputs",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"machine_id": {
"name": "machine_id",
"type": "uuid",
"primaryKey": false,
"notNull": false
},
"origin": {
"name": "origin",
"type": "workflow_run_origin",
"primaryKey": false,
"notNull": true,
"default": "'api'"
},
"status": {
"name": "status",
"type": "workflow_run_status",
"primaryKey": false,
"notNull": true,
"default": "'not-started'"
},
"ended_at": {
"name": "ended_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"workflow_runs_workflow_version_id_workflow_versions_id_fk": {
"name": "workflow_runs_workflow_version_id_workflow_versions_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflow_versions",
"columnsFrom": [
"workflow_version_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
},
"workflow_runs_workflow_id_workflows_id_fk": {
"name": "workflow_runs_workflow_id_workflows_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"workflow_runs_machine_id_machines_id_fk": {
"name": "workflow_runs_machine_id_machines_id_fk",
"tableFrom": "workflow_runs",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflows": {
"name": "workflows",
"schema": "comfyui_deploy",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
},
"org_id": {
"name": "org_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflows_user_id_users_id_fk": {
"name": "workflows_user_id_users_id_fk",
"tableFrom": "workflows",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"workflow_versions": {
"name": "workflow_versions",
"schema": "comfyui_deploy",
"columns": {
"workflow_id": {
"name": "workflow_id",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"workflow": {
"name": "workflow",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"workflow_api": {
"name": "workflow_api",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"snapshot": {
"name": "snapshot",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"updated_at": {
"name": "updated_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"workflow_versions_workflow_id_workflows_id_fk": {
"name": "workflow_versions_workflow_id_workflows_id_fk",
"tableFrom": "workflow_versions",
"tableTo": "workflows",
"columnsFrom": [
"workflow_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {
"deployment_environment": {
"name": "deployment_environment",
"values": {
"staging": "staging",
"production": "production",
"public-share": "public-share"
}
},
"machine_gpu": {
"name": "machine_gpu",
"values": {
"T4": "T4",
"A10G": "A10G",
"A100": "A100"
}
},
"machine_status": {
"name": "machine_status",
"values": {
"ready": "ready",
"building": "building",
"error": "error"
}
},
"machine_type": {
"name": "machine_type",
"values": {
"classic": "classic",
"runpod-serverless": "runpod-serverless",
"modal-serverless": "modal-serverless",
"comfy-deploy-serverless": "comfy-deploy-serverless"
}
},
"workflow_run_origin": {
"name": "workflow_run_origin",
"values": {
"manual": "manual",
"api": "api",
"public-share": "public-share"
}
},
"workflow_run_status": {
"name": "workflow_run_status",
"values": {
"not-started": "not-started",
"running": "running",
"uploading": "uploading",
"success": "success",
"failed": "failed"
}
}
},
"schemas": {
"comfyui_deploy": "comfyui_deploy"
},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View File

@ -225,6 +225,13 @@
"when": 1705763980972, "when": 1705763980972,
"tag": "0031_fast_lyja", "tag": "0031_fast_lyja",
"breakpoints": true "breakpoints": true
},
{
"idx": 32,
"version": "5",
"when": 1705806921697,
"tag": "0032_shallow_vermin",
"breakpoints": true
} }
] ]
} }

View File

@ -2,11 +2,11 @@ import { ButtonActionMenu } from "@/components/ButtonActionLoader";
import { RunWorkflowInline } from "@/components/RunWorkflowInline"; import { RunWorkflowInline } from "@/components/RunWorkflowInline";
import { PublicRunOutputs } from "@/components/VersionSelect"; import { PublicRunOutputs } from "@/components/VersionSelect";
import { import {
Card, Card,
CardContent, CardContent,
CardDescription, CardDescription,
CardHeader, CardHeader,
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { db } from "@/db/db"; import { db } from "@/db/db";
import { usersTable } from "@/db/schema"; import { usersTable } from "@/db/schema";
@ -14,9 +14,9 @@ import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import { getRelativeTime } from "@/lib/getRelativeTime"; import { getRelativeTime } from "@/lib/getRelativeTime";
import { setInitialUserData } from "@/lib/setInitialUserData"; import { setInitialUserData } from "@/lib/setInitialUserData";
import { import {
cloneMachine, cloneMachine,
cloneWorkflow, cloneWorkflow,
findSharedDeployment, findSharedDeployment,
} from "@/server/curdDeploments"; } from "@/server/curdDeploments";
import { auth, clerkClient } from "@clerk/nextjs/server"; import { auth, clerkClient } from "@clerk/nextjs/server";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@ -25,89 +25,87 @@ import { redirect } from "next/navigation";
export const maxDuration = 300; // 5 minutes export const maxDuration = 300; // 5 minutes
export default async function Page({ export default async function Page({
params, params,
}: { }: {
params: { share_id: string }; params: { share_id: string };
}) { }) {
const { userId } = await auth(); const { userId } = await auth();
// If there is user, check if the user data is present // If there is user, check if the user data is present
if (userId) { if (userId) {
const user = await db.query.usersTable.findFirst({ const user = await db.query.usersTable.findFirst({
where: eq(usersTable.id, userId), where: eq(usersTable.id, userId),
}); });
if (!user) { if (!user) {
await setInitialUserData(userId); await setInitialUserData(userId);
} }
} }
const sharedDeployment = await findSharedDeployment(params.share_id); const sharedDeployment = await findSharedDeployment(params.share_id);
if (!sharedDeployment) return redirect("/"); if (!sharedDeployment) return redirect("/");
const userName = sharedDeployment.workflow.org_id const userName = sharedDeployment.workflow.org_id
? await clerkClient.organizations ? await clerkClient.organizations
.getOrganization({ .getOrganization({
organizationId: sharedDeployment.workflow.org_id, organizationId: sharedDeployment.workflow.org_id,
}) })
.then((x) => x.name) .then((x) => x.name)
: sharedDeployment.user.name; : sharedDeployment.user.name;
const inputs = getInputsFromWorkflow(sharedDeployment.version); const inputs = getInputsFromWorkflow(sharedDeployment.version);
return ( return (
<div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]"> <div className="mt-4 w-full grid grid-rows-[1fr,1fr] lg:grid-cols-[minmax(auto,500px),1fr] gap-4 max-h-[calc(100dvh-100px)]">
<Card className="w-full h-fit mt-4"> <Card className="w-full h-fit mt-4">
<CardHeader> <CardHeader>
<CardTitle className="flex justify-between items-center"> <CardTitle className="flex justify-between items-center">
<div> <div>
{userName} {userName}
{" / "} {" / "}
{sharedDeployment.workflow.name} {sharedDeployment.workflow.name}
</div> </div>
<ButtonActionMenu <ButtonActionMenu
title="Clone" title="Clone"
actions={[ actions={[
{ {
title: "Workflow", title: "Workflow",
action: cloneWorkflow.bind(null, sharedDeployment.id), action: cloneWorkflow.bind(null, sharedDeployment.id),
}, },
{ {
title: "Machine", title: "Machine",
action: cloneMachine.bind(null, sharedDeployment.id), action: cloneMachine.bind(null, sharedDeployment.id),
}, },
]} ]}
/> />
</CardTitle> </CardTitle>
<CardDescription suppressHydrationWarning={true}> <CardDescription suppressHydrationWarning={true}>
{getRelativeTime(sharedDeployment?.updated_at)} {getRelativeTime(sharedDeployment?.updated_at)}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div> <div>
{sharedDeployment?.description && ( {sharedDeployment?.description && sharedDeployment?.description}
<>{sharedDeployment?.description}</> </div>
)} <RunWorkflowInline
</div> inputs={inputs}
<RunWorkflowInline machine_id={sharedDeployment.machine_id}
inputs={inputs} workflow_version_id={sharedDeployment.workflow_version_id}
machine_id={sharedDeployment.machine_id} />
workflow_version_id={sharedDeployment.workflow_version_id} </CardContent>
/> </Card>
</CardContent> <Card className="w-full h-fit mt-4">
</Card> <CardHeader>
<Card className="w-full h-fit mt-4"> <CardDescription>Run outputs</CardDescription>
<CardHeader> </CardHeader>
<CardDescription>Run outputs</CardDescription>
</CardHeader>
<CardContent> <CardContent>
<PublicRunOutputs preview={sharedDeployment.showcase_media} /> <PublicRunOutputs preview={sharedDeployment.showcase_media} />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
); );
} }

View File

@ -1,13 +1,13 @@
import { CreateShareButton } from "@/components/CreateShareButton";
import { MachinesWSMain } from "@/components/MachinesWS"; import { MachinesWSMain } from "@/components/MachinesWS";
import { VersionDetails } from "@/components/VersionDetails"; import { VersionDetails } from "@/components/VersionDetails";
import { import {
CopyWorkflowVersion, CopyWorkflowVersion,
CreateDeploymentButton, CreateDeploymentButton,
CreateShareButton, MachineSelect,
MachineSelect, RunWorkflowButton,
RunWorkflowButton, VersionSelect,
VersionSelect, ViewWorkflowDetailsButton,
ViewWorkflowDetailsButton,
} from "@/components/VersionSelect"; } from "@/components/VersionSelect";
import { import {
Card, Card,

View File

@ -4,10 +4,10 @@ import { LoadingIcon } from "@/components/LoadingIcon";
import { callServerPromise } from "@/components/callServerPromise"; import { callServerPromise } from "@/components/callServerPromise";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useAuth, useClerk } from "@clerk/nextjs"; import { useAuth, useClerk } from "@clerk/nextjs";
import { MoreVertical } from "lucide-react"; import { MoreVertical } from "lucide-react";
@ -15,74 +15,80 @@ import { useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
export function ButtonAction({ export function ButtonAction({
action, action,
children, children,
...rest routerAction = "back",
...rest
}: { }: {
action: () => Promise<any>; action: () => Promise<any>;
children: React.ReactNode; routerAction?: "refresh" | "back";
children: React.ReactNode;
}) { }) {
const [pending, setPending] = useState(false); const [pending, setPending] = useState(false);
const router = useRouter(); const router = useRouter();
return ( return (
<button <button
onClick={async () => { onClick={async () => {
if (pending) return; if (pending) return;
setPending(true); setPending(true);
await callServerPromise(action()); await callServerPromise(action());
setPending(false); setPending(false);
router.refresh(); if (routerAction === "back") {
}} router.back();
{...rest} router.refresh();
> }
{children} {pending && <LoadingIcon />} else if (routerAction === "refresh") router.refresh();
</button> }}
); {...rest}
>
{children} {pending && <LoadingIcon />}
</button>
);
} }
export function ButtonActionMenu(props: { export function ButtonActionMenu(props: {
title?: string; title?: string;
actions: { actions: {
title: string; title: string;
action: () => Promise<any>; action: () => Promise<any>;
}[]; }[];
}) { }) {
const user = useAuth(); const user = useAuth();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const clerk = useClerk(); const clerk = useClerk();
return ( return (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="gap-2" variant="outline" disabled={isLoading}> <Button className="gap-2" variant="outline" disabled={isLoading}>
{props.title} {props.title}
{isLoading ? <LoadingIcon /> : <MoreVertical size={14} />} {isLoading ? <LoadingIcon /> : <MoreVertical size={14} />}
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-56"> <DropdownMenuContent className="w-56">
{props.actions.map((action) => ( {props.actions.map((action) => (
<DropdownMenuItem <DropdownMenuItem
key={action.title} key={action.title}
onClick={async () => { onClick={async () => {
if (!user.isSignedIn) { if (!user.isSignedIn) {
clerk.openSignIn({ clerk.openSignIn({
redirectUrl: window.location.href, redirectUrl: window.location.href,
}); });
return; return;
} }
setIsLoading(true); setIsLoading(true);
await callServerPromise(action.action()); await callServerPromise(action.action());
setIsLoading(false); setIsLoading(false);
}} }}
> >
{action.title} {action.title}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );
} }

View File

@ -0,0 +1,66 @@
"use client";
import { LoadingIcon } from "@/components/LoadingIcon";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { Share } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useState } from "react";
import { useSelectedMachine } from "./VersionSelect";
import { callServerPromise } from "./callServerPromise";
export function CreateShareButton({
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 [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version,
)?.id;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" disabled={isLoading} variant="outline">
Share {isLoading ? <LoadingIcon /> : <Share size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={async () => {
if (!workflow_version_id) return;
setIsLoading(true);
await callServerPromise(
createDeployments(
workflow.id,
workflow_version_id,
machine,
"public-share",
),
);
setIsLoading(false);
}}
>
Public
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -1,18 +1,18 @@
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
import { CodeBlock } from "@/components/CodeBlock"; import { CodeBlock } from "@/components/CodeBlock";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription, DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import { TableRow } from "@/components/ui/table"; import { TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow"; import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import type { findAllDeployments } from "@/server/findAllRuns"; import type { findAllDeployments } from "@/server/findAllRuns";
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
const curlTemplate = ` const curlTemplate = `
curl --request POST \ curl --request POST \
@ -83,144 +83,145 @@ const run = await client.getRun(run_id);
`; `;
export function DeploymentDisplay({ export function DeploymentDisplay({
deployment, deployment,
domain, domain,
}: { }: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]; deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
domain: string; domain: string;
}) { }) {
const workflowInput = getInputsFromWorkflow(deployment.version); const workflowInput = getInputsFromWorkflow(deployment.version);
if (deployment.environment == "public-share") { if (deployment.environment === "public-share") {
return <SharePageDeploymentRow deployment={deployment} />; return <SharePageDeploymentRow deployment={deployment} />;
} }
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer"> <DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow> <TableRow>
<DeploymentRow deployment={deployment} /> <DeploymentRow deployment={deployment} />
</TableRow> </TableRow>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-3xl">
<DialogHeader> <DialogHeader>
<DialogTitle className="capitalize"> <DialogTitle className="capitalize">
{deployment.environment} Deployment {deployment.environment} Deployment
</DialogTitle> </DialogTitle>
<DialogDescription>Code for your deployment client</DialogDescription> <DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader> </DialogHeader>
<ScrollArea className="max-h-[600px] pr-4"> <ScrollArea className="max-h-[600px] pr-4">
<Tabs defaultValue="client" className="w-full gap-2 text-sm"> <Tabs defaultValue="client" className="w-full gap-2 text-sm">
<TabsList className="grid w-fit grid-cols-3 mb-2"> <TabsList className="grid w-fit grid-cols-3 mb-2">
<TabsTrigger value="client">Server Client</TabsTrigger> <TabsTrigger value="client">Server Client</TabsTrigger>
<TabsTrigger value="js">NodeJS Fetch</TabsTrigger> <TabsTrigger value="js">NodeJS Fetch</TabsTrigger>
<TabsTrigger value="curl">CURL</TabsTrigger> <TabsTrigger value="curl">CURL</TabsTrigger>
</TabsList> </TabsList>
<TabsContent className="flex flex-col gap-2 !mt-0" value="client"> <TabsContent className="flex flex-col gap-2 !mt-0" value="client">
<div> <div>
Copy and paste the ComfyDeployClient form&nbsp; Copy and paste the ComfyDeployClient form&nbsp;
<a <a
href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts" href="https://github.com/BennyKok/comfyui-deploy-next-example/blob/main/src/lib/comfy-deploy.ts"
className="text-blue-500 hover:underline" className="text-blue-500 hover:underline"
target="_blank" rel="noreferrer" target="_blank"
> rel="noreferrer"
here >
</a> here
</div> </a>
<CodeBlock </div>
lang="js" <CodeBlock
code={formatCode( lang="js"
domain == "https://www.comfydeploy.com" code={formatCode(
? jsClientSetupTemplateHostedVersion domain == "https://www.comfydeploy.com"
: jsClientSetupTemplate, ? jsClientSetupTemplateHostedVersion
deployment, : jsClientSetupTemplate,
domain, deployment,
workflowInput domain,
)} workflowInput,
/> )}
Create a run via deployment id />
<CodeBlock Create a run via deployment id
lang="js" <CodeBlock
code={formatCode( lang="js"
workflowInput && workflowInput.length > 0 code={formatCode(
? jsClientCreateRunTemplate workflowInput && workflowInput.length > 0
: jsClientCreateRunNoInputsTemplate, ? jsClientCreateRunTemplate
deployment, : jsClientCreateRunNoInputsTemplate,
domain, deployment,
workflowInput domain,
)} workflowInput,
/> )}
Check the status of the run, and retrieve the outputs />
<CodeBlock Check the status of the run, and retrieve the outputs
lang="js" <CodeBlock
code={formatCode( lang="js"
clientTemplate_checkStatus, code={formatCode(
deployment, clientTemplate_checkStatus,
domain deployment,
)} domain,
/> )}
</TabsContent> />
<TabsContent className="flex flex-col gap-2 !mt-0" value="js"> </TabsContent>
Trigger the workflow <TabsContent className="flex flex-col gap-2 !mt-0" value="js">
<CodeBlock Trigger the workflow
lang="js" <CodeBlock
code={formatCode(jsTemplate, deployment, domain, workflowInput)} lang="js"
/> code={formatCode(jsTemplate, deployment, domain, workflowInput)}
Check the status of the run, and retrieve the outputs />
<CodeBlock Check the status of the run, and retrieve the outputs
lang="js" <CodeBlock
code={formatCode(jsTemplate_checkStatus, deployment, domain)} lang="js"
/> code={formatCode(jsTemplate_checkStatus, deployment, domain)}
</TabsContent> />
<TabsContent className="flex flex-col gap-2 !mt-2" value="curl"> </TabsContent>
<CodeBlock <TabsContent className="flex flex-col gap-2 !mt-2" value="curl">
lang="bash" <CodeBlock
code={formatCode(curlTemplate, deployment, domain)} lang="bash"
/> code={formatCode(curlTemplate, deployment, domain)}
<CodeBlock />
lang="bash" <CodeBlock
code={formatCode(curlTemplate_checkStatus, deployment, domain)} lang="bash"
/> code={formatCode(curlTemplate_checkStatus, deployment, domain)}
</TabsContent> />
</Tabs> </TabsContent>
</ScrollArea> </Tabs>
</DialogContent> </ScrollArea>
</Dialog> </DialogContent>
); </Dialog>
);
} }
function formatCode( function formatCode(
codeTemplate: string, codeTemplate: string,
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0], deployment: Awaited<ReturnType<typeof findAllDeployments>>[0],
domain: string, domain: string,
inputs?: ReturnType<typeof getInputsFromWorkflow>, inputs?: ReturnType<typeof getInputsFromWorkflow>,
inputsTabs?: number inputsTabs?: number,
) { ) {
if (inputs && inputs.length > 0) { if (inputs && inputs.length > 0) {
codeTemplate = codeTemplate.replace( codeTemplate = codeTemplate.replace(
"inputs: {}", "inputs: {}",
`inputs: ${JSON.stringify( `inputs: ${JSON.stringify(
Object.fromEntries( Object.fromEntries(
inputs.map((x) => { inputs.map((x) => {
return [x?.input_id, ""]; return [x?.input_id, ""];
}) }),
), ),
null, null,
2 2,
) )
.split("\n") .split("\n")
.map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line .map((line, index) => (index === 0 ? line : ` ${line}`)) // Add two spaces indentation except for the first line
.join("\n")}` .join("\n")}`,
); );
} else { } else {
codeTemplate = codeTemplate.replace( codeTemplate = codeTemplate.replace(
` `
inputs: {}`, inputs: {}`,
"" "",
); );
} }
return codeTemplate return codeTemplate
.replace("<URL>", `${domain ?? "http://localhost:3000"}/api/run`) .replace("<URL>", `${domain ?? "http://localhost:3000"}/api/run`)
.replace("<ID>", deployment.id) .replace("<ID>", deployment.id)
.replace("<URLONLY>", domain ?? "http://localhost:3000"); .replace("<URLONLY>", domain ?? "http://localhost:3000");
} }

View File

@ -6,55 +6,57 @@ import type { findAllDeployments } from "@/server/findAllRuns";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export function SharePageDeploymentRow({ export function SharePageDeploymentRow({
deployment, deployment,
}: { }: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]; deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) { }) {
const router = useRouter(); const router = useRouter();
return ( return (
<TableRow <TableRow
className="appearance-none hover:cursor-pointer" className="appearance-none hover:cursor-pointer"
onClick={() => { onClick={() => {
if (deployment.environment == "public-share") { if (deployment.environment === "public-share") {
router.push(`/share/${deployment.id}/settings`); router.push(
} `/share/${deployment.share_slug ?? deployment.id}/settings`,
}} );
> }
<TableCell className="capitalize truncate"> }}
{deployment.environment} >
</TableCell> <TableCell className="capitalize truncate">
<TableCell className="font-medium truncate"> {deployment.environment}
{deployment.version?.version} </TableCell>
</TableCell> <TableCell className="font-medium truncate">
<TableCell className="font-medium truncate"> {deployment.version?.version}
{deployment.machine?.name} </TableCell>
</TableCell> <TableCell className="font-medium truncate">
<TableCell className="text-right truncate"> {deployment.machine?.name}
{getRelativeTime(deployment.updated_at)} </TableCell>
</TableCell> <TableCell className="text-right truncate">
</TableRow> {getRelativeTime(deployment.updated_at)}
); </TableCell>
</TableRow>
);
} }
export function DeploymentRow({ export function DeploymentRow({
deployment, deployment,
}: { }: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]; deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) { }) {
return ( return (
<> <>
<TableCell className="capitalize truncate"> <TableCell className="capitalize truncate">
{deployment.environment} {deployment.environment}
</TableCell> </TableCell>
<TableCell className="font-medium truncate"> <TableCell className="font-medium truncate">
{deployment.version?.version} {deployment.version?.version}
</TableCell> </TableCell>
<TableCell className="font-medium truncate"> <TableCell className="font-medium truncate">
{deployment.machine?.name} {deployment.machine?.name}
</TableCell> </TableCell>
<TableCell className="text-right truncate"> <TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)} {getRelativeTime(deployment.updated_at)}
</TableCell> </TableCell>
</> </>
); );
} }

View File

@ -58,13 +58,14 @@ export function SharePageSettings({
type="button" type="button"
> >
<ButtonAction <ButtonAction
routerAction="back"
action={removePublicShareDeployment.bind(null, deployment.id)} action={removePublicShareDeployment.bind(null, deployment.id)}
> >
Remove Remove
</ButtonAction> </ButtonAction>
</Button> </Button>
<Button asChild className="gap-2 truncate" type="button"> <Button asChild className="gap-2 truncate" type="button">
<Link href={`/share/${deployment.id}`} target="_blank"> <Link href={`/share/${deployment.share_slug ?? deployment.id}`} target="_blank">
View Share Page <ExternalLink size={14} /> View Share Page <ExternalLink size={14} />
</Link> </Link>
</Button> </Button>

View File

@ -1,8 +1,5 @@
"use client"; "use client";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { LoadingIcon } from "@/components/LoadingIcon"; import { LoadingIcon } from "@/components/LoadingIcon";
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form"; import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
@ -44,20 +41,16 @@ import { checkStatus, createRun } from "@/server/createRun";
import { createDeployments } from "@/server/curdDeploments"; import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine"; import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion"; import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { import { Copy, ExternalLink, Info, MoreVertical, Play } from "lucide-react";
Copy,
ExternalLink,
Info,
MoreVertical,
Play,
Share,
} from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate"; import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import useSWR from "swr"; import useSWR from "swr";
import type { z } from "zod"; import type { z } from "zod";
import { create } from "zustand"; import { create } from "zustand";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
export function VersionSelect({ export function VersionSelect({
workflow, workflow,
@ -122,12 +115,14 @@ export function MachineSelect({
); );
} }
function useSelectedMachine(machines: Awaited<ReturnType<typeof getMachines>>) { export function useSelectedMachine(
const a = useQueryState("machine", { machines: Awaited<ReturnType<typeof getMachines>>,
defaultValue: machines?.[0]?.id ?? "", ) {
}); const a = useQueryState("machine", {
defaultValue: machines?.[0]?.id ?? "",
});
return a; return a;
} }
type PublicRunStore = { type PublicRunStore = {
@ -373,55 +368,6 @@ export function CreateDeploymentButton({
); );
} }
export function CreateShareButton({
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 [isLoading, setIsLoading] = useState(false);
const workflow_version_id = workflow?.versions.find(
(x) => x.version === version
)?.id;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" disabled={isLoading} variant="outline">
Share {isLoading ? <LoadingIcon /> : <Share size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
onClick={async () => {
if (!workflow_version_id) return;
setIsLoading(true);
await callServerPromise(
createDeployments(
workflow.id,
workflow_version_id,
machine,
"public-share"
)
);
setIsLoading(false);
}}
>
Public
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
export function CopyWorkflowVersion({ export function CopyWorkflowVersion({
workflow, workflow,
}: { }: {

View File

@ -257,30 +257,31 @@ export const showcaseMediaNullable = z
.nullable(); .nullable();
export const deploymentsTable = dbSchema.table("deployments", { export const deploymentsTable = dbSchema.table("deployments", {
id: uuid("id").primaryKey().defaultRandom().notNull(), id: uuid("id").primaryKey().defaultRandom().notNull(),
user_id: text("user_id") user_id: text("user_id")
.references(() => usersTable.id, { .references(() => usersTable.id, {
onDelete: "cascade", onDelete: "cascade",
}) })
.notNull(), .notNull(),
org_id: text("org_id"), org_id: text("org_id"),
workflow_version_id: uuid("workflow_version_id") workflow_version_id: uuid("workflow_version_id")
.notNull() .notNull()
.references(() => workflowVersionTable.id), .references(() => workflowVersionTable.id),
workflow_id: uuid("workflow_id") workflow_id: uuid("workflow_id")
.notNull() .notNull()
.references(() => workflowTable.id, { .references(() => workflowTable.id, {
onDelete: "cascade", onDelete: "cascade",
}), }),
machine_id: uuid("machine_id") machine_id: uuid("machine_id")
.notNull() .notNull()
.references(() => machinesTable.id), .references(() => machinesTable.id),
description: text("description"), share_slug: text("share_slug").unique(),
showcase_media: description: text("description"),
jsonb("showcase_media").$type<z.infer<typeof showcaseMedia>>(), showcase_media:
environment: deploymentEnvironment("environment").notNull(), jsonb("showcase_media").$type<z.infer<typeof showcaseMedia>>(),
created_at: timestamp("created_at").defaultNow().notNull(), environment: deploymentEnvironment("environment").notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(), created_at: timestamp("created_at").defaultNow().notNull(),
updated_at: timestamp("updated_at").defaultNow().notNull(),
}); });
export const publicShareDeployment = z.object({ export const publicShareDeployment = z.object({

View File

@ -7,247 +7,279 @@ import { createNewWorkflow } from "@/server/createNewWorkflow";
import { addCustomMachine } from "@/server/curdMachine"; import { addCustomMachine } from "@/server/curdMachine";
import { withServerPromise } from "@/server/withServerPromise"; import { withServerPromise } from "@/server/withServerPromise";
import { auth } from "@clerk/nextjs"; import { auth } from "@clerk/nextjs";
import { and, eq, isNull } from "drizzle-orm"; import { clerkClient } from "@clerk/nextjs/server";
import slugify from "@sindresorhus/slugify";
import { and, eq, isNull, or } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import "server-only"; import "server-only";
import { validate as isValidUUID } from "uuid";
import type { z } from "zod"; import type { z } from "zod";
export async function createDeployments( export async function createDeployments(
workflow_id: string, workflow_id: string,
version_id: string, version_id: string,
machine_id: string, machine_id: string,
environment: DeploymentType["environment"] environment: DeploymentType["environment"],
) { ) {
const { userId, orgId } = auth(); const { userId, orgId } = auth();
if (!userId) throw new Error("No user id"); if (!userId) throw new Error("No user id");
if (!machine_id) { if (!machine_id) {
throw new Error("No machine id provided"); throw new Error("No machine id provided");
} }
// Same environment and same workflow // Same environment and same workflow
const existingDeployment = await db.query.deploymentsTable.findFirst({ const existingDeployment = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.workflow_id, workflow_id), eq(deploymentsTable.workflow_id, workflow_id),
eq(deploymentsTable.environment, environment) eq(deploymentsTable.environment, environment),
), ),
}); });
if (existingDeployment) { if (existingDeployment) {
await db await db
.update(deploymentsTable) .update(deploymentsTable)
.set({ .set({
workflow_id, workflow_id,
workflow_version_id: version_id, workflow_version_id: version_id,
machine_id, machine_id,
org_id: orgId, org_id: orgId,
}) })
.where(eq(deploymentsTable.id, existingDeployment.id)); .where(eq(deploymentsTable.id, existingDeployment.id));
} else { } else {
await db.insert(deploymentsTable).values({ const workflow = await db.query.workflowTable.findFirst({
user_id: userId, where: eq(workflowTable.id, workflow_id),
workflow_id, with: {
workflow_version_id: version_id, user: {
machine_id, columns: {
environment, name: true,
org_id: orgId, },
}); },
} },
revalidatePath(`/${workflow_id}`); });
return {
message: `Successfully created deployment for ${environment}`, 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;
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() { export async function findAllDeployments() {
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 deployments = await db.query.workflowTable.findMany({ const deployments = await db.query.workflowTable.findMany({
where: and( where: and(
orgId orgId
? eq(workflowTable.org_id, orgId) ? eq(workflowTable.org_id, orgId)
: and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)) : and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id)),
), ),
columns: { columns: {
name: true, name: true,
}, },
with: { with: {
deployments: { deployments: {
columns: { columns: {
environment: true, environment: true,
}, },
with: { with: {
version: { version: {
columns: { columns: {
id: true, id: true,
snapshot: true, snapshot: true,
}, },
}, },
}, },
}, },
}, },
}); });
return deployments; return deployments;
} }
export async function findSharedDeployment(workflow_id: string) { export async function findSharedDeployment(workflow_id: string) {
const deploymentData = await db.query.deploymentsTable.findFirst({ const deploymentData = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, workflow_id) isValidUUID(workflow_id)
), ? eq(deploymentsTable.id, workflow_id)
with: { : eq(deploymentsTable.share_slug, workflow_id),
user: true, ),
machine: true, with: {
workflow: { user: true,
columns: { machine: true,
name: true, workflow: {
org_id: true, columns: {
user_id: true, name: true,
}, org_id: true,
}, user_id: true,
version: true, },
}, },
}); version: true,
},
});
return deploymentData; return deploymentData;
} }
export const removePublicShareDeployment = withServerPromise( export const removePublicShareDeployment = withServerPromise(
async (deployment_id: string) => { async (deployment_id: string) => {
await db const [removed] = await db
.delete(deploymentsTable) .delete(deploymentsTable)
.where( .where(
and( and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id) eq(deploymentsTable.id, deployment_id),
) ),
); ).returning();
}
// revalidatePath(
// `/workflows/${removed.workflow_id}`
// )
},
); );
export const cloneWorkflow = withServerPromise( export const cloneWorkflow = withServerPromise(
async (deployment_id: string) => { async (deployment_id: string) => {
const deployment = await db.query.deploymentsTable.findFirst({ const deployment = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id) eq(deploymentsTable.id, deployment_id),
), ),
with: { with: {
version: true, version: true,
workflow: 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({ await createNewWorkflow({
user_id: userId, user_id: userId,
org_id: orgId, org_id: orgId,
workflow_name: `${deployment.workflow.name} (Cloned)`, workflow_name: `${deployment.workflow.name} (Cloned)`,
workflowData: { workflowData: {
workflow: deployment.version.workflow, workflow: deployment.version.workflow,
workflow_api: deployment?.version.workflow_api, workflow_api: deployment?.version.workflow_api,
snapshot: deployment?.version.snapshot, snapshot: deployment?.version.snapshot,
}, },
}); });
redirect(`/workflows/${deployment.workflow.id}`); redirect(`/workflows/${deployment.workflow.id}`);
return { return {
message: "Successfully cloned workflow", message: "Successfully cloned workflow",
}; };
} },
); );
export const cloneMachine = withServerPromise(async (deployment_id: string) => { export const cloneMachine = withServerPromise(async (deployment_id: string) => {
const deployment = await db.query.deploymentsTable.findFirst({ const deployment = await db.query.deploymentsTable.findFirst({
where: and( where: and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, deployment_id) eq(deploymentsTable.id, deployment_id),
), ),
with: { with: {
machine: true, machine: true,
}, },
}); });
if (!deployment) throw new Error("No deployment found"); if (!deployment) throw new Error("No deployment found");
if (deployment.machine.type !== "comfy-deploy-serverless") if (deployment.machine.type !== "comfy-deploy-serverless")
throw new Error("Can only clone comfy-deploy-serverlesss"); 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({ await addCustomMachine({
gpu: deployment.machine.gpu, gpu: deployment.machine.gpu,
models: deployment.machine.models, models: deployment.machine.models,
snapshot: deployment.machine.snapshot, snapshot: deployment.machine.snapshot,
name: `${deployment.machine.name} (Cloned)`, name: `${deployment.machine.name} (Cloned)`,
type: "comfy-deploy-serverless", type: "comfy-deploy-serverless",
}); });
return { return {
message: "Successfully cloned workflow", message: "Successfully cloned workflow",
}; };
}); });
export async function findUserShareDeployment(share_id: string) { 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 const [deployment] = await db
.select() .select()
.from(deploymentsTable) .from(deploymentsTable)
.where( .where(
and( and(
eq(deploymentsTable.id, share_id), isValidUUID(share_id)
eq(deploymentsTable.environment, "public-share"), ? eq(deploymentsTable.id, share_id)
orgId : eq(deploymentsTable.share_slug, share_id),
? eq(deploymentsTable.org_id, orgId) eq(deploymentsTable.environment, "public-share"),
: and( orgId
eq(deploymentsTable.user_id, userId), ? eq(deploymentsTable.org_id, orgId)
isNull(deploymentsTable.org_id) : 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( export const updateSharePageInfo = withServerPromise(
async ({ async ({
id, id,
...data ...data
}: z.infer<typeof publicShareDeployment> & { }: z.infer<typeof publicShareDeployment> & {
id: string; id: string;
}) => { }) => {
const { userId } = auth(); const { userId } = auth();
if (!userId) return { error: "No user id" }; if (!userId) return { error: "No user id" };
console.log(data); console.log(data);
const [deployment] = await db const [deployment] = await db
.update(deploymentsTable) .update(deploymentsTable)
.set(data) .set(data)
.where( .where(
and( and(
eq(deploymentsTable.environment, "public-share"), eq(deploymentsTable.environment, "public-share"),
eq(deploymentsTable.id, id) eq(deploymentsTable.id, id),
) ),
) )
.returning(); .returning();
return { message: "Info Updated" }; return { message: "Info Updated" };
} },
); );