feat(docs): add docs and restructure
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { APIKeyList } from "@/components/APIKeyList";
|
||||
import { getAPIKeys } from "@/server/curdApiKeys";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
|
||||
export default function Page() {
|
||||
return <Component />;
|
||||
}
|
||||
|
||||
async function Component() {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return <div>No auth</div>;
|
||||
}
|
||||
|
||||
const workflow = await getAPIKeys();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<APIKeyList
|
||||
data={workflow.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
date: x.updated_at,
|
||||
endpoint: `****${x.key.slice(-4)}`,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
||||
import { handleResourceUpload } from "@/server/resource";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const Request = z.object({
|
||||
file_name: z.string(),
|
||||
run_id: z.string(),
|
||||
type: z.string(),
|
||||
});
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const [data, error] = await parseDataSafe(Request, request);
|
||||
if (!data || error) return error;
|
||||
|
||||
const { file_name, run_id, type } = data;
|
||||
|
||||
try {
|
||||
const uploadUrl = await handleResourceUpload({
|
||||
resourceBucket: process.env.SPACES_BUCKET,
|
||||
resourceId: `outputs/runs/${run_id}/${file_name}`,
|
||||
resourceType: type,
|
||||
isPublic: true,
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
url: uploadUrl,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Unknown error";
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: errorMessage,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
||||
import { createRun } from "../../../../server/createRun";
|
||||
import { db } from "@/db/db";
|
||||
import { deploymentsTable } from "@/db/schema";
|
||||
import { isKeyRevoked } from "@/server/curdApiKeys";
|
||||
import { getRunsData } from "@/server/getRunsOutput";
|
||||
import { parseJWT } from "@/server/parseJWT";
|
||||
import { replaceCDNUrl } from "@/server/replaceCDNUrl";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const Request = z.object({
|
||||
deployment_id: z.string(),
|
||||
inputs: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
const Request2 = z.object({
|
||||
run_id: z.string(),
|
||||
});
|
||||
|
||||
async function checkToken(request: Request) {
|
||||
const token = request.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token"
|
||||
const userData = token ? parseJWT(token) : undefined;
|
||||
if (!userData || token === undefined) {
|
||||
return new NextResponse("Invalid or expired token", {
|
||||
status: 401,
|
||||
});
|
||||
} else {
|
||||
const revokedKey = await isKeyRevoked(token);
|
||||
if (revokedKey)
|
||||
return new NextResponse("Revoked token", {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const invalidRequest = await checkToken(request);
|
||||
if (invalidRequest) return invalidRequest;
|
||||
|
||||
const [data, error] = await parseDataSafe(Request2, request);
|
||||
if (!data || error) return error;
|
||||
|
||||
const run = await getRunsData(data.run_id);
|
||||
|
||||
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 NextResponse.json(run, {
|
||||
status: 200,
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const invalidRequest = await checkToken(request);
|
||||
if (invalidRequest) return invalidRequest;
|
||||
|
||||
const [data, error] = await parseDataSafe(Request, request);
|
||||
if (!data || error) return error;
|
||||
|
||||
const origin = new URL(request.url).origin;
|
||||
|
||||
const { deployment_id, inputs } = data;
|
||||
|
||||
try {
|
||||
const deploymentData = await db.query.deploymentsTable.findFirst({
|
||||
where: eq(deploymentsTable.id, deployment_id),
|
||||
});
|
||||
|
||||
if (!deploymentData) throw new Error("Deployment not found");
|
||||
|
||||
const run_id = await createRun(
|
||||
origin,
|
||||
deploymentData.workflow_version_id,
|
||||
deploymentData.machine_id,
|
||||
inputs
|
||||
);
|
||||
|
||||
if ("error" in run_id) throw new Error(run_id.error);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
run_id: "workflow_run_id" in run_id ? run_id.workflow_run_id : "",
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.message,
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { parseDataSafe } from "../../../../lib/parseDataSafe";
|
||||
import { db } from "@/db/db";
|
||||
import { workflowRunOutputs, workflowRunsTable } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const Request = z.object({
|
||||
run_id: z.string(),
|
||||
status: z
|
||||
.enum(["not-started", "running", "uploading", "success", "failed"])
|
||||
.optional(),
|
||||
output_data: z.any().optional(),
|
||||
});
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const [data, error] = await parseDataSafe(Request, request);
|
||||
if (!data || error) return error;
|
||||
|
||||
const { run_id, status, output_data } = data;
|
||||
|
||||
// console.log(run_id, status, output_data);
|
||||
|
||||
if (output_data) {
|
||||
const workflow_run_output = await db.insert(workflowRunOutputs).values({
|
||||
run_id: run_id,
|
||||
data: output_data,
|
||||
});
|
||||
} else if (status) {
|
||||
// console.log("status", status);
|
||||
const workflow_run = await db
|
||||
.update(workflowRunsTable)
|
||||
.set({
|
||||
status: status,
|
||||
ended_at:
|
||||
status === "success" || status === "failed" ? new Date() : null,
|
||||
})
|
||||
.where(eq(workflowRunsTable.id, run_id))
|
||||
.returning();
|
||||
}
|
||||
|
||||
// const workflow_version = await db.query.workflowVersionTable.findFirst({
|
||||
// where: eq(workflowRunsTable.id, workflow_run[0].workflow_version_id),
|
||||
// });
|
||||
|
||||
// revalidatePath(`./${workflow_version?.workflow_id}`);
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
message: "success",
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { parseJWT } from "../../../../server/parseJWT";
|
||||
import { db } from "@/db/db";
|
||||
import {
|
||||
workflowAPIType,
|
||||
workflowTable,
|
||||
workflowType,
|
||||
workflowVersionTable,
|
||||
} from "@/db/schema";
|
||||
import { parseDataSafe } from "@/lib/parseDataSafe";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
|
||||
const corsHeaders = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
};
|
||||
|
||||
const UploadRequest = z.object({
|
||||
// user_id: z.string(),
|
||||
workflow_id: z.string().optional(),
|
||||
workflow_name: z.string().optional(),
|
||||
workflow: workflowType,
|
||||
workflow_api: workflowAPIType,
|
||||
});
|
||||
|
||||
export async function OPTIONS(request: Request) {
|
||||
return new Response(null, {
|
||||
status: 204,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const token = request.headers.get("Authorization")?.split(" ")?.[1]; // Assuming token is sent as "Bearer your_token"
|
||||
const userData = token ? parseJWT(token) : undefined;
|
||||
if (!userData) {
|
||||
return new NextResponse("Invalid or expired token", {
|
||||
status: 401,
|
||||
headers: corsHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
const { user_id, org_id } = userData;
|
||||
|
||||
if (!user_id) return new NextResponse("Invalid user_id", { status: 401 });
|
||||
|
||||
const [data, error] = await parseDataSafe(
|
||||
UploadRequest,
|
||||
request,
|
||||
corsHeaders
|
||||
);
|
||||
|
||||
if (!data || error) return error;
|
||||
|
||||
const {
|
||||
// user_id,
|
||||
workflow,
|
||||
workflow_api,
|
||||
workflow_id: _workflow_id,
|
||||
workflow_name,
|
||||
} = data;
|
||||
|
||||
let workflow_id = _workflow_id;
|
||||
|
||||
let version = -1;
|
||||
|
||||
// Case 1 new workflow
|
||||
try {
|
||||
if ((!workflow_id || workflow_id.length == 0) && workflow_name) {
|
||||
// Create a new parent workflow
|
||||
const workflow_parent = await db
|
||||
.insert(workflowTable)
|
||||
.values({
|
||||
user_id,
|
||||
name: workflow_name,
|
||||
})
|
||||
.returning();
|
||||
|
||||
workflow_id = workflow_parent[0].id;
|
||||
|
||||
// Create a new version
|
||||
const data = await db
|
||||
.insert(workflowVersionTable)
|
||||
.values({
|
||||
workflow_id: workflow_id,
|
||||
workflow,
|
||||
workflow_api,
|
||||
version: 1,
|
||||
})
|
||||
.returning();
|
||||
version = data[0].version;
|
||||
} else if (workflow_id) {
|
||||
// Case 2 update workflow
|
||||
const data = await db
|
||||
.insert(workflowVersionTable)
|
||||
.values({
|
||||
workflow_id,
|
||||
workflow: workflow,
|
||||
workflow_api,
|
||||
// version: sql`${workflowVersionTable.version} + 1`,
|
||||
version: sql`(
|
||||
SELECT COALESCE(MAX(version), 0) + 1
|
||||
FROM ${workflowVersionTable}
|
||||
WHERE workflow_id = ${workflow_id}
|
||||
)`,
|
||||
})
|
||||
.returning();
|
||||
version = data[0].version;
|
||||
} else {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid request, missing either workflow_id or name",
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
statusText: "Invalid request",
|
||||
headers: corsHeaders,
|
||||
}
|
||||
);
|
||||
}
|
||||
} catch (error: any) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error.toString(),
|
||||
},
|
||||
{
|
||||
status: 500,
|
||||
statusText: "Invalid request",
|
||||
headers: corsHeaders,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
workflow_id: workflow_id,
|
||||
version: version,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: corsHeaders,
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getFileDownloadUrl } from "../../../../server/getFileDownloadUrl";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const file = new URL(request.url).searchParams.get("file");
|
||||
if (!file) return NextResponse.redirect("/");
|
||||
return NextResponse.redirect(await getFileDownloadUrl(file));
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,106 @@
|
||||
@layer base {
|
||||
:root {
|
||||
--shiki-color-text: theme('colors.white');
|
||||
--shiki-token-constant: theme('colors.emerald.300');
|
||||
--shiki-token-string: theme('colors.emerald.300');
|
||||
--shiki-token-comment: theme('colors.zinc.500');
|
||||
--shiki-token-keyword: theme('colors.sky.300');
|
||||
--shiki-token-parameter: theme('colors.pink.300');
|
||||
--shiki-token-function: theme('colors.violet.300');
|
||||
--shiki-token-string-expression: theme('colors.emerald.300');
|
||||
--shiki-token-punctuation: theme('colors.zinc.200');
|
||||
}
|
||||
|
||||
[inert] ::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 222.2 47.4% 11.2%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 210 40% 98%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
}
|
||||
}
|
||||
|
||||
.shiki>code>span {
|
||||
/* text-wrap: wrap; */
|
||||
/* word-wrap: break-word; */
|
||||
/* @apply break-all ; */
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.shiki {
|
||||
/* @apply rounded-lg p-2 overflow-x-scroll */
|
||||
@apply p-2 max-w-full overflow-auto w-full
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import "./globals.css";
|
||||
import { NavbarRight } from "@/components/NavbarRight";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { ClerkProvider, UserButton } from "@clerk/nextjs";
|
||||
import { Github } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import meta from "next-gen/config";
|
||||
import PlausibleProvider from "next-plausible";
|
||||
import { Inter } from "next/font/google";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: meta["og:title"],
|
||||
description: meta["og:description"],
|
||||
|
||||
category: "technology",
|
||||
|
||||
openGraph: {
|
||||
type: "website",
|
||||
title: meta["og:title"],
|
||||
description: meta["og:description"],
|
||||
locale: "en_US",
|
||||
images: "/og.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<ClerkProvider>
|
||||
<TooltipProvider>
|
||||
<head>
|
||||
{process.env.PLAUSIBLE_DOMAIN && (
|
||||
<PlausibleProvider domain={process.env.PLAUSIBLE_DOMAIN} />
|
||||
)}
|
||||
</head>
|
||||
<body className={inter.className}>
|
||||
<main className="w-full flex min-h-[100dvh] flex-col items-center justify-start">
|
||||
<div className="z-[-1] fixed h-full w-full bg-white">
|
||||
<div className="absolute 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="w-full h-18 flex items-center justify-between gap-4 p-4 border-b border-gray-200">
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<a
|
||||
className="font-bold text-md md:text-lg hover:underline"
|
||||
href="/"
|
||||
>
|
||||
{meta.name}
|
||||
</a>
|
||||
<NavbarRight />
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Button
|
||||
asChild
|
||||
variant="link"
|
||||
className="rounded-full aspect-square p-2 mr-4"
|
||||
>
|
||||
<a href="/docs">Docs</a>
|
||||
</Button>
|
||||
<UserButton />
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="rounded-full aspect-square p-2"
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
href="https://github.com/BennyKok/comfyui-deploy"
|
||||
>
|
||||
<Github />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
{/* <div></div> */}
|
||||
</div>
|
||||
<div className="md:px-10 px-6 w-full min-h-[calc(100dvh-73px)]">
|
||||
{children}
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</main>
|
||||
</body>
|
||||
</TooltipProvider>
|
||||
</ClerkProvider>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MachineList } from "@/components/MachineList";
|
||||
import { db } from "@/db/db";
|
||||
import { machinesTable } from "@/db/schema";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export default function Page() {
|
||||
return <MachineListServer />;
|
||||
}
|
||||
|
||||
async function MachineListServer() {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return <div>No auth</div>;
|
||||
}
|
||||
|
||||
const machines = await db.query.machinesTable.findMany({
|
||||
orderBy: desc(machinesTable.updated_at),
|
||||
where: eq(machinesTable.user_id, userId),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* <div>Machines</div> */}
|
||||
<MachineList data={machines} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import Main from "@/components/Main";
|
||||
|
||||
export default function Home() {
|
||||
return <Main />;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { DeploymentsTable, RunsTable } from "../../../../components/RunsTable";
|
||||
import { findFirstTableWithVersion } from "../../../../server/findFirstTableWithVersion";
|
||||
import { MachinesWSMain } from "@/components/MachinesWS";
|
||||
import { VersionDetails } from "@/components/VersionDetails";
|
||||
import {
|
||||
CopyWorkflowVersion,
|
||||
CreateDeploymentButton,
|
||||
MachineSelect,
|
||||
RunWorkflowButton,
|
||||
VersionSelect,
|
||||
ViewWorkflowDetailsButton,
|
||||
} from "@/components/VersionSelect";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { getRelativeTime } from "@/lib/getRelativeTime";
|
||||
import { getMachines } from "@/server/curdMachine";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { workflow_id: string };
|
||||
}) {
|
||||
const workflow_id = params.workflow_id;
|
||||
|
||||
const workflow = await findFirstTableWithVersion(workflow_id);
|
||||
const machines = await getMachines();
|
||||
|
||||
return (
|
||||
<div className="mt-4 w-full flex flex-col lg:flex-row gap-4 max-h-[calc(100dvh-100px)]">
|
||||
<div className="flex gap-4 flex-col">
|
||||
<Card className="w-full lg:w-fit lg:min-w-[600px] h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>{workflow?.name}</CardTitle>
|
||||
<CardDescription suppressHydrationWarning={true}>
|
||||
{getRelativeTime(workflow?.updated_at)}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<VersionSelect workflow={workflow} />
|
||||
<MachineSelect machines={machines} />
|
||||
<RunWorkflowButton workflow={workflow} machines={machines} />
|
||||
<CreateDeploymentButton workflow={workflow} machines={machines} />
|
||||
<CopyWorkflowVersion workflow={workflow} />
|
||||
<ViewWorkflowDetailsButton workflow={workflow} />
|
||||
</div>
|
||||
|
||||
<VersionDetails workflow={workflow} />
|
||||
<MachinesWSMain machines={machines} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="w-full h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Deployments</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<DeploymentsTable workflow_id={workflow_id} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card className="w-full h-fit">
|
||||
<CardHeader>
|
||||
<CardTitle>Run</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<RunsTable workflow_id={workflow_id} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { WorkflowList } from "@/components/WorkflowList";
|
||||
import { db } from "@/db/db";
|
||||
import { usersTable, workflowTable, workflowVersionTable } from "@/db/schema";
|
||||
import { auth, clerkClient } from "@clerk/nextjs";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
export default function Home() {
|
||||
return <WorkflowServer />;
|
||||
}
|
||||
|
||||
async function WorkflowServer() {
|
||||
const { userId } = await auth();
|
||||
|
||||
if (!userId) {
|
||||
return <div>No auth</div>;
|
||||
}
|
||||
|
||||
const user = await db.query.usersTable.findFirst({
|
||||
where: eq(usersTable.id, userId),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
await setInitialUserData(userId);
|
||||
}
|
||||
|
||||
const workflow = await db.query.workflowTable.findMany({
|
||||
// extras: {
|
||||
// count: sql<number>`(select count(*) from ${workflowVersionTable})`.as(
|
||||
// "count",
|
||||
// ),
|
||||
// },
|
||||
with: {
|
||||
versions: {
|
||||
limit: 1,
|
||||
orderBy: desc(workflowVersionTable.version),
|
||||
},
|
||||
},
|
||||
orderBy: desc(workflowTable.updated_at),
|
||||
where: eq(workflowTable.user_id, userId),
|
||||
});
|
||||
|
||||
return (
|
||||
<WorkflowList
|
||||
data={workflow.map((x) => {
|
||||
return {
|
||||
id: x.id,
|
||||
email: x.name,
|
||||
amount: x.versions[0]?.version ?? 0,
|
||||
date: x.updated_at,
|
||||
};
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user