Skyvern Forms UI (#1330)

This commit is contained in:
Shuchang Zheng
2024-12-05 11:56:09 -08:00
committed by GitHub
parent df4d5df48d
commit a22592001b
18 changed files with 479 additions and 234 deletions

View File

@@ -93,17 +93,18 @@ export type TaskApiResponse = {
};
export type CreateTaskRequest = {
title: string | null;
title?: string | null;
url: string;
webhook_callback_url: string | null;
navigation_goal: string | null;
data_extraction_goal: string | null;
navigation_payload: Record<string, unknown> | string | null;
extracted_information_schema: Record<string, unknown> | string | null;
error_code_mapping: Record<string, string> | null;
proxy_location: ProxyLocation | null;
totp_verification_url: string | null;
totp_identifier: string | null;
webhook_callback_url?: string | null;
navigation_goal?: string | null;
data_extraction_goal?: string | null;
navigation_payload?: Record<string, unknown> | string | null;
extracted_information_schema?: Record<string, unknown> | string | null;
error_code_mapping?: Record<string, string> | null;
proxy_location?: ProxyLocation | null;
totp_verification_url?: string | null;
totp_identifier?: string | null;
application?: string | null;
};
export type User = {

View File

@@ -0,0 +1,87 @@
import { useSidebarStore } from "@/store/SidebarStore";
import { cn } from "@/util/utils";
import { NavLink, useMatches } from "react-router-dom";
import { Badge } from "./ui/badge";
type Props = {
title: string;
links: Array<{
label: string;
to: string;
disabled?: boolean;
icon?: React.ReactNode;
}>;
};
function NavLinkGroup({ title, links }: Props) {
const { collapsed } = useSidebarStore();
const matches = useMatches();
const groupIsActive = matches.some((match) => {
const inputs = links.map((link) => link.to);
return inputs.includes(match.pathname);
});
return (
<div
className={cn("flex flex-col gap-[0.625rem]", {
"items-center": collapsed,
})}
>
<div
className={cn("py-2 text-slate-400", {
"text-primary": groupIsActive,
})}
>
<div
className={cn({
"text-center": collapsed,
})}
>
{title}
</div>
</div>
<div className="space-y-2">
{links.map((link) => {
return (
<NavLink
key={link.to}
to={link.to}
className={({ isActive }) => {
return cn(
"block rounded-lg py-2 pl-3 text-slate-400 hover:bg-muted hover:text-primary",
{
"bg-muted": isActive,
},
{
"text-primary": groupIsActive,
"px-3": collapsed,
},
);
}}
>
<div className="flex justify-between">
<div className="flex items-center gap-2">
{link.icon}
{!collapsed && link.label}
</div>
{!collapsed && link.disabled && (
<Badge
className="rounded-[40px] px-2 py-1"
style={{
backgroundColor: groupIsActive ? "#301615" : "#1E1016",
color: groupIsActive ? "#EA580C" : "#8D3710",
}}
>
Training
</Badge>
)}
</div>
</NavLink>
);
})}
</div>
</div>
);
}
export { NavLinkGroup };

View File

@@ -0,0 +1,57 @@
import { ChevronDownIcon } from "@radix-ui/react-icons";
import { Button } from "./ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import { Checkbox } from "./ui/checkbox";
import { Status } from "@/api/types";
type Item = {
label: string;
value: Status;
};
type Props = {
options: Array<Item>;
values: Array<Status>;
onChange: (values: Array<Status>) => void;
};
function StatusFilterDropdown({ options, values, onChange }: Props) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline">
Filter by Status <ChevronDownIcon className="ml-2" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{options.map((item) => {
return (
<div
key={item.value}
className="flex items-center gap-2 p-2 text-sm"
>
<Checkbox
id={item.value}
checked={values.includes(item.value)}
onCheckedChange={(checked) => {
if (checked) {
onChange([...values, item.value]);
} else {
onChange(values.filter((value) => value !== item.value));
}
}}
/>
<label htmlFor={item.value}>{item.label}</label>
</div>
);
})}
</DropdownMenuContent>
</DropdownMenu>
);
}
export { StatusFilterDropdown };

View File

