feat: add deployment endpoint

This commit is contained in:
BennyKok
2023-12-14 22:36:57 +08:00
parent 0835d966f1
commit 7e05fae7b3
29 changed files with 1776 additions and 190 deletions
+30
View File
@@ -0,0 +1,30 @@
import { CopyButton } from "@/components/CopyButton";
import type { Lang } from "shiki";
import shiki from "shiki";
export async function CodeBlock(props: { code: string; lang: Lang }) {
const highlighter = await shiki.getHighlighter({
theme: "one-dark-pro",
});
return (
<div className="relative w-full max-w-full text-sm">
{/* max-w-[calc(32rem-1.5rem-1.5rem)] */}
{/* <div className=""> */}
<p
// tabIndex={1}
className="[&>pre]:p-4 rounded-sm "
style={{
overflowWrap: "break-word",
}}
dangerouslySetInnerHTML={{
__html: highlighter.codeToHtml(props.code.trim(), {
lang: props.lang,
}),
}}
/>
{/* </div> */}
<CopyButton className="absolute right-2 top-2" text={props.code} />
</div>
);
}
+24
View File
@@ -0,0 +1,24 @@
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { Copy } from "lucide-react";
export function CopyButton({
className,
...props
}: {
text: string;
className?: string;
}) {
return (
<Button
onClick={() => {
navigator.clipboard.writeText(props.text);
}}
className={cn(" p-2 min-h-0 aspect-square", className)}
>
<Copy size={14} />
</Button>
);
}
+113
View File
@@ -0,0 +1,113 @@
import { CodeBlock } from "@/components/CodeBlock";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { TableCell, TableRow } from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { getRelativeTime } from "@/lib/getRelativeTime";
import type { findAllDeployments } from "@/server/findAllRuns";
const curlTemplate = `
curl --request POST \
--url <URL> \
--header 'Content-Type: application/json' \
--data '{
"deployment_id": "<ID>"
}'
`;
const jsTemplate = `
const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: '{"deployment_id":"<ID>"}'
};
fetch('<URL>', options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
`;
const jsTemplate_checkStatus = `
const options = {
method: 'GET',
headers: {'Content-Type': 'application/json'},
};
const run_id = '<RUN_ID>';
fetch('<URL>?run_id=' + run_id, options)
.then(response => response.json())
.then(response => console.log(response))
.catch(err => console.error(err));
`;
export function DeploymentDisplay({
deployment,
}: {
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0];
}) {
return (
<Dialog>
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
<TableRow>
<TableCell className="capitalize">{deployment.environment}</TableCell>
<TableCell className="font-medium">
{deployment.version?.version}
</TableCell>
<TableCell className="font-medium">
{deployment.machine?.name}
</TableCell>
<TableCell className="text-right">
{getRelativeTime(deployment.updated_at)}
</TableCell>
</TableRow>
</DialogTrigger>
<DialogContent className="max-w-xl">
<DialogHeader>
<DialogTitle className="capitalize">
{deployment.environment} Deployment
</DialogTitle>
<DialogDescription>Code for your deployment client</DialogDescription>
</DialogHeader>
<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">
<CodeBlock lang="js" code={formatCode(jsTemplate, deployment)} />
<CodeBlock
lang="js"
code={formatCode(jsTemplate_checkStatus, deployment)}
/>
</TabsContent>
<TabsContent value="curl">
<CodeBlock
lang="bash"
code={formatCode(curlTemplate, deployment)}
/>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
}
function formatCode(
codeTemplate: string,
deployment: Awaited<ReturnType<typeof findAllDeployments>>[0]
) {
return codeTemplate
.replace(
"<URL>",
`${process.env.VERCEL_URL ?? "http://localhost:3000"}/api/run`
)
.replace("<ID>", deployment.id);
}
+11 -10
View File
@@ -161,7 +161,7 @@ export const columns: ColumnDef<Machine>[] = [
<DropdownMenuItem
className="text-destructive"
onClick={async () => {
callServerWithToast(await deleteMachine(workflow.id));
callServerPromise(deleteMachine(workflow.id));
}}
>
Delete Machine
@@ -176,15 +176,16 @@ export const columns: ColumnDef<Machine>[] = [
},
];
async function callServerWithToast(result: {
message: string;
error?: boolean;
}) {
if (result.error) {
toast.error(result.message);
} else {
toast.success(result.message);
}
export async function callServerPromise<T>(result: Promise<T>) {
return result
.then((x) => {
if ((x as { message: string })?.message !== undefined) {
toast.success((x as { message: string }).message);
}
})
.catch((error) => {
toast.error(error.message);
});
}
export function MachineList({ data }: { data: Machine[] }) {
+2 -46
View File
@@ -1,5 +1,6 @@
"use client";
import { RunOutputs } from "./RunOutputs";
import { useStore } from "@/components/MachinesWS";
import { StatusBadge } from "@/components/StatusBadge";
import {
@@ -10,18 +11,9 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TableCell, TableRow } from "@/components/ui/table";
import { getRelativeTime } from "@/lib/getRelativeTime";
import { type findAllRuns } from "@/server/findAllRuns";
import { getRunsOutput } from "@/server/getRunsOutput";
import { useEffect, useState } from "react";
export function RunDisplay({
run,
@@ -73,42 +65,6 @@ export function RunDisplay({
);
}
export function RunOutputs({ run_id }: { run_id: string }) {
const [outputs, setOutputs] = useState<
Awaited<ReturnType<typeof getRunsOutput>>
>([]);
useEffect(() => {
getRunsOutput(run_id).then((x) => setOutputs(x));
}, [run_id]);
return (
<Table>
{/* <TableCaption>A list of your recent runs.</TableCaption> */}
<TableHeader className="bg-background top-0 sticky">
<TableRow>
<TableHead className="w-[100px]">File</TableHead>
<TableHead className="">Output</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{outputs?.map((run) => {
const fileName = run.data.images[0].filename;
// const filePath
return (
<TableRow key={run.id}>
<TableCell>{fileName}</TableCell>
<TableCell>
<OutputRender run_id={run_id} filename={fileName} />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}
export function OutputRender(props: { run_id: string; filename: string }) {
if (props.filename.endsWith(".png")) {
return (
+53
View File
@@ -0,0 +1,53 @@
"use client";
import { OutputRender } from "./RunDisplay";
import { callServerPromise } from "@/components/MachineList";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { getRunsOutput } from "@/server/getRunsOutput";
import { useEffect, useState } from "react";
export function RunOutputs({ run_id }: { run_id: string }) {
const [outputs, setOutputs] =
useState<Awaited<ReturnType<typeof getRunsOutput>>>();
useEffect(() => {
if (!run_id) return;
// fetch(`/api/run?run_id=${run_id}`)
// .then((x) => x.json())
// .then((x) => setOutputs(x));
callServerPromise(getRunsOutput(run_id).then((x) => setOutputs(x)));
}, [run_id, outputs]);
return (
<Table>
{/* <TableCaption>A list of your recent runs.</TableCaption> */}
<TableHeader className="bg-background top-0 sticky">
<TableRow>
<TableHead className="w-[100px]">File</TableHead>
<TableHead className="">Output</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{outputs?.map((run) => {
const fileName = run.data.images[0].filename;
// const filePath
return (
<TableRow key={run.id}>
<TableCell>{fileName}</TableCell>
<TableCell>
<OutputRender run_id={run_id} filename={fileName} />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
}
+26 -1
View File
@@ -1,4 +1,5 @@
import { findAllRuns } from "../server/findAllRuns";
import { findAllDeployments, findAllRuns } from "../server/findAllRuns";
import { DeploymentDisplay } from "./DeploymentDisplay";
import { RunDisplay } from "./RunDisplay";
import {
Table,
@@ -33,3 +34,27 @@ export async function RunsTable(props: { workflow_id: string }) {
</div>
);
}
export async function DeploymentsTable(props: { workflow_id: string }) {
const allRuns = await findAllDeployments(props.workflow_id);
return (
<div className="overflow-auto h-[400px] w-full">
<Table className="">
<TableCaption>A list of your deployments</TableCaption>
<TableHeader className="bg-background top-0 sticky">
<TableRow>
<TableHead className=" w-[100px]">Environment</TableHead>
<TableHead className=" w-[100px]">Version</TableHead>
<TableHead className="">Machine</TableHead>
<TableHead className=" text-right">Updated At</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allRuns.map((run) => (
<DeploymentDisplay deployment={run} key={run.id} />
))}
</TableBody>
</Table>
</div>
);
}
+76 -1
View File
@@ -1,7 +1,14 @@
"use client";
import { LoadingIcon } from "@/components/LoadingIcon";
import { callServerPromise } from "@/components/MachineList";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select,
SelectContent,
@@ -12,9 +19,10 @@ import {
SelectValue,
} from "@/components/ui/select";
import { createRun } from "@/server/createRun";
import { createDeployments } from "@/server/curdDeploments";
import type { getMachines } from "@/server/curdMachine";
import type { findFirstTableWithVersion } from "@/server/findFirstTableWithVersion";
import { Play } from "lucide-react";
import { MoreVertical, Play } from "lucide-react";
import { parseAsInteger, useQueryState } from "next-usequerystate";
import { useState } from "react";
@@ -122,3 +130,70 @@ export function RunWorkflowButton({
</Button>
);
}
export function CreateDeploymentButton({
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] = useQueryState("machine", {
defaultValue: machines[0].id ?? "",
});
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">
Deploy <MoreVertical size={14} /> {isLoading && <LoadingIcon />}
</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,
"production"
)
);
setIsLoading(false);
}}
>
Production
</DropdownMenuItem>
<DropdownMenuItem
onClick={async () => {
if (!workflow_version_id) return;
setIsLoading(true);
await callServerPromise(
createDeployments(
workflow.id,
workflow_version_id,
machine,
"staging"
)
);
setIsLoading(false);
}}
>
Staging
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
+22 -16
View File
@@ -1,6 +1,5 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
import * as React from "react";
const Card = React.forwardRef<
HTMLDivElement,
@@ -14,8 +13,8 @@ const Card = React.forwardRef<
)}
{...props}
/>
))
Card.displayName = "Card"
));
Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +25,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
));
CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
@@ -41,8 +40,8 @@ const CardTitle = React.forwardRef<
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
));
CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,16 +52,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
));
CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
));
CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -73,7 +72,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
));
CardFooter.displayName = "CardFooter";
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
};
+24 -24
View File
@@ -1,18 +1,17 @@
"use client"
"use client";
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/utils";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root;
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger;
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = DialogPrimitive.Portal;
const DialogPortal = DialogPrimitive.Portal
const DialogClose = DialogPrimitive.Close
const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
@@ -26,8 +25,8 @@ const DialogOverlay = React.forwardRef<
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
@@ -37,8 +36,9 @@ const DialogContent = React.forwardRef<
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
// grid tuning off grid for styling issue
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
"fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] grid grid-cols-1 translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
@@ -50,8 +50,8 @@ const DialogContent = React.forwardRef<
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
@@ -64,8 +64,8 @@ const DialogHeader = ({
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
@@ -78,8 +78,8 @@ const DialogFooter = ({
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
@@ -93,8 +93,8 @@ const DialogTitle = React.forwardRef<
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
@@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef<
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
@@ -119,4 +119,4 @@ export {
DialogFooter,
DialogTitle,
DialogDescription,
}
};