feat: add millon js, add models picker dialog, update builder
This commit is contained in:
@@ -33,7 +33,7 @@ export default async function Page({
|
||||
endpoint={process.env.MODAL_BUILDER_URL!}
|
||||
/>
|
||||
)}
|
||||
{machine.build_log && (
|
||||
{machine.status !== "building" && machine.build_log && (
|
||||
<LogsViewer logs={JSON.parse(machine.build_log)} />
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { getInputsFromWorkflow } from "@/lib/getInputsFromWorkflow";
|
||||
@@ -72,13 +73,13 @@ export function DeploymentDisplay({
|
||||
<DialogTrigger asChild className="appearance-none hover:cursor-pointer">
|
||||
<TableRow>
|
||||
<TableCell className="capitalize">{deployment.environment}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<TableCell className="font-medium truncate">
|
||||
{deployment.version?.version}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
<TableCell className="font-medium truncate">
|
||||
{deployment.machine?.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className="text-right truncate">
|
||||
{getRelativeTime(deployment.updated_at)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -90,34 +91,36 @@ export function DeploymentDisplay({
|
||||
</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">
|
||||
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>
|
||||
<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>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -43,6 +43,7 @@ export function InsertModal<
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* <ScrollArea> */}
|
||||
<AutoForm
|
||||
fieldConfig={props.fieldConfig}
|
||||
formSchema={props.formSchema}
|
||||
@@ -60,6 +61,7 @@ export function InsertModal<
|
||||
</AutoFormSubmit>
|
||||
</div>
|
||||
</AutoForm>
|
||||
{/* </ScrollArea> */}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -14,12 +14,13 @@ export function MachineBuildLog({
|
||||
endpoint: string;
|
||||
}) {
|
||||
const [logs, setLogs] = useState<LogsType>([]);
|
||||
const [finished, setFinished] = useState(false);
|
||||
|
||||
const wsEndpoint = endpoint.replace(/^http/, "ws");
|
||||
const { lastMessage, readyState } = useWebSocket(
|
||||
`${wsEndpoint}/ws/${machine_id}`,
|
||||
{
|
||||
shouldReconnect: () => true,
|
||||
shouldReconnect: () => !finished,
|
||||
reconnectAttempts: 20,
|
||||
reconnectInterval: 1000,
|
||||
}
|
||||
@@ -36,6 +37,8 @@ export function MachineBuildLog({
|
||||
|
||||
if (message?.event === "LOGS") {
|
||||
setLogs((logs) => [...(logs ?? []), message.data]);
|
||||
} else if (message?.event === "FINISHED") {
|
||||
setFinished(true);
|
||||
}
|
||||
}, [lastMessage]);
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
deleteMachine,
|
||||
disableMachine,
|
||||
enableMachine,
|
||||
updateCustomMachine,
|
||||
updateMachine,
|
||||
} from "@/server/curdMachine";
|
||||
import type {
|
||||
@@ -95,7 +96,7 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
// <a className="hover:underline" href={`/${row.original.id}`}>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<div className="flex flex-row gap-2 items-center truncate">
|
||||
<a href={`/machines/${row.original.id}`} className="hover:underline">
|
||||
{row.getValue("name")}
|
||||
</a>
|
||||
@@ -115,7 +116,9 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
header: () => <div className="text-left">Endpoint</div>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-left font-medium">{row.original.endpoint}</div>
|
||||
<div className="text-left font-medium truncate max-w-[400px]">
|
||||
{row.original.endpoint}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -123,7 +126,11 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
accessorKey: "type",
|
||||
header: () => <div className="text-left">Type</div>,
|
||||
cell: ({ row }) => {
|
||||
return <div className="text-left font-medium">{row.original.type}</div>;
|
||||
return (
|
||||
<div className="text-left font-medium truncate">
|
||||
{row.original.type}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -133,7 +140,7 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<button
|
||||
className="w-full flex items-center justify-end hover:underline"
|
||||
className="w-full flex items-center justify-end hover:underline truncate"
|
||||
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||
>
|
||||
Update Date
|
||||
@@ -195,22 +202,50 @@ export const columns: ColumnDef<Machine>[] = [
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
<UpdateModal
|
||||
data={machine}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title="Edit"
|
||||
description="Edit machines"
|
||||
serverAction={updateMachine}
|
||||
formSchema={addMachineSchema}
|
||||
fieldConfig={{
|
||||
auth_token: {
|
||||
inputProps: {
|
||||
type: "password",
|
||||
{machine.type === "comfy-deploy-serverless" ? (
|
||||
<UpdateModal
|
||||
data={machine}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title="Edit"
|
||||
description="Edit machines"
|
||||
serverAction={updateCustomMachine}
|
||||
formSchema={addCustomMachineSchema}
|
||||
fieldConfig={{
|
||||
type: {
|
||||
fieldType: "fallback",
|
||||
inputProps: {
|
||||
disabled: true,
|
||||
showLabel: false,
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
snapshot: {
|
||||
fieldType: "snapshot",
|
||||
},
|
||||
models: {
|
||||
fieldType: "models",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<UpdateModal
|
||||
data={machine}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
title="Edit"
|
||||
description="Edit machines"
|
||||
serverAction={updateMachine}
|
||||
formSchema={addMachineSchema}
|
||||
fieldConfig={{
|
||||
auth_token: {
|
||||
inputProps: {
|
||||
type: "password",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
@@ -273,8 +308,16 @@ export function MachineList({ data }: { data: Machine[] }) {
|
||||
fieldType: "fallback",
|
||||
inputProps: {
|
||||
disabled: true,
|
||||
showLabel: false,
|
||||
type: "hidden",
|
||||
},
|
||||
},
|
||||
snapshot: {
|
||||
fieldType: "snapshot",
|
||||
},
|
||||
models: {
|
||||
fieldType: "models",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -191,9 +191,11 @@ export function RunWorkflowButton({
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Run inputs</DialogTitle>
|
||||
<DialogTitle>Confirm run</DialogTitle>
|
||||
<DialogDescription>
|
||||
Run your workflow with custom inputs
|
||||
{schema
|
||||
? "Run your workflow with custom inputs"
|
||||
: "Confirm to run your workflow"}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* <div className="max-h-96 overflow-y-scroll"> */}
|
||||
@@ -203,6 +205,7 @@ export function RunWorkflowButton({
|
||||
values={values}
|
||||
onValuesChange={setValues}
|
||||
onSubmit={runWorkflow}
|
||||
className="px-1"
|
||||
>
|
||||
<div className="flex justify-end">
|
||||
<AutoFormSubmit>
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useRef } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
const Model = z.object({
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
base: z.string(),
|
||||
save_path: z.string(),
|
||||
description: z.string(),
|
||||
reference: z.string(),
|
||||
filename: z.string(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
const ModelList = z.array(Model);
|
||||
|
||||
export const ModelListWrapper = z.object({
|
||||
models: ModelList,
|
||||
});
|
||||
|
||||
export function ModelPickerView({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
const value = (field.value as z.infer<typeof ModelList>) ?? [];
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const [modelList, setModelList] =
|
||||
React.useState<z.infer<typeof ModelListWrapper>>();
|
||||
|
||||
// const [selectedModels, setSelectedModels] = React.useState<
|
||||
// z.infer<typeof ModelList>
|
||||
// >(field.value ?? []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
fetch(
|
||||
"https://raw.githubusercontent.com/ltdrdata/ComfyUI-Manager/main/model-list.json",
|
||||
{
|
||||
signal: controller.signal,
|
||||
}
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((a) => {
|
||||
setModelList(ModelListWrapper.parse(a));
|
||||
});
|
||||
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
function toggleModel(model: z.infer<typeof Model>) {
|
||||
const prevSelectedModels = value;
|
||||
if (
|
||||
prevSelectedModels.some(
|
||||
(selectedModel) =>
|
||||
selectedModel.url + selectedModel.name === model.url + model.name
|
||||
)
|
||||
) {
|
||||
field.onChange(
|
||||
prevSelectedModels.filter(
|
||||
(selectedModel) =>
|
||||
selectedModel.url + selectedModel.name !== model.url + model.name
|
||||
)
|
||||
);
|
||||
} else {
|
||||
field.onChange([...prevSelectedModels, model]);
|
||||
}
|
||||
}
|
||||
|
||||
// React.useEffect(() => {
|
||||
// field.onChange(selectedModels);
|
||||
// }, [selectedModels]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div className="" ref={containerRef}>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between flex"
|
||||
>
|
||||
Select models... ({value.length} selected)
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[375px] p-0" side="top">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." className="h-9" />
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandList className="pointer-events-auto">
|
||||
<CommandGroup>
|
||||
{modelList?.models.map((model) => (
|
||||
<CommandItem
|
||||
key={model.url + model.name}
|
||||
value={model.url}
|
||||
onSelect={() => {
|
||||
toggleModel(model);
|
||||
// Update field.onChange to pass the array of selected models
|
||||
}}
|
||||
>
|
||||
{model.name}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value.some(
|
||||
(selectedModel) => selectedModel.url === model.url
|
||||
)
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{field.value && (
|
||||
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[360px] rounded-lg mt-2">
|
||||
<div className="max-h-[200px]">
|
||||
<pre className="p-2 rounded-md text-xs ">
|
||||
{JSON.stringify(field.value, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { findAllDeployments } from "@/server/curdDeploments";
|
||||
import { Check, ChevronsUpDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
export function SnapshotPickerView({
|
||||
field,
|
||||
}: Pick<AutoFormInputComponentProps, "field">) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [selected, setSelected] = React.useState<string | null>(null);
|
||||
|
||||
const [frameworks, setFramework] = React.useState<
|
||||
{
|
||||
id: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}[]
|
||||
>();
|
||||
|
||||
React.useEffect(() => {
|
||||
findAllDeployments().then((a) => {
|
||||
console.log(a);
|
||||
|
||||
const frameworks = a
|
||||
.map((item) => {
|
||||
if (
|
||||
item.deployments.length == 0 ||
|
||||
item.deployments[0].version.snapshot == null
|
||||
)
|
||||
return null;
|
||||
|
||||
return {
|
||||
id: item.deployments[0].version.id,
|
||||
label: `${item.name} - ${item.deployments[0].environment}`,
|
||||
value: JSON.stringify(item.deployments[0].version.snapshot),
|
||||
};
|
||||
})
|
||||
.filter((item): item is NonNullable<typeof item> => item != null);
|
||||
|
||||
setFramework(frameworks);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function findItem(value: string) {
|
||||
// console.log(frameworks);
|
||||
|
||||
return frameworks?.find((item) => item.id === value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between flex"
|
||||
>
|
||||
{selected ? findItem(selected)?.label : "Select snapshot..."}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[375px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." className="h-9" />
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{frameworks?.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.id}
|
||||
value={framework.id}
|
||||
onSelect={(currentValue) => {
|
||||
setSelected(currentValue);
|
||||
const json =
|
||||
frameworks?.find((item) => item.id === currentValue)
|
||||
?.value ?? null;
|
||||
field.onChange(json ? JSON.parse(json) : null);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{framework.label}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
field.value === framework.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{field.value && (
|
||||
<ScrollArea className="w-full bg-gray-100 mx-auto max-w-[360px] rounded-lg mt-2">
|
||||
<div className="max-h-[200px]">
|
||||
<pre className="p-2 rounded-md text-xs ">
|
||||
{JSON.stringify(field.value, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "../ui/form";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
import { ModelPickerView } from "@/components/custom-form/ModelPickerView";
|
||||
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import * as React from "react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function AutoFormModelsPicker({
|
||||
label,
|
||||
isRequired,
|
||||
field,
|
||||
fieldConfigItem,
|
||||
zodItem,
|
||||
}: AutoFormInputComponentProps) {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{label}
|
||||
{isRequired && <span className="text-destructive"> *</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Suspense fallback={<LoadingIcon />}>
|
||||
<ModelPickerView field={field} />
|
||||
</Suspense>
|
||||
</FormControl>
|
||||
{fieldConfigItem.description && (
|
||||
<FormDescription>{fieldConfigItem.description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "../ui/form";
|
||||
import { SnapshotPickerView } from "./SnapshotPickerView";
|
||||
import { LoadingIcon } from "@/components/LoadingIcon";
|
||||
// import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||
import * as React from "react";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function AutoFormSnapshotPicker({
|
||||
label,
|
||||
isRequired,
|
||||
field,
|
||||
fieldConfigItem,
|
||||
zodItem,
|
||||
}: AutoFormInputComponentProps) {
|
||||
return (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{label}
|
||||
{isRequired && <span className="text-destructive"> *</span>}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Suspense fallback={<LoadingIcon />}>
|
||||
<SnapshotPickerView field={field} />
|
||||
</Suspense>
|
||||
</FormControl>
|
||||
{fieldConfigItem.description && (
|
||||
<FormDescription>{fieldConfigItem.description}</FormDescription>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import AutoFormNumber from "./fields/number";
|
||||
import AutoFormRadioGroup from "./fields/radio-group";
|
||||
import AutoFormSwitch from "./fields/switch";
|
||||
import AutoFormTextarea from "./fields/textarea";
|
||||
import AutoFormModelsPicker from "@/components/custom-form/model-picker";
|
||||
import AutoFormSnapshotPicker from "@/components/custom-form/snapshot-picker";
|
||||
|
||||
export const INPUT_COMPONENTS = {
|
||||
checkbox: AutoFormCheckbox,
|
||||
@@ -16,6 +18,10 @@ export const INPUT_COMPONENTS = {
|
||||
textarea: AutoFormTextarea,
|
||||
number: AutoFormNumber,
|
||||
fallback: AutoFormInput,
|
||||
|
||||
// Customs
|
||||
snapshot: AutoFormSnapshotPicker,
|
||||
models: AutoFormModelsPicker,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
FormMessage,
|
||||
} from "../../form";
|
||||
import { Input } from "../../input";
|
||||
import { AutoFormInputComponentProps } from "../types";
|
||||
import type { AutoFormInputComponentProps } from "../types";
|
||||
|
||||
export default function AutoFormInput({
|
||||
label,
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { FieldConfig } from "./types";
|
||||
import type { ZodObjectOrWrapped } from "./utils";
|
||||
import { getDefaultValues, getObjectFormSchema } from "./utils";
|
||||
import AutoFormObject from "@/components/ui/auto-form/fields/object";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { DefaultValues } from "react-hook-form";
|
||||
@@ -68,11 +69,15 @@ function AutoForm<SchemaType extends ZodObjectOrWrapped>({
|
||||
}}
|
||||
className={cn("space-y-5", className)}
|
||||
>
|
||||
<AutoFormObject
|
||||
schema={objectFormSchema}
|
||||
form={form}
|
||||
fieldConfig={fieldConfig}
|
||||
/>
|
||||
<ScrollArea>
|
||||
<div className="max-h-[400px] px-1 py-1 w-full">
|
||||
<AutoFormObject
|
||||
schema={objectFormSchema}
|
||||
form={form}
|
||||
fieldConfig={fieldConfig}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{children}
|
||||
</form>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ControllerRenderProps, FieldValues } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
import { INPUT_COMPONENTS } from "./config";
|
||||
import type { INPUT_COMPONENTS } from "./config";
|
||||
import type { ControllerRenderProps, FieldValues } from "react-hook-form";
|
||||
import type * as z from "zod";
|
||||
|
||||
export type FieldConfigItem = {
|
||||
description?: React.ReactNode;
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
interface CommandDialogProps extends DialogProps {}
|
||||
|
||||
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as React from "react";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
@@ -17,9 +16,9 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
@@ -22,10 +21,24 @@ const PopoverContent = React.forwardRef<
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
// https://github.com/shadcn-ui/ui/pull/2123/files#diff-e43c79299129c57a9055c3d6a20ff7bbeea4035dd6aa80eebe381b29f82f90a8
|
||||
onWheel={(e) => {
|
||||
e.stopPropagation();
|
||||
const isScrollingDown = e.deltaY > 0;
|
||||
if (isScrollingDown) {
|
||||
e.currentTarget.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "ArrowDown" })
|
||||
);
|
||||
} else {
|
||||
e.currentTarget.dispatchEvent(
|
||||
new KeyboardEvent("keydown", { key: "ArrowUp" })
|
||||
);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
||||
@@ -37,6 +37,7 @@ export const workflowTable = dbSchema.table("workflows", {
|
||||
|
||||
export const workflowRelations = relations(workflowTable, ({ many }) => ({
|
||||
versions: many(workflowVersionTable),
|
||||
deployments: many(deploymentsTable),
|
||||
}));
|
||||
|
||||
export const workflowType = z.any();
|
||||
@@ -203,6 +204,7 @@ export const machinesTable = dbSchema.table("machines", {
|
||||
type: machinesType("type").notNull().default("classic"),
|
||||
status: machinesStatus("status").notNull().default("ready"),
|
||||
snapshot: jsonb("snapshot").$type<any>(),
|
||||
models: jsonb("models").$type<any>(),
|
||||
build_log: text("build_log"),
|
||||
});
|
||||
|
||||
@@ -255,6 +257,10 @@ export const deploymentsRelations = relations(deploymentsTable, ({ one }) => ({
|
||||
fields: [deploymentsTable.workflow_version_id],
|
||||
references: [workflowVersionTable.id],
|
||||
}),
|
||||
workflow: one(workflowTable, {
|
||||
fields: [deploymentsTable.workflow_id],
|
||||
references: [workflowTable.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const apiKeyTable = dbSchema.table("api_keys", {
|
||||
|
||||
@@ -17,4 +17,5 @@ export const addCustomMachineSchema = insertCustomMachineSchema.pick({
|
||||
name: true,
|
||||
type: true,
|
||||
snapshot: true,
|
||||
models: true,
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { db } from "@/db/db";
|
||||
import { deploymentsTable } from "@/db/schema";
|
||||
import { deploymentsTable, workflowTable } from "@/db/schema";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import "server-only";
|
||||
|
||||
@@ -47,3 +47,36 @@ export async function createDeployments(
|
||||
message: `Successfully created deployment for ${environment}`,
|
||||
};
|
||||
}
|
||||
|
||||
export async function findAllDeployments() {
|
||||
const { userId, orgId } = auth();
|
||||
if (!userId) throw new Error("No user id");
|
||||
|
||||
const deployments = await db.query.workflowTable.findMany({
|
||||
where: and(
|
||||
orgId
|
||||
? eq(workflowTable.org_id, orgId)
|
||||
: and(eq(workflowTable.user_id, userId), isNull(workflowTable.org_id))
|
||||
),
|
||||
columns: {
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
deployments: {
|
||||
columns: {
|
||||
environment: true,
|
||||
},
|
||||
with: {
|
||||
version: {
|
||||
columns: {
|
||||
id: true,
|
||||
snapshot: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return deployments;
|
||||
}
|
||||
|
||||
+100
-41
@@ -6,6 +6,7 @@ import type {
|
||||
} from "./addMachineSchema";
|
||||
import { withServerPromise } from "./withServerPromise";
|
||||
import { db } from "@/db/db";
|
||||
import type { MachineType } from "@/db/schema";
|
||||
import { machinesTable } from "@/db/schema";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
@@ -25,7 +26,11 @@ export async function getMachines() {
|
||||
and(
|
||||
orgId
|
||||
? eq(machinesTable.org_id, orgId)
|
||||
: eq(machinesTable.user_id, userId),
|
||||
: // make sure org_id is null
|
||||
and(
|
||||
eq(machinesTable.user_id, userId),
|
||||
isNull(machinesTable.org_id)
|
||||
),
|
||||
eq(machinesTable.disabled, false)
|
||||
)
|
||||
);
|
||||
@@ -67,10 +72,59 @@ export const addMachine = withServerPromise(
|
||||
}
|
||||
);
|
||||
|
||||
export const updateCustomMachine = withServerPromise(
|
||||
async ({
|
||||
id,
|
||||
...data
|
||||
}: z.infer<typeof addCustomMachineSchema> & {
|
||||
id: string;
|
||||
}) => {
|
||||
const { userId } = auth();
|
||||
if (!userId) return { error: "No user id" };
|
||||
|
||||
const currentMachine = await db.query.machinesTable.findFirst({
|
||||
where: eq(machinesTable.id, id),
|
||||
});
|
||||
|
||||
if (!currentMachine) return { error: "Machine not found" };
|
||||
|
||||
// Check if snapshot or models have changed
|
||||
const snapshotChanged =
|
||||
JSON.stringify(data.snapshot) !== JSON.stringify(currentMachine.snapshot);
|
||||
const modelsChanged =
|
||||
JSON.stringify(data.models) !== JSON.stringify(currentMachine.models);
|
||||
|
||||
// return {
|
||||
// message: `snapshotChanged: ${snapshotChanged}, modelsChanged: ${modelsChanged}`,
|
||||
// };
|
||||
|
||||
await db.update(machinesTable).set(data).where(eq(machinesTable.id, id));
|
||||
|
||||
// If there are changes
|
||||
if (snapshotChanged || modelsChanged) {
|
||||
// Update status to building
|
||||
await db
|
||||
.update(machinesTable)
|
||||
.set({
|
||||
status: "building",
|
||||
endpoint: "not-ready",
|
||||
})
|
||||
.where(eq(machinesTable.id, id));
|
||||
|
||||
// Perform custom build if there are changes
|
||||
await buildMachine(data, currentMachine);
|
||||
redirect(`/machines/${id}`);
|
||||
} else {
|
||||
revalidatePath("/machines");
|
||||
}
|
||||
|
||||
return { message: "Machine Updated" };
|
||||
}
|
||||
);
|
||||
|
||||
export const addCustomMachine = withServerPromise(
|
||||
async (data: z.infer<typeof addCustomMachineSchema>) => {
|
||||
const { userId, orgId } = auth();
|
||||
const headersList = headers();
|
||||
|
||||
if (!userId) return { error: "No user id" };
|
||||
|
||||
@@ -88,51 +142,56 @@ export const addCustomMachine = withServerPromise(
|
||||
|
||||
const b = a[0];
|
||||
|
||||
// const origin = new URL(request.url).origin;
|
||||
const domain = headersList.get("x-forwarded-host") || "";
|
||||
const protocol = headersList.get("x-forwarded-proto") || "";
|
||||
// console.log("domain", domain);
|
||||
// console.log("domain", `${protocol}://${domain}/api/machine-built`);
|
||||
// return { message: "Machine Building" };
|
||||
|
||||
if (domain === "") {
|
||||
throw new Error("No domain");
|
||||
}
|
||||
|
||||
// Call remote builder
|
||||
const result = await fetch(`${process.env.MODAL_BUILDER_URL!}/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
machine_id: b.id,
|
||||
name: b.id,
|
||||
snapshot: JSON.parse(data.snapshot as string),
|
||||
callback_url: `${protocol}://${domain}/api/machine-built`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const error_log = await result.text();
|
||||
await db
|
||||
.update(machinesTable)
|
||||
.set({
|
||||
...data,
|
||||
status: "error",
|
||||
build_log: error_log,
|
||||
})
|
||||
.where(eq(machinesTable.id, b.id));
|
||||
throw new Error(`Error: ${result.statusText} ${error_log}`);
|
||||
}
|
||||
|
||||
await buildMachine(data, b);
|
||||
redirect(`/machines/${b.id}`);
|
||||
|
||||
// revalidatePath("/machines");
|
||||
return { message: "Machine Building" };
|
||||
}
|
||||
);
|
||||
|
||||
async function buildMachine(
|
||||
data: z.infer<typeof addCustomMachineSchema>,
|
||||
b: MachineType
|
||||
) {
|
||||
const headersList = headers();
|
||||
|
||||
const domain = headersList.get("x-forwarded-host") || "";
|
||||
const protocol = headersList.get("x-forwarded-proto") || "";
|
||||
|
||||
if (domain === "") {
|
||||
throw new Error("No domain");
|
||||
}
|
||||
|
||||
// Call remote builder
|
||||
const result = await fetch(`${process.env.MODAL_BUILDER_URL!}/create`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
machine_id: b.id,
|
||||
name: b.id,
|
||||
snapshot: data.snapshot, //JSON.parse( as string),
|
||||
callback_url: `${protocol}://${domain}/api/machine-built`,
|
||||
models: data.models, //JSON.parse(data.models as string),
|
||||
gpu: "T4",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!result.ok) {
|
||||
const error_log = await result.text();
|
||||
await db
|
||||
.update(machinesTable)
|
||||
.set({
|
||||
...data,
|
||||
status: "error",
|
||||
build_log: error_log,
|
||||
})
|
||||
.where(eq(machinesTable.id, b.id));
|
||||
throw new Error(`Error: ${result.statusText} ${error_log}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const updateMachine = withServerPromise(
|
||||
async ({
|
||||
id,
|
||||
|
||||
Reference in New Issue
Block a user