diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 24638adf6f..51a881ccdb 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -32,6 +32,7 @@ jobs: renku-notebooks: ${{ steps.deploy-comment.outputs.renku-notebooks}} renku-data-services: ${{ steps.deploy-comment.outputs.renku-data-services}} amalthea: ${{ steps.deploy-comment.outputs.amalthea}} + amalthea-sessions: ${{ steps.deploy-comment.outputs.amalthea-sessions}} test-enabled: ${{ steps.deploy-comment.outputs.test-enabled}} extra-values: ${{ steps.deploy-comment.outputs.extra-values}} steps: @@ -90,6 +91,7 @@ jobs: renku_notebooks: "${{ needs.check-deploy.outputs.renku-notebooks }}" renku_data_services: "${{ needs.check-deploy.outputs.renku-data-services }}" amalthea: "${{ needs.check-deploy.outputs.amalthea }}" + amalthea_sessions: "${{ needs.check-deploy.outputs.amalthea-sessions }}" extra_values: "${{ needs.check-deploy.outputs.extra-values }}" selenium-acceptance-tests: diff --git a/client/src/components/Logs.tsx b/client/src/components/Logs.tsx index 0e75078aeb..01c2a97ef4 100644 --- a/client/src/components/Logs.tsx +++ b/client/src/components/Logs.tsx @@ -33,7 +33,9 @@ import { displaySlice } from "../features/display"; import { NotebooksHelper } from "../notebooks"; import { LOG_ERROR_KEY } from "../notebooks/Notebooks.state"; import { NotebookAnnotations } from "../notebooks/components/session.types"; -import useGetSessionLogs from "../utils/customHooks/UseGetSessionLogs"; +import useGetSessionLogs, { + useGetSessionLogsV2, +} from "../utils/customHooks/UseGetSessionLogs"; import useAppDispatch from "../utils/customHooks/useAppDispatch.hook"; import useAppSelector from "../utils/customHooks/useAppSelector.hook"; import { @@ -346,6 +348,39 @@ const EnvironmentLogs = ({ name, annotations }: EnvironmentLogsProps) => { ); }; +/** + * Sessions logs container integrating state and actions V2 + * + * @param {string} name - server name + */ +interface EnvironmentLogsPropsV2 { + name: string; +} +export const EnvironmentLogsV2 = ({ name }: EnvironmentLogsPropsV2) => { + const displayModal = useAppSelector( + ({ display }) => display.modals.sessionLogs + ); + const { logs, fetchLogs } = useGetSessionLogsV2( + displayModal.targetServer, + displayModal.show + ); + const dispatch = useAppDispatch(); + const toggleLogs = function (target: string) { + dispatch( + displaySlice.actions.toggleSessionLogsModal({ targetServer: target }) + ); + }; + + return ( + + ); +}; + /** * Simple environment logs container * @@ -356,7 +391,7 @@ const EnvironmentLogs = ({ name, annotations }: EnvironmentLogsProps) => { * @param {object} annotations - list of annotations */ interface EnvironmentLogsPresentProps { - annotations: Record; + annotations?: Record; fetchLogs: IFetchableLogs["fetchLogs"]; logs?: ILogs; name: string; @@ -371,12 +406,12 @@ const EnvironmentLogsPresent = ({ }: EnvironmentLogsPresentProps) => { if (!logs?.show || logs?.show !== name || !logs) return null; - const cleanAnnotations = NotebooksHelper.cleanAnnotations( - annotations - ) as NotebookAnnotations; + const cleanAnnotations = + annotations && + (NotebooksHelper.cleanAnnotations(annotations) as NotebookAnnotations); - const modalTitle = !cleanAnnotations.renkuVersion && ( -
+ const modalTitle = cleanAnnotations && !cleanAnnotations.renkuVersion && ( +
{cleanAnnotations["namespace"]}/{cleanAnnotations["projectName"]} [ {cleanAnnotations["branch"]}@ diff --git a/client/src/features/admin/SessionEnvironmentAdvanceFields.tsx b/client/src/features/admin/SessionEnvironmentAdvanceFields.tsx deleted file mode 100644 index eba3e1a771..0000000000 --- a/client/src/features/admin/SessionEnvironmentAdvanceFields.tsx +++ /dev/null @@ -1,62 +0,0 @@ -/*! - * Copyright 2024 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import cx from "classnames"; -import { useCallback, useState } from "react"; -import { Control, FieldErrors } from "react-hook-form"; -import { Collapse } from "reactstrap"; -import ChevronFlippedIcon from "../../components/icons/ChevronFlippedIcon"; -import { AdvanceSettingsFields } from "../sessionsV2/components/SessionForm/AdvanceSettingsFields"; -import { SessionEnvironmentForm } from "./SessionEnvironmentFormContent"; - -interface SessionEnvironmentAdvanceFieldsProps { - control: Control; - errors: FieldErrors; -} - -export default function SessionEnvironmentAdvanceFields({ - control, - errors, -}: SessionEnvironmentAdvanceFieldsProps) { - const [isAdvanceSettingOpen, setIsAdvanceSettingsOpen] = useState(false); - const toggleIsOpen = useCallback( - () => - setIsAdvanceSettingsOpen((isAdvanceSettingOpen) => !isAdvanceSettingOpen), - [] - ); - return ( - <> -
- - Advance settings - -
- -
- - control={control} - errors={errors} - /> -
-
- - ); -} diff --git a/client/src/features/admin/SessionEnvironmentsSection.tsx b/client/src/features/admin/SessionEnvironmentsSection.tsx index 26fe808ab9..defe2f0e3e 100644 --- a/client/src/features/admin/SessionEnvironmentsSection.tsx +++ b/client/src/features/admin/SessionEnvironmentsSection.tsx @@ -40,7 +40,6 @@ import AddSessionEnvironmentButton from "./AddSessionEnvironmentButton"; import DeleteSessionEnvironmentButton from "./DeleteSessionEnvironmentButton"; import UpdateSessionEnvironmentButton from "./UpdateSessionEnvironmentButton"; import { useGetSessionEnvironmentsQuery } from "./adminSessions.api"; -import { safeStringify } from "../sessionsV2/session.utils"; export default function SessionEnvironmentsSection() { return ( diff --git a/client/src/features/dashboardV2/DashboardV2Sessions.tsx b/client/src/features/dashboardV2/DashboardV2Sessions.tsx index b95eec252b..2d9a4dac75 100644 --- a/client/src/features/dashboardV2/DashboardV2Sessions.tsx +++ b/client/src/features/dashboardV2/DashboardV2Sessions.tsx @@ -1,84 +1,84 @@ -import { skipToken } from "@reduxjs/toolkit/query"; +import { FetchBaseQueryError, skipToken } from "@reduxjs/toolkit/query"; import cx from "classnames"; -import { useMemo } from "react"; import { Link, generatePath } from "react-router-dom-v5-compat"; import { Col, ListGroup, Row } from "reactstrap"; import { Loader } from "../../components/Loader"; -import { EnvironmentLogs } from "../../components/Logs"; +import { EnvironmentLogsV2 } from "../../components/Logs"; import { RtkErrorAlert } from "../../components/errors/RtkErrorAlert"; -import { NotebooksHelper } from "../../notebooks"; -import { NotebookAnnotations } from "../../notebooks/components/session.types"; +import { useGetSessionsQuery as useGetSessionsQueryV2 } from "../../features/sessionsV2/sessionsV2.api"; +import "../../notebooks/Notebooks.css"; import { ABSOLUTE_ROUTES } from "../../routing/routes.constants"; import useAppSelector from "../../utils/customHooks/useAppSelector.hook"; import { useGetProjectsByProjectIdQuery } from "../projectsV2/api/projectV2.enhanced-api"; -import { useGetSessionsQuery } from "../session/sessions.api"; -import { Session } from "../session/sessions.types"; -import { filterSessionsWithCleanedAnnotations } from "../session/sessions.utils"; import ActiveSessionButton from "../sessionsV2/components/SessionButton/ActiveSessionButton"; import { SessionStatusV2Description, SessionStatusV2Label, } from "../sessionsV2/components/SessionStatus/SessionStatus"; - -// Required for logs formatting -import "../../notebooks/Notebooks.css"; +import { SessionList, SessionV2 } from "../sessionsV2/sessionsV2.types"; +import { SerializedError } from "@reduxjs/toolkit"; export default function DashboardV2Sessions() { - const { data: sessions, error, isLoading } = useGetSessionsQuery(); + const { data: sessions, error, isLoading } = useGetSessionsQueryV2(); - const v2Sessions = useMemo( - () => - sessions != null - ? filterSessionsWithCleanedAnnotations( - sessions, - ({ annotations }) => annotations["renkuVersion"] === "2.0" - ) - : {}, - [sessions] - ); + if (isLoading) { + return ; + } - const noSessions = isLoading ? ( -
- -

Retrieving sessions...

-
- ) : error ? ( -
-

Cannot show sessions.

- -
- ) : !sessions || - (Object.keys(sessions).length == 0 && - Object.keys(v2Sessions).length == 0) ? ( -
No running sessions.
- ) : null; + if (error) { + return ; + } - if (noSessions) return
{noSessions}
; + if (!sessions || sessions.length === 0) { + return ; + } - return ( - - {Object.entries(v2Sessions).map(([key, session]) => ( - - ))} - - ); + return ; } +const LoadingState = () => ( +
+ +

Retrieving sessions...

+
+); + +const ErrorState = ({ + error, +}: { + error: FetchBaseQueryError | SerializedError | undefined; +}) => ( +
+

Cannot show sessions.

+ +
+); + +const NoSessionsState = () =>
No running sessions.
; + +const SessionDashboardList = ({ + sessions, +}: { + sessions: SessionList | undefined; +}) => ( + + {sessions?.map((session) => ( + + ))} + +); + interface DashboardSessionProps { - session: Session; + session: SessionV2; } function DashboardSession({ session }: DashboardSessionProps) { const displayModal = useAppSelector( ({ display }) => display.modals.sessionLogs ); - const { image } = session; - const annotations = NotebooksHelper.cleanAnnotations( - session.annotations - ) as NotebookAnnotations; - const projectId = annotations.projectId; + const { image, project_id: projectId } = session; const { data: project } = useGetProjectsByProjectIdQuery( - projectId ? { projectId: projectId } : skipToken + projectId ? { projectId } : skipToken ); const projectUrl = project @@ -147,10 +147,7 @@ function DashboardSession({ session }: DashboardSessionProps) { - + ); } diff --git a/client/src/features/session/components/SessionButton.tsx b/client/src/features/session/components/SessionButton.tsx index 8d96b654f2..1e82db90aa 100644 --- a/client/src/features/session/components/SessionButton.tsx +++ b/client/src/features/session/components/SessionButton.tsx @@ -755,7 +755,7 @@ function ModifySessionModalContent({ {message}

- Current resources: + Current resources: (); const locationFilePath = location.state?.filePath; @@ -57,9 +58,9 @@ export default function SessionHibernated({ session }: SessionHibernatedProps) { const [isResuming, setIsResuming] = useState(false); const onResumeSession = useCallback(() => { - patchSession({ sessionName: session.name, state: "running" }); + patchSession({ sessionName: sessionName, state: "running" }); setIsResuming(true); - }, [patchSession, session.name]); + }, [patchSession, sessionName]); const { notifications } = useContext(AppContext); diff --git a/client/src/features/session/components/ShowSession.tsx b/client/src/features/session/components/ShowSession.tsx index 5c1febd503..9ea644eadd 100644 --- a/client/src/features/session/components/ShowSession.tsx +++ b/client/src/features/session/components/ShowSession.tsx @@ -239,7 +239,7 @@ function ShowSessionFullscreen({ sessionName }: ShowSessionFullscreenProps) { !isLoading && thisSession == null ? ( ) : thisSession?.status.state === "hibernated" ? ( - + ) : thisSession != null ? ( <> {!isTheSessionReady && ( diff --git a/client/src/features/session/components/StartSessionProgressBar.tsx b/client/src/features/session/components/StartSessionProgressBar.tsx index 67d276c647..54f4cd2ba6 100644 --- a/client/src/features/session/components/StartSessionProgressBar.tsx +++ b/client/src/features/session/components/StartSessionProgressBar.tsx @@ -25,6 +25,8 @@ import ProgressStepsIndicator, { } from "../../../components/progress/ProgressSteps"; import cx from "classnames"; import { Button } from "reactstrap"; +import { SessionV2 } from "../../sessionsV2/sessionsV2.types"; +import { Loader } from "../../../components/Loader"; interface StartSessionProgressBarProps { includeStepInTitle?: boolean; @@ -59,6 +61,43 @@ export default function StartSessionProgressBar({ ); } +interface StartSessionProgressBarV2Props { + includeStepInTitle?: boolean; + session?: SessionV2; + toggleLogs: () => void; +} +export function StartSessionProgressBarV2({ + includeStepInTitle, + session, + toggleLogs, +}: StartSessionProgressBarV2Props) { + const statusData = session?.status; + const title = "Starting Session"; + const logButton = ( + + ); + + const readyNumContainers = statusData?.ready_containers || 0; + const totalNumContainers = statusData?.total_containers || 1; + return ( +

+
+

+ {includeStepInTitle ? `Step 2 of 2: ${title}` : title} +

+

Starting the containers for your session

+
+ +
{`${readyNumContainers} of ${totalNumContainers} containers ready`}
+
+
+
{logButton}
+
+ ); +} + function getStatusData( status: Pick | undefined ): StepsProgressBar[] { diff --git a/client/src/features/session/useWaitForSessionStatus.hook.ts b/client/src/features/session/useWaitForSessionStatus.hook.ts index 2844e856af..8470e57134 100644 --- a/client/src/features/session/useWaitForSessionStatus.hook.ts +++ b/client/src/features/session/useWaitForSessionStatus.hook.ts @@ -18,6 +18,7 @@ import { useEffect, useMemo, useState } from "react"; import { useGetSessionsQuery } from "./sessions.api"; +import { useGetSessionsQuery as useGetSessionsQueryV2 } from "../sessionsV2/sessionsV2.api"; import { SessionStatusState } from "./sessions.types"; const DEFAULT_POLLING_INTERVAL_MS = 5_000; @@ -68,3 +69,43 @@ export default function useWaitForSessionStatus({ return { isWaiting, session }; } + +export function useWaitForSessionStatusV2({ + desiredStatus, + pollingInterval = DEFAULT_POLLING_INTERVAL_MS, + sessionName, + skip, +}: UseWaitForSessionStatusArgs) { + const [isWaiting, setIsWaiting] = useState(false); + + const result = useGetSessionsQueryV2(undefined, { + pollingInterval, + skip: skip || !isWaiting, + }); + const session = useMemo(() => { + if (result.data == null) { + return undefined; + } + return Object.values(result.data).find(({ name }) => name === sessionName); + }, [result.data, sessionName]); + + useEffect(() => { + if (skip) { + setIsWaiting(false); + } + }, [skip]); + + useEffect(() => { + if (skip) { + return; + } + const desiredStatuses = + typeof desiredStatus === "string" ? [desiredStatus] : desiredStatus; + const isWaiting = + (session != null && !desiredStatuses.includes(session.status.state)) || + (session == null && !desiredStatuses.includes("stopping")); + setIsWaiting(isWaiting); + }, [desiredStatus, session, skip]); + + return { isWaiting, session }; +} diff --git a/client/src/features/sessionsV2/LazyShowSessionPage.tsx b/client/src/features/sessionsV2/LazyShowSessionPage.tsx index d367ce793e..7cc4b476cd 100644 --- a/client/src/features/sessionsV2/LazyShowSessionPage.tsx +++ b/client/src/features/sessionsV2/LazyShowSessionPage.tsx @@ -20,7 +20,7 @@ import { Suspense, lazy } from "react"; import PageLoader from "../../components/PageLoader"; -const ShowSessionPage = lazy(() => import("./ShowSessionPage")); +const ShowSessionPage = lazy(() => import("./SessionShowPage/ShowSessionPage")); export default function LazyShowSessionPage() { return ( diff --git a/client/src/features/sessionsV2/PauseOrDeleteSessionModal.tsx b/client/src/features/sessionsV2/PauseOrDeleteSessionModal.tsx index 139266224c..c79d623583 100644 --- a/client/src/features/sessionsV2/PauseOrDeleteSessionModal.tsx +++ b/client/src/features/sessionsV2/PauseOrDeleteSessionModal.tsx @@ -19,7 +19,7 @@ import { SerializedError } from "@reduxjs/toolkit"; import { FetchBaseQueryError } from "@reduxjs/toolkit/query"; import cx from "classnames"; -import { Duration } from "luxon"; +import { DateTime } from "luxon"; import { useCallback, useContext, useEffect, useState } from "react"; import { generatePath, @@ -31,28 +31,25 @@ import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap"; import { InfoAlert } from "../../components/Alert"; import { Loader } from "../../components/Loader"; import { User } from "../../model/renkuModels.types"; -import { NotebooksHelper } from "../../notebooks"; -import { NotebookAnnotations } from "../../notebooks/components/session.types"; import { NOTIFICATION_TOPICS } from "../../notifications/Notifications.constants"; import { NotificationsManager } from "../../notifications/notifications.types"; import { ABSOLUTE_ROUTES } from "../../routing/routes.constants"; import AppContext from "../../utils/context/appContext"; import useLegacySelector from "../../utils/customHooks/useLegacySelector.hook"; -import { toHumanDuration } from "../../utils/helpers/DurationUtils"; -import UnsavedWorkWarning from "../session/components/UnsavedWorkWarning"; +import { toHumanRelativeDuration } from "../../utils/helpers/DurationUtils"; +import { useWaitForSessionStatusV2 } from "../session/useWaitForSessionStatus.hook"; import { usePatchSessionMutation, useStopSessionMutation, -} from "../session/sessions.api"; -import { Session } from "../session/sessions.types"; -import useWaitForSessionStatus from "../session/useWaitForSessionStatus.hook"; +} from "../sessionsV2/sessionsV2.api"; import styles from "../session/components/SessionModals.module.scss"; +import { SessionV2 } from "./sessionsV2.types"; interface PauseOrDeleteSessionModalProps { action?: "pause" | "delete"; isOpen: boolean; - session: Session | undefined; + session: SessionV2 | undefined; sessionName: string; toggleAction: () => void; toggleModal: () => void; @@ -115,11 +112,11 @@ function AnonymousDeleteSessionModal({ const [isStopping, setIsStopping] = useState(false); const onStopSession = useCallback(async () => { - stopSession({ serverName: sessionName }); + stopSession({ session_id: sessionName }); setIsStopping(true); }, [sessionName, stopSession]); - const { isWaiting } = useWaitForSessionStatus({ + const { isWaiting } = useWaitForSessionStatusV2({ desiredStatus: "stopping", sessionName, skip: !isStopping, @@ -144,13 +141,20 @@ function AnonymousDeleteSessionModal({ return ( - Delete Session + Shut Down Session -

Are you sure you want to delete this session?

+

Are you sure you want to shut down this session?

+ -
); @@ -184,7 +185,7 @@ function LoggedPauseOrDeleteSessionModal({ return ( - {action === "pause" ? "Pause Session" : "Delete Session"} + {action === "pause" ? "Pause Session" : "Shut Down Session"} {action === "pause" ? ( { - patchSession({ sessionName, state: "hibernated" }); + patchSession({ session_id: sessionName, state: "hibernated" }); setIsStopping(true); }, [patchSession, sessionName]); - const { isWaiting } = useWaitForSessionStatus({ + const { isWaiting } = useWaitForSessionStatusV2({ desiredStatus: "hibernated", sessionName, skip: !isStopping, @@ -257,22 +258,13 @@ function PauseSessionModalContent({ } }, [backUrl, isSuccess, isWaiting, navigate]); - const annotations = session - ? (NotebooksHelper.cleanAnnotations( - session.annotations - ) as NotebookAnnotations) - : null; - const hibernatedSecondsThreshold = parseInt( - annotations?.hibernatedSecondsThreshold ?? "", - 10 - ); - const duration = isNaN(hibernatedSecondsThreshold) - ? Duration.fromISO("") - : Duration.fromObject({ seconds: hibernatedSecondsThreshold }); - const hibernationThreshold = duration.isValid - ? toHumanDuration({ duration }) - : "a period"; - + const now = DateTime.utc(); + const hibernationThreshold = session?.status?.will_hibernate_at + ? toHumanRelativeDuration({ + datetime: session?.status?.will_hibernate_at, + now, + }) + : 0; return ( <> @@ -281,12 +273,13 @@ function PauseSessionModalContent({ session (new and edited files) will be preserved while the session is paused.

- {hibernatedSecondsThreshold > 0 && ( - - Please note that paused session are deleted after{" "} - {hibernationThreshold} of inactivity. - - )} + {session?.status?.will_hibernate_at && + session?.status?.will_hibernate_at?.length > 0 && ( + + Please note that paused session are deleted after{" "} + {hibernationThreshold} of inactivity. + + )}
+ - ); diff --git a/client/src/features/sessionsV2/SessionList/SessionItem.tsx b/client/src/features/sessionsV2/SessionList/SessionItem.tsx index df93d2fb65..d054cabbe6 100644 --- a/client/src/features/sessionsV2/SessionList/SessionItem.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionItem.tsx @@ -28,14 +28,13 @@ import { SessionStatusV2Description, SessionStatusV2Label, } from "../components/SessionStatus/SessionStatus"; -import { SessionLauncher } from "../sessionsV2.types"; -import { Session } from "../../session/sessions.types"; +import { SessionLauncher, SessionV2 } from "../sessionsV2.types"; interface SessionItemProps { launcher?: SessionLauncher; name?: string; project: Project; - session?: Session; + session?: SessionV2; toggleSessionDetails: () => void; } export default function SessionItem({ diff --git a/client/src/features/sessionsV2/SessionList/SessionItemDisplay.tsx b/client/src/features/sessionsV2/SessionList/SessionItemDisplay.tsx index 98df67b1a6..c40c05c5ba 100644 --- a/client/src/features/sessionsV2/SessionList/SessionItemDisplay.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionItemDisplay.tsx @@ -17,10 +17,8 @@ */ import { useMemo, useState } from "react"; -import { NotebookAnnotations } from "../../../notebooks/components/session.types"; import { Project } from "../../projectsV2/api/projectV2.api"; -import sessionsApi from "../../session/sessions.api"; -import { filterSessionsWithCleanedAnnotations } from "../../session/sessions.utils"; +import { useGetSessionsQuery as useGetSessionsQueryV2 } from "../sessionsV2.api"; import { SessionView } from "../SessionView/SessionView"; import { SessionLauncher } from "../sessionsV2.types"; import SessionItem from "./SessionItem"; @@ -35,34 +33,30 @@ export function SessionItemDisplay({ }: SessionLauncherDisplayProps) { const { name } = launcher; const [toggleSessionView, setToggleSessionView] = useState(false); - const { data: sessions } = sessionsApi.endpoints.getSessions.useQueryState(); + const { data: sessions } = useGetSessionsQueryV2(); + const filteredSessions = useMemo( () => sessions != null - ? filterSessionsWithCleanedAnnotations( - sessions, - ({ annotations }) => - annotations["renkuVersion"] === "2.0" && - annotations["projectId"] === project.id && - annotations["launcherId"] === launcher.id + ? sessions.filter( + (session) => + session.launcher_id === launcher.id && + session.project_id === project.id ) - : {}, + : [], [launcher.id, project.id, sessions] ); - const filteredSessionsLength = useMemo( - () => Object.keys(filteredSessions).length, - [filteredSessions] - ); + const toggleSessionDetails = () => { setToggleSessionView((open: boolean) => !open); }; return ( <> - {filteredSessionsLength > 0 ? ( - Object.entries(filteredSessions).map(([key, session]) => ( + {filteredSessions?.length > 0 ? ( + filteredSessions.map((session) => ( { + return !isSessionReady + ? { + position: "absolute", + top: 0, + visibility: "hidden", + } + : {}; +}; + +interface SessionIframeProps { + height: string; + isSessionReady: boolean; + session: SessionV2; +} + +export default function SessionIframe({ + height, + isSessionReady, + session: { status, url }, +}: SessionIframeProps) { + if (status.state !== "running") return null; + + const style = getIframeStyle(isSessionReady); + + try { + const secureUrl = ensureHTTPS(url); + return ( +