browser sessions v2 - frontend (#4514)
Co-authored-by: Benji Visser <benji@093b.org>
This commit is contained in:
@@ -31,12 +31,7 @@ import {
|
||||
type MessageInExfiltratedEvent,
|
||||
} from "@/store/useRecordingStore";
|
||||
import { useSettingsStore } from "@/store/SettingsStore";
|
||||
import {
|
||||
environment,
|
||||
wssBaseUrl,
|
||||
newWssBaseUrl,
|
||||
getRuntimeApiKey,
|
||||
} from "@/util/env";
|
||||
import { wssBaseUrl, newWssBaseUrl, getRuntimeApiKey } from "@/util/env";
|
||||
import { copyText } from "@/util/copyText";
|
||||
import { cn } from "@/util/utils";
|
||||
|
||||
@@ -223,7 +218,7 @@ function BrowserStream({
|
||||
|
||||
let credentialQueryParam = runtimeApiKey ? `apikey=${runtimeApiKey}` : "";
|
||||
|
||||
if (environment !== "local" && credentialGetter) {
|
||||
if (credentialGetter) {
|
||||
const token = await credentialGetter();
|
||||
credentialQueryParam = token ? `token=Bearer ${token}` : "";
|
||||
}
|
||||
|
||||
@@ -30,9 +30,6 @@ 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);
|
||||
|
||||
@@ -42,22 +39,17 @@ function BrowserSession() {
|
||||
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 response = await client.get<BrowserSessionType>(
|
||||
`/browser_sessions/${browserSessionId}`,
|
||||
);
|
||||
return response.data;
|
||||
},
|
||||
refetchInterval: (query) =>
|
||||
query.state.data?.status === "running" ? 5000 : false,
|
||||
});
|
||||
|
||||
const browserSession = query.data;
|
||||
|
||||
const closeBrowserSessionMutation = useCloseBrowserSessionMutation({
|
||||
browserSessionId,
|
||||
onSuccess: () => {
|
||||
@@ -76,7 +68,7 @@ function BrowserSession() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasBrowserSession) {
|
||||
if (query.isError || !browserSession) {
|
||||
return (
|
||||
<div className="h-screen w-full gap-4 p-6">
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
@@ -96,7 +88,20 @@ function BrowserSession() {
|
||||
<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="flex items-center justify-end gap-2">
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs font-medium ${
|
||||
browserSession.status === "running"
|
||||
? "bg-green-500/20 text-green-500"
|
||||
: browserSession.status === "completed"
|
||||
? "bg-blue-500/20 text-blue-500"
|
||||
: browserSession.status === "failed"
|
||||
? "bg-red-500/20 text-red-500"
|
||||
: "bg-gray-500/20 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{browserSession.status}
|
||||
</span>
|
||||
<div className="max-w-[20rem] truncate font-mono text-xs opacity-75">
|
||||
{browserSession.browser_session_id}
|
||||
</div>
|
||||
@@ -141,7 +146,7 @@ function BrowserSession() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{browserSessionId && (
|
||||
{browserSessionId && browserSession?.status === "running" && (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="ml-auto" variant="secondary">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams } from "react-router-dom";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { artifactApiBaseUrl } from "@/util/env";
|
||||
|
||||
interface Recording {
|
||||
url: string;
|
||||
@@ -11,6 +12,16 @@ interface Recording {
|
||||
modified_at: string;
|
||||
}
|
||||
|
||||
function getRecordingUrl(url: string | null | undefined): string | null {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
if (url.startsWith("file://")) {
|
||||
return `${artifactApiBaseUrl}/artifact/recording?path=${url.slice(7)}`;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function BrowserSessionVideo() {
|
||||
const { browserSessionId } = useParams();
|
||||
const credentialGetter = useCredentialGetter();
|
||||
@@ -31,7 +42,9 @@ function BrowserSessionVideo() {
|
||||
enabled: !!browserSessionId,
|
||||
});
|
||||
|
||||
const recordings = browserSession?.recordings || [];
|
||||
const isSessionRunning = browserSession?.status === "running";
|
||||
// Don't show recordings while session is running - they're incomplete
|
||||
const recordings = isSessionRunning ? [] : browserSession?.recordings || [];
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -59,8 +72,9 @@ function BrowserSessionVideo() {
|
||||
No recordings available
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
Video recordings will appear here when the browser session is active
|
||||
and recording
|
||||
{isSessionRunning
|
||||
? "Recordings will be available after the session completes"
|
||||
: "No recordings were created for this session"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,19 +107,19 @@ function BrowserSessionVideo() {
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{recording.url ? (
|
||||
{getRecordingUrl(recording.url) ? (
|
||||
<div className="w-full">
|
||||
<video
|
||||
controls
|
||||
className="w-full max-w-4xl rounded-lg"
|
||||
src={recording.url}
|
||||
src={getRecordingUrl(recording.url)!}
|
||||
preload="metadata"
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
<a
|
||||
href={recording.url}
|
||||
href={getRecordingUrl(recording.url)!}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 hover:text-blue-800"
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { getClient } from "@/api/AxiosClient";
|
||||
import { toast } from "@/components/ui/use-toast";
|
||||
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
||||
import { BrowserSession } from "@/routes/workflows/types/browserSessionTypes";
|
||||
import { ProxyLocation } from "@/api/types";
|
||||
@@ -35,6 +36,26 @@ function useCreateBrowserSessionMutation() {
|
||||
});
|
||||
navigate(`/browser-session/${response.data.browser_session_id}`);
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
let errorMessage =
|
||||
"Browser session could not be started. Please try again.";
|
||||
if (error && typeof error === "object") {
|
||||
const axiosError = error as {
|
||||
response?: { data?: { detail?: string } };
|
||||
message?: string;
|
||||
};
|
||||
if (axiosError.response?.data?.detail) {
|
||||
errorMessage = axiosError.response.data.detail;
|
||||
} else if (axiosError.message) {
|
||||
errorMessage = axiosError.message;
|
||||
}
|
||||
}
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Failed to create browser session",
|
||||
description: errorMessage,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ interface BrowserSession {
|
||||
runnable_id: string | null;
|
||||
runnable_type: string | null;
|
||||
started_at: string | null;
|
||||
status: string;
|
||||
timeout: number | null;
|
||||
vnc_streaming_supported: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user