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
+69 -63
View File
@@ -4,10 +4,10 @@ import { LoadingIcon } from "@/components/LoadingIcon";
import { callServerPromise } from "@/components/callServerPromise";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useAuth, useClerk } from "@clerk/nextjs";
import { MoreVertical } from "lucide-react";
@@ -15,74 +15,80 @@ import { useRouter } from "next/navigation";
import { useState } from "react";
export function ButtonAction({
action,
children,
...rest
action,
children,
routerAction = "back",
...rest
}: {
action: () => Promise<any>;
children: React.ReactNode;
action: () => Promise<any>;
routerAction?: "refresh" | "back";
children: React.ReactNode;
}) {
const [pending, setPending] = useState(false);
const router = useRouter();
const [pending, setPending] = useState(false);
const router = useRouter();
return (
<button
onClick={async () => {
if (pending) return;
return (
<button
onClick={async () => {
if (pending) return;
setPending(true);
await callServerPromise(action());
setPending(false);
setPending(true);
await callServerPromise(action());
setPending(false);
router.refresh();
}}
{...rest}
>
{children} {pending && <LoadingIcon />}
</button>
);
if (routerAction === "back") {
router.back();
router.refresh();
}
else if (routerAction === "refresh") router.refresh();
}}
{...rest}
>
{children} {pending && <LoadingIcon />}
</button>
);
}
export function ButtonActionMenu(props: {
title?: string;
actions: {
title: string;
action: () => Promise<any>;
}[];
title?: string;
actions: {
title: string;
action: () => Promise<any>;
}[];
}) {
const user = useAuth();
const [isLoading, setIsLoading] = useState(false);
const clerk = useClerk();
const user = useAuth();
const [isLoading, setIsLoading] = useState(false);
const clerk = useClerk();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" variant="outline" disabled={isLoading}>
{props.title}
{isLoading ? <LoadingIcon /> : <MoreVertical size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
{props.actions.map((action) => (
<DropdownMenuItem
key={action.title}
onClick={async () => {
if (!user.isSignedIn) {
clerk.openSignIn({
redirectUrl: window.location.href,
});
return;
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button className="gap-2" variant="outline" disabled={isLoading}>
{props.title}
{isLoading ? <LoadingIcon /> : <MoreVertical size={14} />}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
{props.actions.map((action) => (
<DropdownMenuItem
key={action.title}
onClick={async () => {
if (!user.isSignedIn) {
clerk.openSignIn({
redirectUrl: window.location.href,
});
return;
}
setIsLoading(true);
await callServerPromise(action.action());
setIsLoading(false);
}}
>
{action.title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
setIsLoading(true);
await callServerPromise(action.action());
setIsLoading(false);
}}
>
{action.title}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
+66
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>
);
}
+139 -138
View File
@@ -1,18 +1,18 @@
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
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";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
import type { findAllDeployments } from "@/server/findAllRuns";
import { DeploymentRow, SharePageDeploymentRow } from "./DeploymentRow";
const curlTemplate = `
curl --request POST \
@@ -83,144 +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");
}
+46 -44
View File
@@ -6,55 +6,57 @@ import type { findAllDeployments } from "@/server/findAllRuns";
import { useRouter } from "next/navigation";
export function SharePageDeploymentRow({
deployment,
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
const router = useRouter();
return (
<TableRow
className="appearance-none hover:cursor-pointer"
onClick={() => {
if (deployment.environment == "public-share") {
router.push(`/share/${deployment.id}/settings`);
}
}}
>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
);
const router = useRouter();
return (
<TableRow
className="appearance-none hover:cursor-pointer"
onClick={() => {
if (deployment.environment === "public-share") {
router.push(
`/share/${deployment.share_slug ?? deployment.id}/settings`,
);
}
}}
>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
);
}
export function DeploymentRow({
deployment,
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
return (
<>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</>
);
return (
<>
<TableCell className="capitalize truncate">
{deployment.environment}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium truncate">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right truncate">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</>
);
}
+2 -1
View File
@@ -58,13 +58,14 @@ export function SharePageSettings({
type="button"
>
<ButtonAction
routerAction="back"
action={removePublicShareDeployment.bind(null, deployment.id)}
>
Remove
</ButtonAction>
</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} />
</Link>
</Button>
+11 -65
View File
@@ -1,8 +1,5 @@
"use client";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
import { LoadingIcon } from "@/components/LoadingIcon";
import AutoForm, { AutoFormSubmit } from "@/components/ui/auto-form";
import { Badge } from "@/components/ui/badge";
@@ -44,20 +41,16 @@ 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,
Share,
} from "lucide-react";
import { Copy, ExternalLink, Info, MoreVertical, Play } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import useSWR from "swr";
import type { z } from "zod";
import { create } from "zustand";
import { workflowVersionInputsToZod } from "../lib/workflowVersionInputsToZod";
import { callServerPromise } from "./callServerPromise";
import fetcher from "./fetcher";
export function VersionSelect({
workflow,
@@ -122,12 +115,14 @@ export function MachineSelect({
);
}
function useSelectedMachine(machines: Awaited<ReturnType<typeof getMachines>>) {
const a = useQueryState("machine", {
defaultValue: machines?.[0]?.id ?? "",
});
export function useSelectedMachine(
machines: Awaited<ReturnType<typeof getMachines>>,
) {
const a = useQueryState("machine", {
defaultValue: machines?.[0]?.id ?? "",
});
return a;
return a;
}
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({
workflow,
}: {