@@ -0,0 +1,26 @@
type Props = {
className?: string;
};
function BagIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
>
<path
d="M16.0004 9V6C16.0004 3.79086 14.2095 2 12.0004 2C9.79123 2 8.00037 3.79086 8.00037 6V9M3.59237 10.352L2.99237 16.752C2.82178 18.5717 2.73648 19.4815 3.03842 20.1843C3.30367 20.8016 3.76849 21.3121 4.35839 21.6338C5.0299 22 5.94374 22 7.77142 22H16.2293C18.057 22 18.9708 22 19.6423 21.6338C20.2322 21.3121 20.6971 20.8016 20.9623 20.1843C21.2643 19.4815 21.179 18.5717 21.0084 16.752L20.4084 10.352C20.2643 8.81535 20.1923 8.04704 19.8467 7.46616C19.5424 6.95458 19.0927 6.54511 18.555 6.28984C17.9444 6 17.1727 6 15.6293 6L8.37142 6C6.82806 6 6.05638 6 5.44579 6.28984C4.90803 6.54511 4.45838 6.95458 4.15403 7.46616C3.80846 8.04704 3.73643 8.81534 3.59237 10.352Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export { BagIcon };

View File

@@ -0,0 +1,26 @@
type Props = {
className?: string;
};
function GovernmentIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
>
<path
d="M5 9.00002V17M9.5 9.00002V17M14.5 9.00002V17M19 9.00002V17M3 18.6L3 19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7952 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H19.4C19.9601 21 20.2401 21 20.454 20.891C20.6422 20.7952 20.7951 20.6422 20.891 20.454C21 20.2401 21 19.9601 21 19.4V18.6C21 18.04 21 17.7599 20.891 17.546C20.7951 17.3579 20.6422 17.2049 20.454 17.109C20.2401 17 19.9601 17 19.4 17H4.6C4.03995 17 3.75992 17 3.54601 17.109C3.35785 17.2049 3.20487 17.3579 3.10899 17.546C3 17.7599 3 18.04 3 18.6ZM11.6529 3.07715L4.25291 4.7216C3.80585 4.82094 3.58232 4.87062 3.41546 4.99082C3.26829 5.09685 3.15273 5.24092 3.08115 5.40759C3 5.59654 3 5.82553 3 6.28349L3 7.40002C3 7.96007 3 8.2401 3.10899 8.45401C3.20487 8.64217 3.35785 8.79515 3.54601 8.89103C3.75992 9.00002 4.03995 9.00002 4.6 9.00002H19.4C19.9601 9.00002 20.2401 9.00002 20.454 8.89103C20.6422 8.79515 20.7951 8.64217 20.891 8.45401C21 8.2401 21 7.96007 21 7.40002V6.2835C21 5.82553 21 5.59655 20.9188 5.40759C20.8473 5.24092 20.7317 5.09685 20.5845 4.99082C20.4177 4.87062 20.1942 4.82094 19.7471 4.7216L12.3471 3.07715C12.2176 3.04837 12.1528 3.03398 12.0874 3.02824C12.0292 3.02314 11.9708 3.02314 11.9126 3.02824C11.8472 3.03398 11.7824 3.04837 11.6529 3.07715Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export { GovernmentIcon };

View File

@@ -0,0 +1,26 @@
type Props = {
className?: string;
};
function ReceiptIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
>
<path
d="M4 7.8C4 6.11984 4 5.27976 4.32698 4.63803C4.6146 4.07354 5.07354 3.6146 5.63803 3.32698C6.27976 3 7.11984 3 8.8 3H15.2C16.8802 3 17.7202 3 18.362 3.32698C18.9265 3.6146 19.3854 4.07354 19.673 4.63803C20 5.27976 20 6.11984 20 7.8V21L17.25 19L14.75 21L12 19L9.25 21L6.75 19L4 21V7.8Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export { ReceiptIcon };

View File

