Skip to content

Commit

Permalink
🔄 synced local 'skyvern-frontend/src/' with remote 'skyvern-frontend/…
Browse files Browse the repository at this point in the history
…src/'

<!-- ELLIPSIS_HIDDEN -->

| 🚀 | This description was created by [Ellipsis](https://www.ellipsis.dev) for commit d6fb3b10d334b87412cd692095b339b40e72f982  |
|--------|--------|

### Summary:
Introduced a new 'Workflows' tab with routing, components, API integrations, pagination, loading states, and added a `FileUpload` component with error handling using a toast notification.

**Key points**:
- Introduced a new 'Workflows' tab with routing, components, API integrations, pagination, and loading states.
- Added `WorkflowsPageLayout`, `Workflows`, and `WorkflowPage` components in `skyvern-frontend/src/routes/workflows/`.
- Updated `skyvern-frontend/cloud/router.tsx` and `skyvern-frontend/src/router.tsx` to include new routes for workflows.
- Modified `skyvern-frontend/cloud/routes/root/SideNav.tsx` to add a 'Workflows (Beta)' link.
- Introduced `FileUpload` component in `skyvern-frontend/src/components/FileUpload.tsx` for handling file inputs with error handling using a toast notification.
- Added `Checkbox` component in `skyvern-frontend/src/components/ui/checkbox.tsx`.
- Updated `skyvern-frontend/package.json` to include `@radix-ui/react-checkbox` dependency.
- Adjusted CSS variables in `skyvern-frontend/cloud/index.css` for better theming.
- Enabled Sentry only in production in `skyvern-frontend/cloud/index.tsx`.
- Added types for workflow parameters and responses in `skyvern-frontend/src/api/types.ts`.
- Implemented pagination and loading states in `skyvern-frontend/src/routes/workflows/WorkflowPage.tsx` and `skyvern-frontend/src/routes/workflows/Workflows.tsx`.
- Updated `RunWorkflowForm` in `skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx` to invalidate `workflowRuns` query on success.
- Updated `skyvern-frontend/src/routes/workflows/WorkflowRun.tsx` to handle cases where a workflow run does not have any tasks.

----
Generated with ❤️ by [ellipsis.dev](https://www.ellipsis.dev)

<!-- ELLIPSIS_HIDDEN -->
  • Loading branch information
ykeremy committed Jul 11, 2024
1 parent 3a04767 commit b74f0d1
Show file tree
Hide file tree
Showing 17 changed files with 1,207 additions and 8 deletions.
40 changes: 39 additions & 1 deletion skyvern-frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,21 @@ export type ApiKeyApiResponse = {
valid: boolean;
};

export const WorkflowParameterType = {
String: "string",
Integer: "integer",
Float: "float",
Boolean: "boolean",
JSON: "json",
FileURL: "file_url",
} as const;

export type WorkflowParameterType =
(typeof WorkflowParameterType)[keyof typeof WorkflowParameterType];

export type WorkflowParameter = {
workflow_parameter_id: string;
workflow_parameter_type?: string;
workflow_parameter_type: WorkflowParameterType;
key: string;
description: string | null;
workflow_id: string;
Expand Down Expand Up @@ -144,6 +156,7 @@ export type WorkflowBlock = {
export type WorkflowApiResponse = {
workflow_id: string;
organization_id: string;
is_saved_task: boolean;
title: string;
workflow_permanent_id: string;
version: number;
Expand Down Expand Up @@ -204,3 +217,28 @@ export type Action = {
stepId: string;
index: number;
};

export type WorkflowRunApiResponse = {
workflow_permanent_id: string;
workflow_run_id: string;
workflow_id: string;
status: Status;
proxy_location: string;
webhook_callback_url: string;
created_at: string;
modified_at: string;
};

export type WorkflowRunStatusApiResponse = {
workflow_id: string;
workflow_run_id: string;
status: Status;
proxy_location: string;
webhook_callback_url: string | null;
created_at: string;
modified_at: string;
parameters: Record<string, unknown>;
screenshot_urls: Array<string> | null;
recording_url: string | null;
outputs: Record<string, unknown> | null;
};
203 changes: 203 additions & 0 deletions skyvern-frontend/src/components/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { getClient } from "@/api/AxiosClient";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils";
import { ReloadIcon } from "@radix-ui/react-icons";
import { useMutation } from "@tanstack/react-query";
import { useId, useState } from "react";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Label } from "./ui/label";
import { toast } from "./ui/use-toast";

export type FileInputValue =
| {
s3uri: string;
presignedUrl: string;
}
| string
| null;

type Props = {
value: FileInputValue;
onChange: (value: FileInputValue) => void;
};

const FILE_SIZE_LIMIT_IN_BYTES = 10 * 1024 * 1024; // 10 MB

function showFileSizeError() {
toast({
variant: "destructive",
title: "File size limit exceeded",
description:
"The file you are trying to upload exceeds the 10MB limit, please try again with a different file",
});
}

function FileUpload({ value, onChange }: Props) {
const credentialGetter = useCredentialGetter();
const [file, setFile] = useState<File | null>(null);
const [fileUrl, setFileUrl] = useState<string>("");
const [highlight, setHighlight] = useState(false);
const inputId = useId();

const uploadFileMutation = useMutation({
mutationFn: async (file: File) => {
const client = await getClient(credentialGetter);
const formData = new FormData();
formData.append("file", file);
return client.post<
FormData,
{
data: {
s3_uri: string;
presigned_url: string;
};
}
>("/upload_file", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
},
onSuccess: (response) => {
onChange({
s3uri: response.data.s3_uri,
presignedUrl: response.data.presigned_url,
});
},
onError: (error) => {
setFile(null);
toast({
variant: "destructive",
title: "Failed to upload file",
description: `An error occurred while uploading the file: ${error.message}`,
});
},
});

const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0] as File;
if (file.size > FILE_SIZE_LIMIT_IN_BYTES) {
showFileSizeError();
return;
}
setFile(file);
uploadFileMutation.mutate(file);
}
};

function reset() {
setFile(null);
onChange(null);
}

if (value === null) {
return (
<div className="flex gap-4">
<div className="w-1/2">
<Label
htmlFor={inputId}
className={cn(
"flex w-full cursor-pointer border border-dashed items-center justify-center py-8",
{
"border-slate-500": highlight,
},
)}
onDragEnter={(event) => {
event.preventDefault();
event.stopPropagation();
setHighlight(true);
}}
onDragOver={(event) => {
event.preventDefault();
event.stopPropagation();
setHighlight(true);
}}
onDragLeave={(event) => {
event.preventDefault();
event.stopPropagation();
setHighlight(false);
}}
onDrop={(event) => {
event.preventDefault();
event.stopPropagation();
setHighlight(false);
if (
event.dataTransfer.files &&
event.dataTransfer.files.length > 0
) {
const file = event.dataTransfer.files[0] as File;
if (file.size > FILE_SIZE_LIMIT_IN_BYTES) {
showFileSizeError();
return;
}
setFile(file);
uploadFileMutation.mutate(file);
}
}}
>
<input
id={inputId}
type="file"
onChange={handleFileChange}
accept=".csv"
className="hidden"
/>
<div className="max-w-full truncate flex gap-2">
{uploadFileMutation.isPending && (
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
)}
<span>
{file
? file.name
: "Drag and drop file here or click to select (Max 10MB)"}
</span>
</div>
</Label>
</div>
<div className="flex flex-col items-center justify-center before:flex before:content-[''] before:bg-slate-600">
OR
</div>
<div className="w-1/2">
<Label>File URL</Label>
<div className="flex gap-2">
<Input
value={fileUrl}
onChange={(e) => setFileUrl(e.target.value)}
/>
<Button
onClick={() => {
onChange(fileUrl);
}}
>
Save
</Button>
</div>
</div>
</div>
);
}

if (typeof value === "string") {
return (
<div className="flex gap-4 items-center">
<span>{value}</span>
<Button onClick={() => reset()}>Change</Button>
</div>
);
}

if (typeof value === "object" && file && "s3uri" in value) {
return (
<div className="flex gap-4 items-center">
<a href={value.presignedUrl} className="underline">
<span>{file.name}</span>
</a>
<Button onClick={() => reset()}>Change</Button>
</div>
);
}
}

export { FileUpload };
28 changes: 28 additions & 0 deletions skyvern-frontend/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "@radix-ui/react-icons";

import { cn } from "@/util/utils";

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<CheckIcon className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
Checkbox.displayName = CheckboxPrimitive.Root.displayName;

export { Checkbox };
30 changes: 29 additions & 1 deletion skyvern-frontend/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Navigate, createBrowserRouter } from "react-router-dom";
import { Navigate, Outlet, createBrowserRouter } from "react-router-dom";
import { RootLayout } from "./routes/root/RootLayout";
import { TasksPageLayout } from "./routes/tasks/TasksPageLayout";
import { TaskTemplates } from "./routes/tasks/create/TaskTemplates";
Expand All @@ -13,6 +13,10 @@ import { TaskRecording } from "./routes/tasks/detail/TaskRecording";
import { TaskParameters } from "./routes/tasks/detail/TaskParameters";
import { StepArtifactsLayout } from "./routes/tasks/detail/StepArtifactsLayout";
import { CreateNewTaskFromPrompt } from "./routes/tasks/create/CreateNewTaskFromPrompt";
import { WorkflowsPageLayout } from "./routes/workflows/WorkflowsPageLayout";
import { Workflows } from "./routes/workflows/Workflows";
import { WorkflowPage } from "./routes/workflows/WorkflowPage";
import { WorkflowRunParameters } from "./routes/workflows/WorkflowRunParameters";

const router = createBrowserRouter([
{
Expand Down Expand Up @@ -77,6 +81,30 @@ const router = createBrowserRouter([
},
],
},
{
path: "workflows",
element: <WorkflowsPageLayout />,
children: [
{
index: true,
element: <Workflows />,
},
{
path: ":workflowPermanentId",
element: <Outlet />,
children: [
{
index: true,
element: <WorkflowPage />,
},
{
path: "run",
element: <WorkflowRunParameters />,
},
],
},
],
},
{
path: "settings",
element: <SettingsPageLayout />,
Expand Down
16 changes: 14 additions & 2 deletions skyvern-frontend/src/routes/root/SideNav.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { cn } from "@/util/utils";
import {
GearIcon,
LightningBoltIcon,
ListBulletIcon,
PlusCircledIcon,
} from "@radix-ui/react-icons";
Expand All @@ -18,7 +19,7 @@ function SideNav() {
}}
>
<PlusCircledIcon className="mr-4 w-6 h-6" />
<span className="text-lg">New Task</span>
<span className="text-lg">Create</span>
</NavLink>
<NavLink
to="tasks"
Expand All @@ -29,7 +30,18 @@ function SideNav() {
}}
>
<ListBulletIcon className="mr-4 w-6 h-6" />
<span className="text-lg">Task History</span>
<span className="text-lg">Tasks</span>
</NavLink>
<NavLink
to="workflows"
className={({ isActive }) => {
return cn("flex items-center px-5 py-3 hover:bg-muted rounded-2xl", {
"bg-muted": isActive,
});
}}
>
<LightningBoltIcon className="mr-4 w-6 h-6" />
<span className="text-lg">Workflows (Beta)</span>
</NavLink>
<NavLink
to="settings"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function CreateNewTaskFormPage() {
const credentialGetter = useCredentialGetter();

const { data, isFetching } = useQuery({
queryKey: ["workflows", template],
queryKey: ["savedTask", template],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
Expand Down
2 changes: 1 addition & 1 deletion skyvern-frontend/src/routes/tasks/create/SavedTasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function SavedTasks() {
const navigate = useNavigate();

const { data } = useQuery<Array<WorkflowApiResponse>>({
queryKey: ["workflows"],
queryKey: ["savedTasks"],
queryFn: async () => {
const client = await getClient(credentialGetter);
return client
Expand Down
Loading

0 comments on commit b74f0d1

Please sign in to comment.