mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2025-09-02 02:30:07 +00:00
New workflow table (#661)
This commit is contained in:
parent
3c81e4269a
commit
dadb3724b7
7 changed files with 257 additions and 14 deletions
7
skyvern-frontend/src/components/BadgeLoading.tsx
Normal file
7
skyvern-frontend/src/components/BadgeLoading.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Skeleton } from "./ui/skeleton";
|
||||
|
||||
function BadgeLoading() {
|
||||
return <Skeleton className="h-7 w-24" />;
|
||||
}
|
||||
|
||||
export { BadgeLoading };
|
|
@ -69,6 +69,8 @@
|
|||
--border: 215.3 25% 26.7%;
|
||||
--input: 215.3 25% 26.7%;
|
||||
--ring: 212.7 26.8% 83.9%;
|
||||
|
||||
--slate-elevation-2: 228 37% 11%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,5 @@
|
|||
import { getClient } from "@/api/AxiosClient";
|
||||
import { WorkflowApiResponse, WorkflowRunApiResponse } from "@/api/types";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import {
|
||||
Pagination,
|
||||
|
@ -21,9 +9,21 @@ import {
|
|||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { cn } from "@/util/utils";
|
||||
import { WorkflowTitle } from "./WorkflowTitle";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
import { cn } from "@/util/utils";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { WorkflowsBetaAlertCard } from "./WorkflowsBetaAlertCard";
|
||||
import { WorkflowTitle } from "./WorkflowTitle";
|
||||
|
||||
function Workflows() {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useWorkflowLastRunQuery } from "../hooks/useWorkflowLastRunQuery";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
|
||||
type Props = {
|
||||
workflowId: string;
|
||||
};
|
||||
|
||||
function LastRunAtTime({ workflowId }: Props) {
|
||||
const { data, isLoading } = useWorkflowLastRunQuery({ workflowId });
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton className="h-full w-full" />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.status === "N/A") {
|
||||
return <span>N/A</span>;
|
||||
}
|
||||
|
||||
return <span>{basicTimeFormat(data.time)}</span>;
|
||||
}
|
||||
|
||||
export { LastRunAtTime };
|
|
@ -0,0 +1,27 @@
|
|||
import { BadgeLoading } from "@/components/BadgeLoading";
|
||||
import { StatusBadge } from "@/components/StatusBadge";
|
||||
import { useWorkflowLastRunQuery } from "../hooks/useWorkflowLastRunQuery";
|
||||
|
||||
type Props = {
|
||||
workflowId: string;
|
||||
};
|
||||
|
||||
function LastRunStatus({ workflowId }: Props) {
|
||||
const { data, isLoading } = useWorkflowLastRunQuery({ workflowId });
|
||||
|
||||
if (isLoading) {
|
||||
return <BadgeLoading />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.status === "N/A") {
|
||||
return <span>N/A</span>;
|
||||
}
|
||||
|
||||
return <StatusBadge status={data.status} />;
|
||||
}
|
||||
|
||||
export { LastRunStatus };
|
|
@ -0,0 +1,140 @@
|
|||
import { getClient } from "@/api/AxiosClient";
|
||||
import { WorkflowApiResponse } from "@/api/types";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LastRunStatus } from "./LastRunStatus";
|
||||
import { LastRunAtTime } from "./LastRunAtTime";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { cn } from "@/util/utils";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function WorkflowsTable() {
|
||||
const [page, setPage] = useState(1);
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: workflows, isLoading } = useQuery<Array<WorkflowApiResponse>>({
|
||||
queryKey: ["workflows", page],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
const params = new URLSearchParams();
|
||||
params.append("page", String(page));
|
||||
return client
|
||||
.get("/workflows", {
|
||||
params,
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
});
|
||||
|
||||
const skeleton = Array.from({ length: 5 }).map((_, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Skeleton className="h-6 w-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-6 w-full" />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Skeleton className="h-6 w-full" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Table>
|
||||
<TableHeader className="bg-slate-elevation2 text-slate-400 [&_tr]:border-b-0">
|
||||
<TableRow className="rounded-lg px-6 [&_th:first-child]:pl-6 [&_th]:py-4">
|
||||
<TableHead className="text-sm text-slate-400">Title</TableHead>
|
||||
<TableHead className="text-sm text-slate-400">
|
||||
Last Run Status
|
||||
</TableHead>
|
||||
<TableHead className="text-sm text-slate-400">
|
||||
Last Run Time
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="h-5"></TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
<TableBody>
|
||||
{isLoading && skeleton}
|
||||
{workflows?.map((workflow) => {
|
||||
return (
|
||||
<TableRow
|
||||
key={workflow.workflow_permanent_id}
|
||||
className="cursor-pointer [&_td:first-child]:pl-6 [&_td:last-child]:pr-6 [&_td]:py-4"
|
||||
onClick={(event) => {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
window.open(
|
||||
window.location.origin +
|
||||
`/workflows/${workflow.workflow_permanent_id}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
return;
|
||||
}
|
||||
navigate(`${workflow.workflow_permanent_id}`);
|
||||
}}
|
||||
>
|
||||
<TableCell>
|
||||
<span className="text-sm leading-5">{workflow.title}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<LastRunStatus workflowId={workflow.workflow_permanent_id} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<LastRunAtTime workflowId={workflow.workflow_permanent_id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={cn({ "cursor-not-allowed": page === 1 })}
|
||||
onClick={() => {
|
||||
setPage((prev) => Math.max(1, prev - 1));
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => {
|
||||
setPage((prev) => prev + 1);
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { WorkflowsTable };
|
|
@ -0,0 +1,40 @@
|
|||
import { getClient } from "@/api/AxiosClient";
|
||||
import { Status, WorkflowRunApiResponse } from "@/api/types";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type Props = {
|
||||
workflowId: string;
|
||||
};
|
||||
|
||||
type LastRunInfo = {
|
||||
status: Status | "N/A";
|
||||
time: string | "N/A";
|
||||
};
|
||||
|
||||
function useWorkflowLastRunQuery({ workflowId }: Props) {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const queryResult = useQuery<LastRunInfo | null>({
|
||||
queryKey: ["lastRunInfo", workflowId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter);
|
||||
const data = (await client
|
||||
.get(`/workflows/${workflowId}/runs?page_size=1`)
|
||||
.then((response) => response.data)) as Array<WorkflowRunApiResponse>;
|
||||
if (data.length === 0) {
|
||||
return {
|
||||
status: "N/A",
|
||||
time: "N/A",
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: data[0]!.status,
|
||||
time: data[0]!.created_at,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
return queryResult;
|
||||
}
|
||||
|
||||
export { useWorkflowLastRunQuery };
|
Loading…
Add table
Reference in a new issue