Add Browsers (browser management) to cloud app (#3549)
This commit is contained in:
@@ -7,14 +7,15 @@ import {
|
||||
} from "./ui/tooltip";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
function HelpTooltip({ content }: Props) {
|
||||
function HelpTooltip({ content, className }: Props) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={300}>
|
||||
<TooltipTrigger asChild>
|
||||
<TooltipTrigger className={className} asChild>
|
||||
<QuestionMarkCircledIcon className="size-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-[250px]">{content}</TooltipContent>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Navigate, Outlet, createBrowserRouter } from "react-router-dom";
|
||||
import { BrowserSession } from "@/routes/browserSession/BrowserSession";
|
||||
import { BrowserSession } from "@/routes/browserSessions/BrowserSession";
|
||||
import { PageLayout } from "./components/PageLayout";
|
||||
import { DiscoverPage } from "./routes/discover/DiscoverPage";
|
||||
import { HistoryPage } from "./routes/history/HistoryPage";
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { BrowserStream } from "@/components/BrowserStream";
|
||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||
import { SwitchBar } from "@/components/SwitchBar";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { BrowserSessionVideo } from "./BrowserSessionVideo";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
type TabName = "stream" | "videos";
|
||||
|
||||
function BrowserSession() {
|
||||
const { browserSessionId } = useParams();
|
||||
const [hasBrowserSession, setHasBrowserSession] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<TabName>("stream");
|
||||
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["browserSession", browserSessionId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
||||
try {
|
||||
const response = await client.get(
|
||||
`/browser_sessions/${browserSessionId}`,
|
||||
);
|
||||
setHasBrowserSession(true);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
setHasBrowserSession(false);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
{/* we need nice artwork here */}
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasBrowserSession) {
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
{/* we need nice artwork here */}
|
||||
No browser session found.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full flex-col items-start justify-start gap-2">
|
||||
<div className="flex w-full flex-shrink-0 flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="flex flex-row items-center justify-start gap-2">
|
||||
<LogoMinimized />
|
||||
<div className="text-xl">browser session</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<SwitchBar
|
||||
className="mb-2 border-none"
|
||||
onChange={(value) => setActiveTab(value as TabName)}
|
||||
value={activeTab}
|
||||
options={[
|
||||
{
|
||||
label: "Stream",
|
||||
value: "stream",
|
||||
helpText: "The live stream of the browser session (if active).",
|
||||
},
|
||||
{
|
||||
label: "Recordings",
|
||||
value: "videos",
|
||||
helpText: "All recordings of this browser session.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="relative min-h-0 w-full flex-1 rounded-lg border p-4">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 top-0 z-10 flex h-full w-full items-center justify-center",
|
||||
{
|
||||
hidden: activeTab !== "stream",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<BrowserStream
|
||||
browserSessionId={browserSessionId}
|
||||
interactive={false}
|
||||
showControlButtons={true}
|
||||
/>
|
||||
</div>
|
||||
{activeTab === "videos" && <BrowserSessionVideo />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BrowserSession };
|
||||
207
skyvern-frontend/src/routes/browserSessions/BrowserSession.tsx
Normal file
207
skyvern-frontend/src/routes/browserSessions/BrowserSession.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import { ReloadIcon, StopIcon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { BrowserStream } from "@/components/BrowserStream";
|
||||
import { LogoMinimized } from "@/components/LogoMinimized";
|
||||
import { SwitchBar } from "@/components/SwitchBar";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { useCloseBrowserSessionMutation } from "@/routes/browserSessions/hooks/useCloseBrowserSessionMutation";
|
||||
import { CopyText } from "@/routes/workflows/editor/Workspace";
|
||||
import { type BrowserSession as BrowserSessionType } from "@/routes/workflows/types/browserSessionTypes";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
import { BrowserSessionVideo } from "./BrowserSessionVideo";
|
||||
|
||||
type TabName = "stream" | "videos";
|
||||
|
||||
function BrowserSession() {
|
||||
const { browserSessionId } = useParams();
|
||||
const [hasBrowserSession, setHasBrowserSession] = useState(false);
|
||||
const [browserSession, setBrowserSession] =
|
||||
useState<BrowserSessionType | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<TabName>("stream");
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["browserSession", browserSessionId],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
||||
try {
|
||||
const response = await client.get<BrowserSessionType>(
|
||||
`/browser_sessions/${browserSessionId}`,
|
||||
);
|
||||
setHasBrowserSession(true);
|
||||
setBrowserSession(response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
setHasBrowserSession(false);
|
||||
setBrowserSession(null);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const closeBrowserSessionMutation = useCloseBrowserSessionMutation({
|
||||
browserSessionId,
|
||||
onSuccess: () => {
|
||||
setIsDialogOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
{/* we need nice artwork here */}
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasBrowserSession) {
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
{/* we need nice artwork here */}
|
||||
No browser session found.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full flex-col items-start justify-start gap-2">
|
||||
<div className="flex w-full flex-shrink-0 flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="flex w-full flex-row items-center justify-start gap-2">
|
||||
<LogoMinimized />
|
||||
<div className="text-xl">Browser Session</div>
|
||||
{browserSession && (
|
||||
<div className="ml-auto flex flex-col items-end justify-end overflow-hidden">
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="max-w-[20rem] truncate font-mono text-xs opacity-75">
|
||||
{browserSession.browser_session_id}
|
||||
</div>
|
||||
<CopyText
|
||||
className="opacity-75 hover:opacity-100"
|
||||
text={browserSession.browser_session_id}
|
||||
/>
|
||||
</div>
|
||||
{browserSession.browser_address && (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="max-w-[20rem] truncate font-mono text-xs opacity-75">
|
||||
{browserSession.browser_address}
|
||||
</div>
|
||||
<CopyText
|
||||
className="opacity-75 hover:opacity-100"
|
||||
text={browserSession.browser_address}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex w-full items-center justify-start gap-2">
|
||||
<SwitchBar
|
||||
className="border-none"
|
||||
onChange={(value) => setActiveTab(value as TabName)}
|
||||
value={activeTab}
|
||||
options={[
|
||||
{
|
||||
label: "Stream",
|
||||
value: "stream",
|
||||
helpText: "The live stream of the browser session (if active).",
|
||||
},
|
||||
{
|
||||
label: "Recordings",
|
||||
value: "videos",
|
||||
helpText: "All recordings of this browser session.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{browserSessionId && (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="ml-auto" variant="secondary">
|
||||
<StopIcon className="mr-2 h-4 w-4" />
|
||||
Stop
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to stop (shut down) this browser
|
||||
session?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Back</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
closeBrowserSessionMutation.mutate();
|
||||
}}
|
||||
disabled={closeBrowserSessionMutation.isPending}
|
||||
>
|
||||
{closeBrowserSessionMutation.isPending && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}
|
||||
Stop Browser Session
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="relative min-h-0 w-full flex-1 rounded-lg border p-4">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute left-0 top-0 z-10 flex h-full w-full items-center justify-center",
|
||||
{
|
||||
hidden: activeTab !== "stream",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<BrowserStream
|
||||
browserSessionId={browserSessionId}
|
||||
interactive={false}
|
||||
showControlButtons={true}
|
||||
/>
|
||||
</div>
|
||||
{activeTab === "videos" && <BrowserSessionVideo />}
|
||||
</div>
|
||||
</div>
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BrowserSession };
|
||||
324
skyvern-frontend/src/routes/browserSessions/BrowserSessions.tsx
Normal file
324
skyvern-frontend/src/routes/browserSessions/BrowserSessions.tsx
Normal file
@@ -0,0 +1,324 @@
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
GlobeIcon,
|
||||
PlusIcon,
|
||||
ReloadIcon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useBrowserSessionsQuery } from "@/routes/browserSessions/hooks/useBrowserSessionsQuery";
|
||||
import { useCreateBrowserSessionMutation } from "@/routes/browserSessions/hooks/useCreateBrowserSessionMutation";
|
||||
import { type BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
|
||||
import { CopyText } from "@/routes/workflows/editor/Workspace";
|
||||
import { basicTimeFormat } from "@/util/timeFormat";
|
||||
import { cn, formatMs } from "@/util/utils";
|
||||
|
||||
function toDate(
|
||||
time: string,
|
||||
defaultDate: Date | null = new Date(0),
|
||||
): Date | null {
|
||||
time = time.replace(/\.(\d{3})\d*/, ".$1");
|
||||
|
||||
if (!time.endsWith("Z")) {
|
||||
time += "Z";
|
||||
}
|
||||
|
||||
const date = new Date(time);
|
||||
|
||||
if (isNaN(date.getTime())) {
|
||||
return defaultDate;
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
function sessionIsOpen(browserSession: BrowserSession): boolean {
|
||||
return (
|
||||
browserSession.completed_at === null && browserSession.started_at !== null
|
||||
);
|
||||
}
|
||||
|
||||
function BrowserSessions() {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const page = searchParams.get("page") ? Number(searchParams.get("page")) : 1;
|
||||
const itemsPerPage = searchParams.get("page_size")
|
||||
? Number(searchParams.get("page_size"))
|
||||
: 10;
|
||||
|
||||
function setParamPatch(patch: Record<string, string>) {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
Object.entries(patch).forEach(([k, v]) => params.set(k, v));
|
||||
setSearchParams(params, { replace: true });
|
||||
}
|
||||
|
||||
function handlePreviousPage() {
|
||||
if (page === 1) return;
|
||||
setParamPatch({ page: String(page - 1) });
|
||||
}
|
||||
|
||||
function handleNextPage() {
|
||||
if (isNextDisabled) return;
|
||||
setParamPatch({ page: String(page + 1) });
|
||||
}
|
||||
|
||||
const createBrowserSessionMutation = useCreateBrowserSessionMutation();
|
||||
|
||||
const { data: browserSessions = [], isLoading } = useBrowserSessionsQuery(
|
||||
page,
|
||||
itemsPerPage,
|
||||
);
|
||||
|
||||
const { data: nextPageBrowserSessions } = useBrowserSessionsQuery(
|
||||
page + 1,
|
||||
itemsPerPage,
|
||||
);
|
||||
|
||||
const isNextDisabled =
|
||||
isLoading ||
|
||||
!nextPageBrowserSessions ||
|
||||
nextPageBrowserSessions.length === 0;
|
||||
|
||||
function handleRowClick(browserSessionId: string) {
|
||||
window.open(
|
||||
window.location.origin + `/browser-session/${browserSessionId}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="px-8">
|
||||
{/* header */}
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<GlobeIcon className="size-6" />
|
||||
<h1 className="text-2xl">Browsers</h1>
|
||||
</div>
|
||||
<p className="text-slate-300">
|
||||
Create your own live browsers to interact with websites, or run
|
||||
workflows in.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* browsers */}
|
||||
<div className="mt-6 space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<div className="flex gap-4">
|
||||
<Button
|
||||
disabled={createBrowserSessionMutation.isPending}
|
||||
onClick={() => {
|
||||
createBrowserSessionMutation.mutate({
|
||||
proxyLocation: null,
|
||||
timeout: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{createBrowserSessionMutation.isPending ? (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<PlusIcon className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border">
|
||||
<Table className="w-full table-fixed">
|
||||
<TableHeader className="rounded-t-lg bg-slate-elevation2">
|
||||
<TableRow>
|
||||
<TableHead className="w-1/4 truncate rounded-tl-lg text-slate-400">
|
||||
ID
|
||||
</TableHead>
|
||||
<TableHead className="w-1/12 truncate text-slate-400">
|
||||
Open
|
||||
</TableHead>
|
||||
<TableHead className="w-1/6 truncate text-slate-400">
|
||||
<span className="mr-2">Occupied</span>
|
||||
<HelpTooltip
|
||||
className="inline"
|
||||
content="Browser is busy running a task or workflow"
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-1/6 truncate text-slate-400">
|
||||
Started
|
||||
</TableHead>
|
||||
<TableHead className="w-1/6 truncate text-slate-400">
|
||||
Timeout
|
||||
</TableHead>
|
||||
<TableHead className="w-1/2 truncate text-slate-400">
|
||||
CDP Url
|
||||
</TableHead>
|
||||
<TableHead className="w-[2.5rem] rounded-tr-lg"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>Loading...</TableCell>
|
||||
</TableRow>
|
||||
) : browserSessions?.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>No browser sessions found</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
browserSessions?.map((browserSession) => {
|
||||
const isOpen = sessionIsOpen(browserSession);
|
||||
const startedAtDate = toDate(
|
||||
browserSession.started_at ?? "",
|
||||
null,
|
||||
);
|
||||
const ago = startedAtDate ? (
|
||||
formatMs(Date.now() - startedAtDate.getTime()).ago
|
||||
) : (
|
||||
<span className="opacity-50">never</span>
|
||||
);
|
||||
const cdpUrl =
|
||||
browserSession.browser_address ??
|
||||
"wss://session-staging.skyvern.com/pbs_442960015326262218/devtools/browser/f01f27e1-182b-4a33-9017-4c1146d3eb3e";
|
||||
|
||||
return (
|
||||
<TableRow key={browserSession.browser_session_id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1 truncate opacity-75">
|
||||
{browserSession.browser_session_id}
|
||||
</div>
|
||||
<CopyText
|
||||
className="opacity-75 hover:opacity-100"
|
||||
text={browserSession.browser_session_id}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isOpen ? (
|
||||
"Yes"
|
||||
) : (
|
||||
<span className="opacity-50">No</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{browserSession.runnable_id ? (
|
||||
"Yes"
|
||||
) : (
|
||||
<span className="opacity-50">No</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
title={
|
||||
browserSession.started_at
|
||||
? basicTimeFormat(browserSession.started_at)
|
||||
: "not started"
|
||||
}
|
||||
>
|
||||
{ago}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{browserSession.timeout
|
||||
? `${browserSession.timeout}m`
|
||||
: "-"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center">
|
||||
<div className="flex-1 truncate opacity-75">
|
||||
{cdpUrl}
|
||||
</div>
|
||||
{cdpUrl !== "-" ? (
|
||||
<CopyText
|
||||
className="opacity-75 hover:opacity-100"
|
||||
text={cdpUrl}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
handleRowClick(browserSession.browser_session_id);
|
||||
}}
|
||||
>
|
||||
<ExternalLinkIcon className="inline size-4" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="relative px-3 py-3">
|
||||
<div className="absolute left-3 top-1/2 flex -translate-y-1/2 items-center gap-2 text-sm">
|
||||
<span className="text-slate-400">Items per page</span>
|
||||
<select
|
||||
className="h-9 rounded-md border border-slate-300 bg-background"
|
||||
value={itemsPerPage}
|
||||
onChange={(e) => {
|
||||
const next = Number(e.target.value);
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set("page_size", String(next));
|
||||
params.set("page", "1");
|
||||
setSearchParams(params, { replace: true });
|
||||
}}
|
||||
>
|
||||
<option className="px-3" value={5}>
|
||||
5
|
||||
</option>
|
||||
<option className="px-3" value={10}>
|
||||
10
|
||||
</option>
|
||||
<option className="px-3" value={20}>
|
||||
20
|
||||
</option>
|
||||
<option className="px-3" value={50}>
|
||||
50
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<Pagination className="pt-0">
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={cn({
|
||||
"cursor-not-allowed opacity-50": page === 1,
|
||||
})}
|
||||
onClick={handlePreviousPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationLink>{page}</PaginationLink>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className={cn({
|
||||
"cursor-not-allowed opacity-50": isNextDisabled,
|
||||
})}
|
||||
onClick={handleNextPage}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { BrowserSessions };
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { type BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
|
||||
|
||||
const useBrowserSessionsQuery = (page: number, itemsPerPage: number) => {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
|
||||
return useQuery<BrowserSession[]>({
|
||||
queryKey: ["browser_sessions", page, itemsPerPage],
|
||||
queryFn: async () => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
const params = new URLSearchParams();
|
||||
params.append("page", String(page));
|
||||
params.append("page_size", String(itemsPerPage));
|
||||
return client
|
||||
.get(`/browser_sessions/history`, {
|
||||
params,
|
||||
})
|
||||
.then((response) => response.data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { useBrowserSessionsQuery };
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
|
||||
const useCloseBrowserSessionMutation = ({
|
||||
browserSessionId,
|
||||
onSuccess,
|
||||
}: {
|
||||
browserSessionId?: string;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const closeBrowserSessionMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!browserSessionId) {
|
||||
console.warn("No browserSessionId provided for close mutation");
|
||||
return;
|
||||
}
|
||||
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
|
||||
return client
|
||||
.post(`/browser_sessions/${browserSessionId}/close`)
|
||||
.then((response) => response.data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["browserSessions"],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["browserSession", browserSessionId],
|
||||
});
|
||||
toast({
|
||||
variant: "success",
|
||||
title: "Browser Session Closed",
|
||||
description: "The browser session has been successfully closed.",
|
||||
});
|
||||
onSuccess?.();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error",
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return closeBrowserSessionMutation;
|
||||
};
|
||||
|
||||
export { useCloseBrowserSessionMutation };
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
|
||||
|
||||
function useCreateBrowserSessionMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
proxyLocation = null,
|
||||
timeout = null,
|
||||
}: {
|
||||
proxyLocation: string | null;
|
||||
timeout: number | null;
|
||||
}) => {
|
||||
const client = await getClient(credentialGetter, "sans-api-v1");
|
||||
return client.post<string, { data: BrowserSession }>(
|
||||
"/browser_sessions",
|
||||
{
|
||||
proxy_location: proxyLocation,
|
||||
timeout,
|
||||
},
|
||||
);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["browser_sessions"],
|
||||
});
|
||||
navigate(`/browser-session/${response.data.browser_session_id}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export { useCreateBrowserSessionMutation };
|
||||
@@ -108,7 +108,7 @@ interface Dom {
|
||||
splitLeft: MutableRefObject<HTMLInputElement | null>;
|
||||
}
|
||||
|
||||
function CopyText({ text }: { text: string }) {
|
||||
function CopyText({ className, text }: { className?: string; text: string }) {
|
||||
const [wasCopied, setWasCopied] = useState(false);
|
||||
|
||||
function handleCopy(code: string) {
|
||||
@@ -118,7 +118,15 @@ function CopyText({ text }: { text: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button size="icon" variant="link" onClick={() => handleCopy(text)}>
|
||||
<Button
|
||||
className={className}
|
||||
size="icon"
|
||||
variant="link"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopy(text);
|
||||
}}
|
||||
>
|
||||
{wasCopied ? <CheckIcon /> : <CopyIcon />}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
interface BrowserSession {
|
||||
browser_address: string | null;
|
||||
browser_session_id: string;
|
||||
completed_at: string | null;
|
||||
recordings: Recording[];
|
||||
runnable_id: string | null;
|
||||
runnable_type: string | null;
|
||||
started_at: string | null;
|
||||
timeout: number | null;
|
||||
vnc_streaming_supported: boolean;
|
||||
}
|
||||
|
||||
interface Recording {
|
||||
url: string;
|
||||
checksum: string;
|
||||
filename: string;
|
||||
modified_at: string;
|
||||
}
|
||||
|
||||
export { type BrowserSession, type Recording };
|
||||
Reference in New Issue
Block a user