@@ -0,0 +1,26 @@
type Props = {
className?: string;
};
function ToolIcon({ className }: Props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={className}
>
<path
d="M15.6316 7.63137C15.2356 7.23535 15.0376 7.03735 14.9634 6.80902C14.8981 6.60817 14.8981 6.39183 14.9634 6.19098C15.0376 5.96265 15.2356 5.76465 15.6316 5.36863L18.47 2.53026C17.7168 2.18962 16.8806 2 16.0002 2C12.6865 2 10.0002 4.68629 10.0002 8C10.0002 8.49104 10.0592 8.9683 10.1705 9.42509C10.2896 9.91424 10.3492 10.1588 10.3387 10.3133C10.3276 10.4751 10.3035 10.5612 10.2289 10.7051C10.1576 10.8426 10.0211 10.9791 9.74804 11.2522L3.50023 17.5C2.6718 18.3284 2.6718 19.6716 3.50023 20.5C4.32865 21.3284 5.6718 21.3284 6.50023 20.5L12.748 14.2522C13.0211 13.9791 13.1576 13.8426 13.2951 13.7714C13.4391 13.6968 13.5251 13.6727 13.6869 13.6616C13.8414 13.651 14.086 13.7106 14.5751 13.8297C15.0319 13.941 15.5092 14 16.0002 14C19.3139 14 22.0002 11.3137 22.0002 8C22.0002 7.11959 21.8106 6.28347 21.47 5.53026L18.6316 8.36863C18.2356 8.76465 18.0376 8.96265 17.8092 9.03684C17.6084 9.1021 17.3921 9.1021 17.1912 9.03684C16.9629 8.96265 16.7649 8.76465 16.3689 8.36863L15.6316 7.63137Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
export { ToolIcon };

View File

@@ -4,15 +4,13 @@ import { Settings } from "./routes/settings/Settings";
import { SettingsPageLayout } from "./routes/settings/SettingsPageLayout";
import { TasksPageLayout } from "./routes/tasks/TasksPageLayout";
import { CreateNewTaskFormPage } from "./routes/tasks/create/CreateNewTaskFormPage";
import { CreateNewTaskLayout } from "./routes/tasks/create/CreateNewTaskLayout";
import { TaskTemplates } from "./routes/tasks/create/TaskTemplates";
import { RetryTask } from "./routes/tasks/create/retry/RetryTask";
import { StepArtifactsLayout } from "./routes/tasks/detail/StepArtifactsLayout";
import { TaskActions } from "./routes/tasks/detail/TaskActions";
import { TaskDetails } from "./routes/tasks/detail/TaskDetails";
import { TaskParameters } from "./routes/tasks/detail/TaskParameters";
import { TaskRecording } from "./routes/tasks/detail/TaskRecording";
import { TaskList } from "./routes/tasks/list/TaskList";
import { TasksPage } from "./routes/tasks/list/TasksPage";
import { WorkflowPage } from "./routes/workflows/WorkflowPage";
import { WorkflowRun } from "./routes/workflows/WorkflowRun";
import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters";
@@ -27,7 +25,7 @@ const router = createBrowserRouter([
children: [
{
index: true,
element: <Navigate to="/create" />,
element: <Navigate to="/tasks" />,
},
{
path: "tasks",
@@ -35,7 +33,21 @@ const router = createBrowserRouter([
children: [
{
index: true,
element: <TaskList />,
element: <TasksPage />,
},
{
path: "create",
element: <Outlet />,
children: [
{
path: ":template",
element: <CreateNewTaskFormPage />,
},
{
path: "retry/:taskId",
element: <RetryTask />,
},
],
},
{
path: ":taskId",
@@ -65,24 +77,6 @@ const router = createBrowserRouter([
},
],
},
{
path: "create",
element: <CreateNewTaskLayout />,
children: [
{
index: true,
element: <TaskTemplates />,
},
{
path: ":template",
element: <CreateNewTaskFormPage />,
},
{
path: "retry/:taskId",
element: <RetryTask />,
},
],
},
{
path: "workflows",
element: <WorkflowsPageLayout />,

View File

@@ -1,75 +1,43 @@
import { RobotIcon } from "@/components/icons/RobotIcon";
import { NavLinkGroup } from "@/components/NavLinkGroup";
import { useSidebarStore } from "@/store/SidebarStore";
import { cn } from "@/util/utils";
import {
GearIcon,
LightningBoltIcon,
ListBulletIcon,
PlusCircledIcon,
} from "@radix-ui/react-icons";
import { NavLink } from "react-router-dom";
import { GearIcon, LightningBoltIcon } from "@radix-ui/react-icons";
type Props = {
collapsed: boolean;
};
function SideNav() {
const { collapsed } = useSidebarStore();
function SideNav({ collapsed }: Props) {
return (
<nav className="space-y-2">
<NavLink
to="create"
className={({ isActive }) => {
return cn(
"flex h-[3.25rem] items-center gap-4 rounded-2xl px-5 hover:bg-muted",
{
"bg-muted": isActive,
},
);
}}
>
<PlusCircledIcon className="h-6 w-6" />
{!collapsed && <span className="text-lg">Create</span>}
</NavLink>
<NavLink
to="tasks"
className={({ isActive }) => {
return cn(
"flex h-[3.25rem] items-center gap-4 rounded-2xl px-5 hover:bg-muted",
{
"bg-muted": isActive,
},
);
}}
>
<ListBulletIcon className="h-6 w-6" />
{!collapsed && <span className="text-lg">Tasks</span>}
</NavLink>
<NavLink
to="workflows"
className={({ isActive }) => {
return cn(
"flex h-[3.25rem] items-center gap-4 rounded-2xl px-5 hover:bg-muted",
{
"bg-muted": isActive,
},
);
}}
>
<LightningBoltIcon className="h-6 w-6" />
{!collapsed && <span className="text-lg">Workflows</span>}
</NavLink>
<NavLink
to="settings"
className={({ isActive }) => {
return cn(
"flex h-[3.25rem] items-center gap-4 rounded-2xl px-5 hover:bg-muted",
{
"bg-muted": isActive,
},
);
}}
>
<GearIcon className="h-6 w-6" />
{!collapsed && <span className="text-lg">Settings</span>}
</NavLink>
<nav
className={cn("space-y-5", {
"items-center": collapsed,
})}
>
<NavLinkGroup
title={"Build"}
links={[
{
label: "Tasks",
to: "/tasks",
icon: <RobotIcon className="size-6" />,
},
{
label: "Workflows",
to: "/workflows",
icon: <LightningBoltIcon className="size-6" />,
},
]}
/>
<NavLinkGroup
title={"General"}
links={[
{
label: "Settings",
to: "/settings",
icon: <GearIcon className="size-6" />,
},
]}
/>
</nav>
);
}

View File

@@ -22,7 +22,7 @@ function SidebarContent({ useCollapsedState }: Props) {
{collapsed ? <LogoMinimized /> : <Logo />}
</div>
</Link>
<SideNav collapsed={collapsed} />
<SideNav />
<div
className={cn("mt-auto flex min-h-16", {
"justify-center": collapsed,

View File

@@ -16,7 +16,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { useToast } from "@/components/ui/use-toast";
import { toast } from "@/components/ui/use-toast";
import { useApiCredential } from "@/hooks/useApiCredential";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
@@ -88,7 +88,6 @@ type Section = "base" | "extraction" | "advanced";
function CreateNewTaskForm({ initialValues }: Props) {
const queryClient = useQueryClient();
const { toast } = useToast();
const credentialGetter = useCredentialGetter();
const apiCredential = useApiCredential();
const [activeSections, setActiveSections] = useState<Array<Section>>([

View File

@@ -1,7 +1,6 @@
import { getClient } from "@/api/AxiosClient";
import { TaskGenerationApiResponse } from "@/api/types";
import img from "@/assets/promptBoxBg.png";
import { BookIcon } from "@/components/icons/BookIcon";
import { CartIcon } from "@/components/icons/CartIcon";
import { GraphIcon } from "@/components/icons/GraphIcon";
import { InboxIcon } from "@/components/icons/InboxIcon";
@@ -16,6 +15,7 @@ import {
GearIcon,
PaperPlaneIcon,
Pencil1Icon,
PlusIcon,
ReloadIcon,
} from "@radix-ui/react-icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
@@ -97,11 +97,6 @@ const exampleCases = [
label: "Search for AAPL on Google Finance",
icon: <GraphIcon className="size-6" />,
},
{
key: "NYTBestseller",
label: "Get the top NYT bestseller",
icon: <BookIcon className="size-6" />,
},
{
key: "topRankedFootballTeam",
label: "Get the top ranked football team",
@@ -196,7 +191,7 @@ function PromptBox() {
const taskGenerationResponse =
await getTaskFromPromptMutation.mutateAsync(prompt);
await saveTaskMutation.mutateAsync(taskGenerationResponse);
navigate("/create/from-prompt", {
navigate("/tasks/create/from-prompt", {
state: {
data: taskGenerationResponse,
},
@@ -209,13 +204,22 @@ function PromptBox() {
</div>
</div>
<div className="flex flex-wrap justify-center gap-4 rounded-sm bg-slate-elevation1 p-4">
<div
className="flex cursor-pointer gap-2 whitespace-normal rounded-sm border-2 border-dashed bg-slate-elevation3 px-4 py-3 hover:bg-slate-elevation5 lg:whitespace-nowrap"
onClick={() => {
navigate("/tasks/create/blank");
}}
>
<PlusIcon className="size-6" />
Build Your Own
</div>
{exampleCases.map((example) => {
return (
<div
key={example.key}
className="flex cursor-pointer gap-2 whitespace-normal rounded-sm bg-slate-elevation3 px-4 py-3 hover:bg-slate-elevation5 lg:whitespace-nowrap"
onClick={() => {
navigate(`/create/${example.key}`);
navigate(`/tasks/create/${example.key}`);
}}
>
<div>{example.icon}</div>

View File

@@ -22,12 +22,12 @@ function RetryTask() {
<CreateNewTaskForm
initialValues={{
url: task.request.url,
navigationGoal: task.request.navigation_goal,
navigationGoal: task.request.navigation_goal ?? null,
navigationPayload:
typeof task.request.navigation_payload === "string"
? task.request.navigation_payload
: JSON.stringify(task.request.navigation_payload, null, 2),
dataExtractionGoal: task.request.data_extraction_goal,
dataExtractionGoal: task.request.data_extraction_goal ?? null,
extractedInformationSchema:
typeof task.request.extracted_information_schema === "string"
? task.request.extracted_information_schema
@@ -36,13 +36,13 @@ function RetryTask() {
null,
2,
),
webhookCallbackUrl: task.request.webhook_callback_url,
totpIdentifier: task.request.totp_identifier,
totpVerificationUrl: task.request.totp_verification_url,
webhookCallbackUrl: task.request.webhook_callback_url ?? null,
totpIdentifier: task.request.totp_identifier ?? null,
totpVerificationUrl: task.request.totp_verification_url ?? null,
errorCodeMapping: task.request.error_code_mapping
? JSON.stringify(task.request.error_code_mapping, null, 2)
: "",
proxyLocation: task.request.proxy_location,
proxyLocation: task.request.proxy_location ?? null,
}}
/>
</div>

View File

@@ -242,7 +242,7 @@ function TaskDetails() {
)}
{taskHasTerminalState && (
<Button asChild>
<Link to={`/create/retry/${task.task_id}`}>
<Link to={`/tasks/create/retry/${task.task_id}`}>
<PlayIcon className="mr-2 h-4 w-4" />
Rerun
</Link>

View File

@@ -71,7 +71,7 @@ function TaskParameters() {
value={
typeof task.request.navigation_payload === "object"
? JSON.stringify(task.request.navigation_payload, null, 2)
: task.request.navigation_payload
: task.request.navigation_payload ?? ""
}
readOnly
minHeight="96px"
@@ -107,7 +107,7 @@ function TaskParameters() {
null,
2,
)
: task.request.extracted_information_schema
: task.request.extracted_information_schema ?? ""
}
readOnly
minHeight="96px"

View File

@@ -1,18 +1,7 @@
import { getClient } from "@/api/AxiosClient";
import { TaskApiResponse } from "@/api/types";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { useQuery } from "@tanstack/react-query";
import { useNavigate, useSearchParams } from "react-router-dom";
import { PAGE_SIZE } from "../constants";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TaskListSkeletonRows } from "./TaskListSkeletonRows";
import { Status, TaskApiResponse } from "@/api/types";
import { StatusBadge } from "@/components/StatusBadge";
import { StatusFilterDropdown } from "@/components/StatusFilterDropdown";
import {
Pagination,
PaginationContent,
@@ -21,16 +10,69 @@ import {
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
import { StatusBadge } from "@/components/StatusBadge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { basicLocalTimeFormat, basicTimeFormat } from "@/util/timeFormat";
import { cn } from "@/util/utils";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { TaskActions } from "./TaskActions";
import { TaskListSkeletonRows } from "./TaskListSkeletonRows";
type StatusDropdownItem = {
label: string;
value: Status;
};
const statusDropdownItems: Array<StatusDropdownItem> = [
{
label: "Completed",
value: Status.Completed,
},
{
label: "Failed",
value: Status.Failed,
},
{
label: "Running",
value: Status.Running,
},
{
label: "Queued",
value: Status.Queued,
},
{
label: "Terminated",
value: Status.Terminated,
},
{
label: "Canceled",
value: Status.Canceled,
},
{
label: "Timed Out",
value: Status.TimedOut,
},
{
label: "Created",
value: Status.Created,
},
];
function TaskHistory() {
const credentialGetter = useCredentialGetter();
const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
const navigate = useNavigate();
const [statusFilters, setStatusFilters] = useState<Array<Status>>([]);
const {
data: tasks,
@@ -38,17 +80,15 @@ function TaskHistory() {
isError,
error,
} = useQuery<Array<TaskApiResponse>>({
queryKey: ["tasks", "history", page],
queryKey: ["tasks", { statusFilters }, page],
queryFn: async () => {
const client = await getClient(credentialGetter);
const params = new URLSearchParams();
params.append("page", String(page));
params.append("page_size", String(PAGE_SIZE));
params.append("task_status", "completed");
params.append("task_status", "failed");
params.append("task_status", "terminated");
params.append("task_status", "timed_out");
params.append("task_status", "canceled");
statusFilters.forEach((status) => {
params.append("task_status", status);
});
params.append("only_standalone_tasks", "true");
return client
@@ -76,7 +116,15 @@ function TaskHistory() {
}
return (
<>
<div className="space-y-4">
<header className="flex items-center justify-between">
<h1 className="text-2xl">Task Runs</h1>
<StatusFilterDropdown
values={statusFilters}
onChange={setStatusFilters}
options={statusDropdownItems}
/>
</header>
<div className="rounded-md border">
<Table>
<TableHeader>
@@ -133,38 +181,37 @@ function TaskHistory() {
)}
</TableBody>
</Table>
<Pagination className="pt-2">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={cn({ "cursor-not-allowed": page === 1 })}
onClick={() => {
if (page === 1) {
return;
}
const params = new URLSearchParams();
params.set("page", String(Math.max(1, page - 1)));
setSearchParams(params, { replace: true });
}}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink>{page}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={() => {
const params = new URLSearchParams();
params.set("page", String(page + 1));
setSearchParams(params, { replace: true });
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
<Pagination className="pt-2">
<PaginationContent>
<PaginationItem>
<PaginationPrevious
className={cn({ "cursor-not-allowed": page === 1 })}
onClick={() => {
if (page === 1) {
return;
}
const params = new URLSearchParams();
params.set("page", String(Math.max(1, page - 1)));
setSearchParams(params, { replace: true });
}}
/>
</PaginationItem>
<PaginationItem>
<PaginationLink>{page}</PaginationLink>
</PaginationItem>
<PaginationItem>
<PaginationNext
onClick={() => {
const params = new URLSearchParams();
params.set("page", String(page + 1));
setSearchParams(params, { replace: true });
}}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</>
</div>
);
}

View File

@@ -1,59 +0,0 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { QueuedTasks } from "../running/QueuedTasks";
import { RunningTasks } from "../running/RunningTasks";
import { TaskHistory } from "./TaskHistory";
import { Button } from "@/components/ui/button";
import { Link } from "react-router-dom";
import { PlusIcon } from "@radix-ui/react-icons";
function TaskList() {
return (
<div className="space-y-8">
<header className="flex justify-end">
<Button asChild>
<Link to="/create">
<PlusIcon className="mr-2 h-4 w-4" />
Create New Task
</Link>
</Button>
</header>
<Card>
<CardHeader className="border-b-2">
<CardTitle className="text-xl">Running Tasks</CardTitle>
<CardDescription>Tasks that are currently running</CardDescription>
</CardHeader>
<CardContent className="p-4">
<div className="grid grid-cols-4 gap-4">
<RunningTasks />
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="border-b-2">
<CardTitle className="text-xl">Queued Tasks</CardTitle>
<CardDescription>Tasks that are waiting to run</CardDescription>
</CardHeader>
<CardContent className="p-4">
<QueuedTasks />
</CardContent>
</Card>
<Card>
<CardHeader className="border-b-2">
<CardTitle className="text-xl">Task History</CardTitle>
<CardDescription>Tasks you have run previously</CardDescription>
</CardHeader>
<CardContent className="p-4">
<TaskHistory />
</CardContent>
</Card>
</div>
);
}
export { TaskList };

View File

@@ -0,0 +1,43 @@
import { TaskHistory } from "./TaskHistory";
import { PromptBox } from "../create/PromptBox";
import { useState } from "react";
import { cn } from "@/util/utils";
import { SavedTasks } from "../create/SavedTasks";
function TasksPage() {
const [view, setView] = useState<"history" | "myTasks">("history");
return (
<div className="space-y-8">
<PromptBox />
<div className="flex w-fit gap-1 rounded-sm border border-slate-700 p-2">
<div
className={cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": view === "history",
},
)}
onClick={() => setView("history")}
>
Run History
</div>
<div
className={cn(
"cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700",
{
"bg-slate-700": view === "myTasks",
},
)}
onClick={() => setView("myTasks")}
>
My Tasks
</div>
</div>
{view === "history" && <TaskHistory />}
{view === "myTasks" && <SavedTasks />}
</div>
);
}
export { TasksPage };