Merge branch 'nickkao/checkpoint-volume'

# Conflicts:
#	web/bun.lockb
This commit is contained in:
bennykok
2024-01-25 21:07:27 +08:00
23 changed files with 2708 additions and 30 deletions
+373
View File
@@ -0,0 +1,373 @@
"use client";
import { getRelativeTime } from "../lib/getRelativeTime";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { InsertModal, UpdateModal } from "./InsertModal";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import type { getAllUserCheckpoints } from "@/server/getAllUserCheckpoints";
import type {
ColumnDef,
ColumnFiltersState,
SortingState,
VisibilityState,
} from "@tanstack/react-table";
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from "@tanstack/react-table";
import { ArrowUpDown } from "lucide-react";
import * as React from "react";
import { addCivitaiCheckpoint } from "@/server/curdCheckpoint";
import { addCivitaiCheckpointSchema } from "@/server/addCheckpointSchema";
export type CheckpointItemList = NonNullable<
Awaited<ReturnType<typeof getAllUserCheckpoints>>
>[0];
export const columns: ColumnDef<CheckpointItemList>[] = [
{
accessorKey: "id",
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
},
{
accessorKey: "model_name",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Model Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
const checkpoint = row.original;
return (
<a
className="hover:underline flex gap-2"
href={`/storage/${checkpoint.id}`} // TODO
>
<span className="truncate max-w-[200px]">
{row.original.model_name}
</span>
{checkpoint.is_public ? (
<Badge variant="green">Public</Badge>
) : (
<Badge variant="orange">Private</Badge>
)}
</a>
);
},
},
{
accessorKey: "status",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
return (
<Badge variant={row.original.status === "failed" ? "red" : (row.original.status === "started" ? "yellow" : "green")}>
{row.original.status}
</Badge>
);
// NOTE: retry downloads on failures
// const oneHourAgo = new Date(new Date().getTime() - (60 * 60 * 1000));
// const lastUpdated = new Date(row.original.updated_at);
// const canRefresh = row.original.status === "failed" && lastUpdated < oneHourAgo;
// const canRefresh = row.original.status === "failed" && lastUpdated < oneHourAgo;
// cell: ({ row }) => {
// // const oneHourAgo = new Date(new Date().getTime() - (60 * 60 * 1000));
// // const lastUpdated = new Date(row.original.updated_at);
// // const canRefresh = row.original.status === "failed" && lastUpdated < oneHourAgo;
// const canReDownload = true;
//
// return (
// <div className="flex items-center space-x-2">
// <Badge
// variant={row.original.status === "failed"
// ? "red"
// : row.original.status === "started"
// ? "yellow"
// : "green"}
// >
// {row.original.status}
// </Badge>
// {canReDownload && (
// <RefreshCcw
// onClick={() => {
// redownloadCheckpoint(row.original);
// }}
// className="h-4 w-4 cursor-pointer" // Adjust the size with h-x and w-x classes
// />
// )}
// </div>
// );
// },
},
},
{
accessorKey: "upload_type",
header: ({ column }) => {
return (
<button
className="flex items-center hover:underline"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Source
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => {
return <Badge variant="cyan">{row.original.upload_type}</Badge>;
},
},
{
accessorKey: "date",
sortingFn: "datetime",
enableSorting: true,
header: ({ column }) => {
return (
<button
className="w-full flex items-center justify-end hover:underline truncate"
// variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Update Date
<ArrowUpDown className="ml-2 h-4 w-4" />
</button>
);
},
cell: ({ row }) => (
<div className="w-full capitalize text-right truncate">
{getRelativeTime(row.original.updated_at)}
</div>
),
},
// TODO: deletion and editing for future sprint
// {
// id: "actions",
// enableHiding: false,
// cell: ({ row }) => {
// const checkpoint = row.original;
//
// return (
// <DropdownMenu>
// <DropdownMenuTrigger asChild>
// <Button variant="ghost" className="h-8 w-8 p-0">
// <span className="sr-only">Open menu</span>
// <MoreHorizontal className="h-4 w-4" />
// </Button>
// </DropdownMenuTrigger>
// <DropdownMenuContent align="end">
// <DropdownMenuLabel>Actions</DropdownMenuLabel>
// <DropdownMenuItem
// className="text-destructive"
// onClick={() => {
// deleteWorkflow(checkpoint.id);
// }}
// >
// Delete Workflow
// </DropdownMenuItem>
// </DropdownMenuContent>
// </DropdownMenu>
// );
// },
// },
];
export function CheckpointList({ data }: { data: CheckpointItemList[] }) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
[]
);
const [columnVisibility, setColumnVisibility] =
React.useState<VisibilityState>({});
const [rowSelection, setRowSelection] = React.useState({});
const table = useReactTable({
data,
columns,
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onColumnVisibilityChange: setColumnVisibility,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
columnVisibility,
rowSelection,
},
});
return (
<div className="grid grid-rows-[auto,1fr,auto] h-full">
<div className="flex flex-row w-full items-center py-4">
<Input
placeholder="Filter workflows..."
value={(table.getColumn("name")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("name")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<div className="ml-auto flex gap-2">
<InsertModal
dialogClassName="sm:max-w-[600px]"
disabled={
false
// TODO: limitations based on plan
}
tooltip={"Add models using their civitai url!"}
title="Civitai Checkpoint"
description="Pick a model from civitai"
serverAction={addCivitaiCheckpoint}
formSchema={addCivitaiCheckpointSchema}
fieldConfig={{
civitai_url: {
fieldType: "fallback",
inputProps: { required: true },
description: (
<>
Pick a checkpoint from{" "}
<a
href="https://www.civitai.com/models"
target="_blank"
className="underline text-blue-600 hover:text-blue-800 visited:text-purple-600"
>
civitai.com
</a>{" "}
and place it's url here
</>
),
},
}}
/>
</div>
</div>
<ScrollArea className="h-full w-full rounded-md border">
<Table>
<TableHeader className="bg-background top-0 sticky">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</ScrollArea>
<div className="flex flex-row items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<div className="space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
</div>
);
}
+6 -2
View File
@@ -34,6 +34,10 @@ export function NavbarMenu({ className }: { className?: string }) {
name: "API Keys",
path: "/api-keys",
},
{
name: "Storage",
path: "/storage",
},
];
return (
@@ -42,9 +46,9 @@ export function NavbarMenu({ className }: { className?: string }) {
{isDesktop && (
<Tabs
defaultValue={pathname}
className="w-[300px] flex pointer-events-auto"
className="w-[400px] flex pointer-events-auto"
>
<TabsList className="grid w-full grid-cols-3">
<TabsList className="grid w-full grid-cols-4">
{pages.map((page) => (
<TabsTrigger
key={page.name}
@@ -0,0 +1,86 @@
// NOTE: this is WIP for doing client side validation for civitai model downloading
import type { AutoFormInputComponentProps } from "../ui/auto-form/types";
import { FormControl, FormItem, FormLabel } from "../ui/form";
import { LoadingIcon } from "@/components/LoadingIcon";
import * as React from "react";
import AutoFormInput from "../ui/auto-form/fields/input";
import { useDebouncedCallback } from "use-debounce";
import { CivitaiModelResponse } from "@/types/civitai";
import { z } from "zod";
import { insertCivitaiCheckpointSchema } from "@/db/schema";
function getUrl(civitai_url: string) {
// expect to be a URL to be https://civitai.com/models/36520
// possiblity with slugged name and query-param modelVersionId
const baseUrl = "https://civitai.com/api/v1/models/";
const url = new URL(civitai_url);
const pathSegments = url.pathname.split("/");
const modelId = pathSegments[pathSegments.indexOf("models") + 1];
const modelVersionId = url.searchParams.get("modelVersionId");
return { url: baseUrl + modelId, modelVersionId };
}
export default function AutoFormCheckpointInput(
props: AutoFormInputComponentProps
) {
const [loading, setLoading] = React.useState(false);
const [modelRes, setModelRes] =
React.useState<z.infer<typeof CivitaiModelResponse>>();
const [modelVersionid, setModelVersionId] = React.useState<string | null>();
const { label, isRequired, fieldProps, zodItem, fieldConfigItem } = props;
const handleSearch = useDebouncedCallback((search) => {
const validationResult =
insertCivitaiCheckpointSchema.shape.civitai_url.safeParse(search);
if (!validationResult.success) {
console.error(validationResult.error);
// Optionally set an error state here
return;
}
setLoading(true);
const controller = new AbortController();
const { url, modelVersionId: versionId } = getUrl(search);
setModelVersionId(versionId);
fetch(url, {
signal: controller.signal,
})
.then((x) => x.json())
.then((a) => {
const res = CivitaiModelResponse.parse(a);
console.log(a);
console.log(res);
setModelRes(res);
setLoading(false);
});
return () => {
controller.abort();
setLoading(false);
};
}, 300);
const modifiedField = {
...fieldProps,
// onChange: (event: React.ChangeEvent<HTMLInputElement>) => {
// handleSearch(event.target.value);
// },
};
return (
<FormItem>
{fieldConfigItem.inputProps?.showLabel && (
<FormLabel>
{label}
{isRequired && <span className="text-destructive">*</span>}
</FormLabel>
)}
<FormControl>
<AutoFormInput {...props} fieldProps={modifiedField} />
</FormControl>
</FormItem>
);
}