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

@@ -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 };