feat: add public share options for workflow
This commit is contained in:
@@ -3,9 +3,8 @@ import { db } from "@/db/db";
|
||||
import { deploymentsTable, workflowRunsTable } from "@/db/schema";
|
||||
import { createSelectSchema } from "@/lib/drizzle-zod-hono";
|
||||
import { isKeyRevoked } from "@/server/curdApiKeys";
|
||||
import { getRunsData } from "@/server/getRunsOutput";
|
||||
import { getRunsData } from "@/server/getRunsData";
|
||||
import { parseJWT } from "@/server/parseJWT";
|
||||
import { replaceCDNUrl } from "@/server/replaceCDNUrl";
|
||||
import type { ResponseConfig } from "@asteasolutions/zod-to-openapi";
|
||||
import { z, createRoute } from "@hono/zod-openapi";
|
||||
import { OpenAPIHono } from "@hono/zod-openapi";
|
||||
@@ -122,7 +121,7 @@ app.openapi(getOutputRoute, async (c) => {
|
||||
const apiKeyTokenData = c.get("apiKeyTokenData")!;
|
||||
|
||||
try {
|
||||
const run = await getRunsData(apiKeyTokenData, data.run_id);
|
||||
const run = await getRunsData(data.run_id, apiKeyTokenData);
|
||||
|
||||
if (!run)
|
||||
return c.json(
|
||||
@@ -133,29 +132,6 @@ app.openapi(getOutputRoute, async (c) => {
|
||||
400
|
||||
);
|
||||
|
||||
// Fill in the CDN url
|
||||
if (run?.status === "success" && run?.outputs?.length > 0) {
|
||||
for (let i = 0; i < run.outputs.length; i++) {
|
||||
const output = run.outputs[i];
|
||||
|
||||
if (output.data?.images !== undefined) {
|
||||
for (let j = 0; j < output.data?.images.length; j++) {
|
||||
const element = output.data?.images[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${run.id}/${element.filename}`
|
||||
);
|
||||
}
|
||||
} else if (output.data?.files !== undefined) {
|
||||
for (let j = 0; j < output.data?.files.length; j++) {
|
||||
const element = output.data?.files[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${run.id}/${element.filename}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.json(run, 200);
|
||||
} catch (error: any) {
|
||||
return c.json(
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingPageWrapper } from "@/components/LoadingWrapper";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function Loading() {
|
||||
const pathName = usePathname();
|
||||
return <LoadingPageWrapper className="h-full" tag={pathName.toLowerCase()} />;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
PublicRunOutputs,
|
||||
RunWorkflowInline,
|
||||
} from "@/components/VersionSelect";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { db } from "@/db/db";
|
||||
import { usersTable } from "@/db/schema";
|
||||
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
|
||||
import { getRelativeTime } from "@/lib/getRelativeTime";
|
||||
import { setInitialUserData } from "@/lib/setInitialUserData";
|
||||
import { findSharedDeployment } from "@/server/curdDeploments";
|
||||
import { auth, clerkClient } from "@clerk/nextjs/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { share_id: string };
|
||||
}) {
|
||||
const { userId } = await auth();
|
||||
|
||||
// If there is user, check if the user data is present
|
||||
if (userId) {
|
||||
const user = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await setInitialUserData(userId);
|
||||
}
|
||||
}
|
||||
|
||||
const sharedDeployment = await findSharedDeployment(params.share_id);
|
||||
|
||||
if (!sharedDeployment) return redirect("/");
|
||||
|
||||
const userName = sharedDeployment.workflow.org_id
|
||||
? await clerkClient.organizations
|
||||
.getOrganization({
|
||||
organizationId: sharedDeployment.workflow.org_id,
|
||||
})
|
||||
.then((x) => x.name)
|
||||
: sharedDeployment.user.name;
|
||||
|
||||
const inputs = getInputsFromWorkflow(sharedDeployment.version);
|
||||
|
||||
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)]">
|
||||
<Card className="w-full h-fit mt-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{userName}
|
||||
{" / "}
|
||||
{sharedDeployment.workflow.name}
|
||||
</CardTitle>
|
||||
<CardDescription suppressHydrationWarning={true}>
|
||||
{getRelativeTime(sharedDeployment?.updated_at)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<RunWorkflowInline
|
||||
inputs={inputs}
|
||||
machine_id={sharedDeployment.machine_id}
|
||||
workflow_version_id={sharedDeployment.workflow_version_id}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full h-fit mt-4">
|
||||
<CardHeader>
|
||||
<CardDescription>Run outputs</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<PublicRunOutputs />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { VersionDetails } from "@/components/VersionDetails";
|
||||
import {
|
||||
CopyWorkflowVersion,
|
||||
CreateDeploymentButton,
|
||||
CreateShareButton,
|
||||
MachineSelect,
|
||||
RunWorkflowButton,
|
||||
VersionSelect,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
import { getRelativeTime } from "@/lib/getRelativeTime";
|
||||
import { getMachines } from "@/server/curdMachine";
|
||||
import { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
@@ -45,6 +45,7 @@ export default async function Page({
|
||||
<MachineSelect machines={machines} />
|
||||
<RunWorkflowButton workflow={workflow} machines={machines} />
|
||||
<CreateDeploymentButton workflow={workflow} machines={machines} />
|
||||
<CreateShareButton workflow={workflow} machines={machines} />
|
||||
<CopyWorkflowVersion workflow={workflow} />
|
||||
<ViewWorkflowDetailsButton workflow={workflow} />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { setInitialUserData } from "../../../lib/setInitialUserData";
|
||||
import { WorkflowList } from "@/components/WorkflowList";
|
||||
import { db } from "@/db/db";
|
||||
import { usersTable, workflowTable, workflowVersionTable } from "@/db/schema";
|
||||
import { auth, clerkClient } from "@clerk/nextjs";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { and, desc, eq, isNull } from "drizzle-orm";
|
||||
|
||||
export default function Home() {
|
||||
@@ -55,26 +56,3 @@ async function WorkflowServer() {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function setInitialUserData(userId: string) {
|
||||
const user = await clerkClient.users.getUser(userId);
|
||||
|
||||
// incase we dont have username such as google login, fallback to first name + last name
|
||||
const usernameFallback =
|
||||
user.username ?? (user.firstName ?? "") + (user.lastName ?? "");
|
||||
|
||||
// For the display name, if it for some reason is empty, fallback to username
|
||||
let nameFallback = (user.firstName ?? "") + (user.lastName ?? "");
|
||||
if (nameFallback === "") {
|
||||
nameFallback = usernameFallback;
|
||||
}
|
||||
|
||||
const result = await db.insert(usersTable).values({
|
||||
id: userId,
|
||||
// this is used for path, make sure this is unique
|
||||
username: usernameFallback,
|
||||
|
||||
// this is for display name, maybe different from username
|
||||
name: nameFallback,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CodeBlock } from "@/components/CodeBlock";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -13,7 +14,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
|
||||
import { getRelativeTime } from "@/lib/getRelativeTime";
|
||||
import type { findAllDeployments } from "@/server/findAllRuns";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { headers } from "next/headers";
|
||||
import Link from "next/link";
|
||||
|
||||
const curlTemplate = `
|
||||
curl --request POST \
|
||||
@@ -72,7 +75,9 @@ export function DeploymentDisplay({
|
||||
<Dialog>
|
||||
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
||||
<TableRow>
|
||||
<TableCell className="capitalize">{deployment.environment}</TableCell>
|
||||
<TableCell className="capitalize truncate">
|
||||
{deployment.environment}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium truncate">
|
||||
{deployment.version?.version}
|
||||
</TableCell>
|
||||
@@ -92,34 +97,53 @@ export function DeploymentDisplay({
|
||||
<DialogDescription>Code for your deployment client</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[600px]">
|
||||
<Tabs defaultValue="js" className="w-full">
|
||||
<TabsList className="grid w-fit grid-cols-2">
|
||||
<TabsTrigger value="js">js</TabsTrigger>
|
||||
<TabsTrigger value="curl">curl</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="flex flex-col gap-2" 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" value="curl">
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={formatCode(curlTemplate, deployment, domain)}
|
||||
/>
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={formatCode(curlTemplate_checkStatus, deployment, domain)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
{deployment.environment !== "public-share" ? (
|
||||
<Tabs defaultValue="js" className="w-full">
|
||||
<TabsList className="grid w-fit grid-cols-2">
|
||||
<TabsTrigger value="js">js</TabsTrigger>
|
||||
<TabsTrigger value="curl">curl</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent className="flex flex-col gap-2" 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" value="curl">
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={formatCode(curlTemplate, deployment, domain)}
|
||||
/>
|
||||
<CodeBlock
|
||||
lang="bash"
|
||||
code={formatCode(
|
||||
curlTemplate_checkStatus,
|
||||
deployment,
|
||||
domain
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<div className="w-full text-right">
|
||||
<Button asChild className="gap-2">
|
||||
<Link href={`/share/${deployment.id}`} target="_blank">
|
||||
View Share Page <ExternalLink size={14} />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "./ui/badge";
|
||||
import macBookMainImage from "@/assets/images/macbook-main.png";
|
||||
import { Section } from "@/components/Section";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
import { Fragment } from "react";
|
||||
import meta from 'next-gen/config';
|
||||
import meta from "next-gen/config";
|
||||
|
||||
function isDevelopment() {
|
||||
return process.env.NODE_ENV === "development";
|
||||
}
|
||||
|
||||
function FeatureCard(props: {
|
||||
className?: String;
|
||||
className?: string;
|
||||
title: React.ReactNode;
|
||||
description: String;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative text-center bg-opacity-20 rounded-lg py-6 ring-1 shadow-sm ring-stone-200/50 overflow-hidden",
|
||||
"group relative text-center bg-opacity-20 rounded-lg py-6 ring-1 shadow-sm ring-stone-200/50 overflow-hidden"
|
||||
// props.className,
|
||||
)}
|
||||
>
|
||||
@@ -28,14 +24,14 @@ function FeatureCard(props: {
|
||||
<div
|
||||
className={cn(
|
||||
"opacity-60 group-hover:opacity-100 transition-all -z-[5] absolute top-0 h-full w-full duration-700",
|
||||
props.className,
|
||||
props.className
|
||||
)}
|
||||
></div>
|
||||
<div className="opacity-60 group-hover:opacity-100 absolute top-0 inset-0 -z-[5] h-full w-full bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]"></div>
|
||||
/>
|
||||
<div className="opacity-60 group-hover:opacity-100 absolute top-0 inset-0 -z-[5] h-full w-full bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] [background-size:16px_16px] [mask-image:radial-gradient(ellipse_50%_50%_at_50%_50%,#000_70%,transparent_100%)]" />
|
||||
|
||||
<div className="">
|
||||
<div className="font-mono text-lg ">{props.title}</div>
|
||||
<div className="divider px-4 py-0 h-[1px] opacity-30 my-2"></div>
|
||||
<div className="divider px-4 py-0 h-[1px] opacity-30 my-2" />
|
||||
<div className="px-8 text-stone-800 ">{props.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -86,7 +82,6 @@ export default function Main() {
|
||||
></Image> */}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
</div>
|
||||
|
||||
<footer className="text-base-content mx-auto flex flex-col md:flex-row items-center justify-center w-full max-w-5xl gap-4 p-10 ">
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
|
||||
export function Navbar() {
|
||||
const _isDesktop = useMediaQuery("(min-width: 768px)");
|
||||
const _isDesktop = useMediaQuery("(min-width: 1024px)");
|
||||
const [isDesktop, setIsDesktop] = useState(true);
|
||||
useEffect(() => {
|
||||
setIsDesktop(_isDesktop);
|
||||
|
||||
@@ -34,13 +34,19 @@ export function NavbarMenu({ className }: { className?: string }) {
|
||||
<Tabs
|
||||
defaultValue={pathname}
|
||||
className="w-[300px] hidden lg:flex pointer-events-auto"
|
||||
onValueChange={(value) => {
|
||||
router.push(value);
|
||||
}}
|
||||
// onValueChange={(value) => {
|
||||
|
||||
// }}
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
{pages.map((page) => (
|
||||
<TabsTrigger key={page.name} value={page.path}>
|
||||
<TabsTrigger
|
||||
key={page.name}
|
||||
value={page.path}
|
||||
onClick={() => {
|
||||
router.push(page.path);
|
||||
}}
|
||||
>
|
||||
{page.name}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
plainInputsToZod,
|
||||
workflowVersionInputsToZod,
|
||||
} from "../lib/workflowVersionInputsToZod";
|
||||
import { callServerPromise } from "./callServerPromise";
|
||||
import fetcher from "./fetcher";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
@@ -29,6 +33,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -38,17 +43,26 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { workflowAPINodeType } from "@/db/schema";
|
||||
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
|
||||
import { createRun } from "@/server/createRun";
|
||||
import type { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
|
||||
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 { useAuth, useClerk } from "@clerk/nextjs";
|
||||
import {
|
||||
Copy,
|
||||
ExternalLink,
|
||||
Info,
|
||||
MoreVertical,
|
||||
Play,
|
||||
Share,
|
||||
} from "lucide-react";
|
||||
import { parseAsInteger, useQueryState } from "next-usequerystate";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
import { z } from "zod";
|
||||
import type { z } from "zod";
|
||||
import { create } from "zustand";
|
||||
|
||||
export function VersionSelect({
|
||||
workflow,
|
||||
@@ -121,6 +135,168 @@ function useSelectedMachine(machines: Awaited<ReturnType<typeof getMachines>>) {
|
||||
return a;
|
||||
}
|
||||
|
||||
type PublicRunStore = {
|
||||
image: string;
|
||||
loading: boolean;
|
||||
runId: string;
|
||||
status: string;
|
||||
|
||||
setImage: (image: string) => void;
|
||||
setLoading: (loading: boolean) => void;
|
||||
setRunId: (runId: string) => void;
|
||||
setStatus: (status: string) => void;
|
||||
};
|
||||
|
||||
const publicRunStore = create<PublicRunStore>((set) => ({
|
||||
image: "",
|
||||
loading: false,
|
||||
runId: "",
|
||||
status: "",
|
||||
|
||||
setImage: (image) => set({ image }),
|
||||
setLoading: (loading) => set({ loading }),
|
||||
setRunId: (runId) => set({ runId }),
|
||||
setStatus: (status) => set({ status }),
|
||||
}));
|
||||
|
||||
export function PublicRunOutputs() {
|
||||
const { image, loading, runId, status, setStatus, setImage, setLoading } =
|
||||
publicRunStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!runId) return;
|
||||
const interval = setInterval(() => {
|
||||
checkStatus(runId).then((res) => {
|
||||
console.log(res?.status);
|
||||
if (res) setStatus(res.status);
|
||||
if (res && res.status === "success") {
|
||||
setImage(res.outputs[0]?.data.images[0].url);
|
||||
setLoading(false);
|
||||
clearInterval(interval);
|
||||
}
|
||||
});
|
||||
}, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [runId]);
|
||||
|
||||
return (
|
||||
<div className="border border-gray-200 w-full square h-[400px] rounded-lg relative">
|
||||
{!loading && image && (
|
||||
<img
|
||||
className="w-full h-full object-contain"
|
||||
src={image}
|
||||
alt="Generated image"
|
||||
/>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="absolute top-0 left-0 w-full h-full flex items-center justify-center gap-2">
|
||||
{status} <LoadingIcon />
|
||||
</div>
|
||||
)}
|
||||
{loading && <Skeleton className="w-full h-full" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RunWorkflowInline({
|
||||
inputs,
|
||||
workflow_version_id,
|
||||
machine_id,
|
||||
}: {
|
||||
inputs: ReturnType<typeof getInputsFromWorkflow>;
|
||||
workflow_version_id: string;
|
||||
machine_id: string;
|
||||
}) {
|
||||
const [values, setValues] = useState<Record<string, string>>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const user = useAuth();
|
||||
const clerk = useClerk();
|
||||
|
||||
const schema = useMemo(() => {
|
||||
return plainInputsToZod(inputs);
|
||||
}, [inputs]);
|
||||
|
||||
const {
|
||||
setRunId,
|
||||
loading,
|
||||
setLoading: setLoading2,
|
||||
setStatus,
|
||||
} = publicRunStore();
|
||||
|
||||
const runWorkflow = async () => {
|
||||
console.log();
|
||||
|
||||
if (!user.isSignedIn) {
|
||||
clerk.openSignIn({
|
||||
redirectUrl: window.location.href,
|
||||
});
|
||||
console.log("hi");
|
||||
return;
|
||||
}
|
||||
console.log(values);
|
||||
|
||||
const val = Object.keys(values).length > 0 ? values : undefined;
|
||||
setLoading2(true);
|
||||
setIsLoading(true);
|
||||
setStatus("preparing");
|
||||
try {
|
||||
const origin = window.location.origin;
|
||||
const a = await callServerPromise(
|
||||
createRun({
|
||||
origin,
|
||||
workflow_version_id: workflow_version_id,
|
||||
machine_id: machine_id,
|
||||
inputs: val,
|
||||
isManualRun: true,
|
||||
})
|
||||
);
|
||||
if (a && !("error" in a)) {
|
||||
setRunId(a.workflow_run_id);
|
||||
} else {
|
||||
setLoading2(false);
|
||||
}
|
||||
console.log(a);
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
setIsLoading(false);
|
||||
setLoading2(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{schema && (
|
||||
<AutoForm
|
||||
formSchema={schema}
|
||||
values={values}
|
||||
onValuesChange={setValues}
|
||||
onSubmit={runWorkflow}
|
||||
className="px-1"
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<AutoFormSubmit disabled={isLoading || loading}>
|
||||
Run
|
||||
<span className="ml-2">
|
||||
{isLoading || loading ? <LoadingIcon /> : <Play size={14} />}
|
||||
</span>
|
||||
</AutoFormSubmit>
|
||||
</div>
|
||||
</AutoForm>
|
||||
)}
|
||||
{!schema && (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isLoading || loading}
|
||||
onClick={runWorkflow}
|
||||
>
|
||||
Confirm {isLoading || loading ? <LoadingIcon /> : <Play size={14} />}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function RunWorkflowButton({
|
||||
workflow,
|
||||
machines,
|
||||
@@ -143,17 +319,10 @@ export function RunWorkflowButton({
|
||||
workflow,
|
||||
version
|
||||
);
|
||||
const inputs = getInputsFromWorkflow(workflow_version);
|
||||
|
||||
if (!inputs) return null;
|
||||
if (!workflow_version) return null;
|
||||
|
||||
return z.object({
|
||||
...Object.fromEntries(
|
||||
inputs?.map((x) => {
|
||||
return [x?.input_id, z.string().optional()];
|
||||
})
|
||||
),
|
||||
});
|
||||
return workflowVersionInputsToZod(workflow_version);
|
||||
}, [version]);
|
||||
|
||||
const runWorkflow = async () => {
|
||||
@@ -228,8 +397,6 @@ export function RunWorkflowButton({
|
||||
Confirm {isLoading ? <LoadingIcon /> : <Play size={14} />}
|
||||
</Button>
|
||||
)}
|
||||
{/* </div> */}
|
||||
{/* <div className="max-h-96 overflow-y-scroll">{view}</div> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -301,6 +468,55 @@ 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,
|
||||
}: {
|
||||
|
||||
@@ -13,8 +13,18 @@ import type { DefaultValues } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
|
||||
export function AutoFormSubmit({ children }: { children?: React.ReactNode }) {
|
||||
return <Button type="submit">{children ?? "Submit"}</Button>;
|
||||
export function AutoFormSubmit({
|
||||
children,
|
||||
disabled,
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Button type="submit" disabled={disabled}>
|
||||
{children ?? "Submit"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function AutoForm<SchemaType extends ZodObjectOrWrapped>({
|
||||
|
||||
@@ -97,6 +97,7 @@ export const workflowRunStatus = pgEnum("workflow_run_status", [
|
||||
export const deploymentEnvironment = pgEnum("deployment_environment", [
|
||||
"staging",
|
||||
"production",
|
||||
"public-share",
|
||||
]);
|
||||
|
||||
export const workflowRunOrigin = pgEnum("workflow_run_origin", [
|
||||
@@ -265,6 +266,10 @@ export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({
|
||||
fields: [deploymentsTable.workflow_id],
|
||||
references: [workflowTable.id],
|
||||
}),
|
||||
user: one(usersTable, {
|
||||
fields: [deploymentsTable.user_id],
|
||||
references: [usersTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const apiKeyTable = dbSchema.table("api_keys", {
|
||||
@@ -286,3 +291,4 @@ export type UserType = InferSelectModel<typeof usersTable>;
|
||||
export type WorkflowType = InferSelectModel<typeof workflowTable>;
|
||||
export type MachineType = InferSelectModel<typeof machinesTable>;
|
||||
export type WorkflowVersionType = InferSelectModel<typeof workflowVersionTable>;
|
||||
export type DeploymentType = InferSelectModel<typeof deploymentsTable>;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { db } from "@/db/db";
|
||||
import { usersTable } from "@/db/schema";
|
||||
import { clerkClient } from "@clerk/nextjs";
|
||||
|
||||
export async function setInitialUserData(userId: string) {
|
||||
const user = await clerkClient.users.getUser(userId);
|
||||
|
||||
// incase we dont have username such as google login, fallback to first name + last name
|
||||
const usernameFallback = user.username ?? (user.firstName ?? "") + (user.lastName ?? "");
|
||||
|
||||
// For the display name, if it for some reason is empty, fallback to username
|
||||
let nameFallback = (user.firstName ?? "") + (user.lastName ?? "");
|
||||
if (nameFallback === "") {
|
||||
nameFallback = usernameFallback;
|
||||
}
|
||||
|
||||
const result = await db.insert(usersTable).values({
|
||||
id: userId,
|
||||
// this is used for path, make sure this is unique
|
||||
username: usernameFallback,
|
||||
|
||||
// this is for display name, maybe different from username
|
||||
name: nameFallback,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { WorkflowVersionType } from "@/db/schema";
|
||||
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
|
||||
import { z } from "zod";
|
||||
|
||||
export function workflowVersionInputsToZod(
|
||||
workflow_version: WorkflowVersionType
|
||||
) {
|
||||
const inputs = getInputsFromWorkflow(workflow_version);
|
||||
return plainInputsToZod(inputs);
|
||||
}
|
||||
|
||||
export function plainInputsToZod(
|
||||
inputs: ReturnType<typeof getInputsFromWorkflow>
|
||||
) {
|
||||
if (!inputs) return null;
|
||||
|
||||
return z.object({
|
||||
...Object.fromEntries(
|
||||
inputs?.map((x) => {
|
||||
return [x?.input_id, z.string().optional()];
|
||||
})
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { authMiddleware, redirectToSignIn } from "@clerk/nextjs";
|
||||
// See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware
|
||||
export default authMiddleware({
|
||||
// debug: true,
|
||||
publicRoutes: ["/", "/api/(.*)", "/docs(.*)"],
|
||||
publicRoutes: ["/", "/api/(.*)", "/docs(.*)", "/share(.*)"],
|
||||
// publicRoutes: ["/", "/(.*)"],
|
||||
async afterAuth(auth, req, evt) {
|
||||
// redirect them to organization selection page
|
||||
|
||||
@@ -5,7 +5,9 @@ import { db } from "@/db/db";
|
||||
import type { MachineType, WorkflowVersionType } from "@/db/schema";
|
||||
import { machinesTable, workflowRunsTable } from "@/db/schema";
|
||||
import type { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
||||
import { getRunsData } from "@/server/getRunsData";
|
||||
import { ComfyAPI_Run } from "@/types/ComfyAPI_Run";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import "server-only";
|
||||
@@ -219,3 +221,10 @@ export const createRun = withServerPromise(
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export async function checkStatus(run_id: string) {
|
||||
const { userId } = auth();
|
||||
if (!userId) throw new Error("User not found");
|
||||
|
||||
return await getRunsData(run_id);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { db } from "@/db/db";
|
||||
import type { DeploymentType } from "@/db/schema";
|
||||
import { deploymentsTable, workflowTable } from "@/db/schema";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
@@ -11,7 +12,7 @@ export async function createDeployments(
|
||||
workflow_id: string,
|
||||
version_id: string,
|
||||
machine_id: string,
|
||||
environment: "production" | "staging"
|
||||
environment: DeploymentType["environment"]
|
||||
) {
|
||||
const { userId } = auth();
|
||||
if (!userId) throw new Error("No user id");
|
||||
@@ -80,3 +81,26 @@ export async function findAllDeployments() {
|
||||
|
||||
return deployments;
|
||||
}
|
||||
|
||||
export async function findSharedDeployment(workflow_id: string) {
|
||||
const deploymentData = await db.query.deploymentsTable.findFirst({
|
||||
where: and(
|
||||
eq(deploymentsTable.environment, "public-share"),
|
||||
eq(deploymentsTable.id, workflow_id)
|
||||
),
|
||||
with: {
|
||||
user: true,
|
||||
machine: true,
|
||||
workflow: {
|
||||
columns: {
|
||||
name: true,
|
||||
org_id: true,
|
||||
user_id: true,
|
||||
},
|
||||
},
|
||||
version: true,
|
||||
},
|
||||
});
|
||||
|
||||
return deploymentData;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { db } from "@/db/db";
|
||||
import { workflowRunsTable } from "@/db/schema";
|
||||
import type { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
||||
import { replaceCDNUrl } from "@/server/replaceCDNUrl";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
export async function getRunsData(run_id: string, user?: APIKeyUserType) {
|
||||
const data = await db.query.workflowRunsTable.findFirst({
|
||||
where: and(eq(workflowRunsTable.id, run_id)),
|
||||
with: {
|
||||
workflow: {
|
||||
columns: {
|
||||
org_id: true,
|
||||
user_id: true,
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
columns: {
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
if (user.org_id) {
|
||||
// is org api call, check org only
|
||||
if (data.workflow.org_id != user.org_id) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// is user api call, check user only
|
||||
if (
|
||||
data.workflow.user_id != user.user_id &&
|
||||
data.workflow.org_id == null
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data) {
|
||||
// Fill in the CDN url
|
||||
if (data?.status === "success" && data?.outputs?.length > 0) {
|
||||
for (let i = 0; i < data.outputs.length; i++) {
|
||||
const output = data.outputs[i];
|
||||
|
||||
if (output.data?.images !== undefined) {
|
||||
for (let j = 0; j < output.data?.images.length; j++) {
|
||||
const element = output.data?.images[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
||||
);
|
||||
}
|
||||
} else if (output.data?.files !== undefined) {
|
||||
for (let j = 0; j < output.data?.files.length; j++) {
|
||||
const element = output.data?.files[j];
|
||||
element.url = replaceCDNUrl(
|
||||
`${process.env.SPACES_ENDPOINT}/${process.env.SPACES_BUCKET}/outputs/runs/${data.id}/${element.filename}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import { RunOutputs } from "@/components/RunOutputs";
|
||||
import { db } from "@/db/db";
|
||||
import { workflowRunOutputs, workflowRunsTable } from "@/db/schema";
|
||||
import type { APIKeyUserType } from "@/server/APIKeyBodyRequest";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { workflowRunOutputs } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function getRunsOutputDisplay(run_id: string) {
|
||||
return <RunOutputs run_id={run_id} />;
|
||||
@@ -17,40 +16,3 @@ export async function getRunsOutput(run_id: string) {
|
||||
.from(workflowRunOutputs)
|
||||
.where(eq(workflowRunOutputs.run_id, run_id));
|
||||
}
|
||||
|
||||
export async function getRunsData(user: APIKeyUserType, run_id: string) {
|
||||
const data = await db.query.workflowRunsTable.findFirst({
|
||||
where: and(eq(workflowRunsTable.id, run_id)),
|
||||
with: {
|
||||
workflow: {
|
||||
columns: {
|
||||
org_id: true,
|
||||
user_id: true,
|
||||
},
|
||||
},
|
||||
outputs: {
|
||||
columns: {
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user.org_id) {
|
||||
// is org api call, check org only
|
||||
if (data.workflow.org_id != user.org_id) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// is user api call, check user only
|
||||
if (data.workflow.user_id != user.user_id && data.workflow.org_id == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user