From fbc0695f980d9453f6ba9bb07e2c329c4a231f7c Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Thu, 5 Sep 2024 16:58:25 +0200 Subject: [PATCH] feat: use data service sessions api --- client/src/components/Logs.tsx | 41 ++++- .../dashboardV2/DashboardV2Sessions.tsx | 47 ++---- .../session/components/SessionHibernated.tsx | 11 +- .../session/components/ShowSession.tsx | 2 +- .../components/StartSessionProgressBar.tsx | 36 ++++ .../session/useWaitForSessionStatus.hook.ts | 41 +++++ .../sessionsV2/LazyShowSessionPage.tsx | 2 +- .../sessionsV2/PauseOrDeleteSessionModal.tsx | 67 +++----- .../sessionsV2/SessionList/SessionItem.tsx | 5 +- .../SessionList/SessionItemDisplay.tsx | 29 ++-- .../SessionShowPage/SessionFrame.tsx | 83 ++++++++++ .../{ => SessionShowPage}/ShowSessionPage.tsx | 82 ++++----- .../features/sessionsV2/SessionStartPage.tsx | 33 ++-- .../sessionsV2/SessionView/SessionView.tsx | 11 +- client/src/features/sessionsV2/SessionsV2.tsx | 46 +++--- .../SessionButton/ActiveSessionButton.tsx | 124 ++++++-------- .../SessionStatus/SessionStatus.tsx | 156 ++++++++++-------- .../src/features/sessionsV2/session.utils.ts | 14 +- .../src/features/sessionsV2/sessionsV2.api.ts | 91 +++++++++- .../features/sessionsV2/sessionsV2.types.ts | 68 ++++++++ .../sessionsV2/useSessionLaunchState.hook.ts | 34 ++-- .../utils/customHooks/UseGetSessionLogs.ts | 30 ++++ .../handlers/sessionStatusHandlerV2.ts | 34 ++++ client/src/websocket/index.ts | 16 ++ server/src/api-client/index.ts | 15 ++ server/src/utils/index.ts | 52 ++++++ server/src/websocket/handlers/sessionsV2.ts | 99 +++++++++++ server/src/websocket/index.ts | 12 ++ 28 files changed, 915 insertions(+), 366 deletions(-) create mode 100644 client/src/features/sessionsV2/SessionShowPage/SessionFrame.tsx rename client/src/features/sessionsV2/{ => SessionShowPage}/ShowSessionPage.tsx (84%) create mode 100644 client/src/websocket/handlers/sessionStatusHandlerV2.ts create mode 100644 server/src/websocket/handlers/sessionsV2.ts diff --git a/client/src/components/Logs.tsx b/client/src/components/Logs.tsx index 0e75078aeb..10ab35cb87 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 { @@ -317,7 +319,7 @@ function SessionLogs(props: LogBodyProps) { * @param {object} annotations - list of cleaned annotations */ interface EnvironmentLogsProps { - annotations: Record; + annotations?: Record; name: string; } const EnvironmentLogs = ({ name, annotations }: EnvironmentLogsProps) => { @@ -346,6 +348,31 @@ const EnvironmentLogs = ({ name, annotations }: EnvironmentLogsProps) => { ); }; +export const EnvironmentLogsV2 = ({ name }: EnvironmentLogsProps) => { + 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 +383,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,11 +398,11 @@ 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"]} [ diff --git a/client/src/features/dashboardV2/DashboardV2Sessions.tsx b/client/src/features/dashboardV2/DashboardV2Sessions.tsx index ec2da1f281..1393c9a378 100644 --- a/client/src/features/dashboardV2/DashboardV2Sessions.tsx +++ b/client/src/features/dashboardV2/DashboardV2Sessions.tsx @@ -1,20 +1,15 @@ import { 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 { RtkErrorAlert } from "../../components/errors/RtkErrorAlert"; -import { NotebooksHelper } from "../../notebooks"; -import { NotebookAnnotations } from "../../notebooks/components/session.types"; 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 { useGetSessionsQuery as useGetSessionsQueryV2 } from "../../features/sessionsV2/sessionsV2.api.ts"; import ActiveSessionButton from "../sessionsV2/components/SessionButton/ActiveSessionButton"; import { SessionStatusV2Description, @@ -23,20 +18,10 @@ import { // Required for logs formatting import "../../notebooks/Notebooks.css"; +import { SessionV2 } from "../sessionsV2/sessionsV2.types.ts"; export default function DashboardV2Sessions() { - const { data: sessions, error, isLoading } = useGetSessionsQuery(); - - const v2Sessions = useMemo( - () => - sessions != null - ? filterSessionsWithCleanedAnnotations( - sessions, - ({ annotations }) => annotations["renkuVersion"] === "2.0" - ) - : {}, - [sessions] - ); + const { data: sessions, error, isLoading } = useGetSessionsQueryV2(); const noSessions = isLoading ? (
@@ -48,9 +33,7 @@ export default function DashboardV2Sessions() {

Cannot show sessions.

- ) : !sessions || - (Object.keys(sessions).length == 0 && - Object.keys(v2Sessions).length == 0) ? ( + ) : !sessions || sessions.length == 0 ? (
No running sessions.
) : null; @@ -58,27 +41,24 @@ export default function DashboardV2Sessions() { return ( - {Object.entries(v2Sessions).map(([key, session]) => ( - - ))} + {sessions && + 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 +127,7 @@ function DashboardSession({ session }: DashboardSessionProps) { - + ); } diff --git a/client/src/features/session/components/SessionHibernated.tsx b/client/src/features/session/components/SessionHibernated.tsx index 9b3bac00a9..bbaa2ec230 100644 --- a/client/src/features/session/components/SessionHibernated.tsx +++ b/client/src/features/session/components/SessionHibernated.tsx @@ -32,13 +32,14 @@ import AppContext from "../../../utils/context/appContext"; import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; import { Url } from "../../../utils/helpers/url"; import { usePatchSessionMutation } from "../sessions.api"; -import { Session } from "../sessions.types"; interface SessionHibernatedProps { - session: Session; + sessionName: string; } -export default function SessionHibernated({ session }: SessionHibernatedProps) { +export default function SessionHibernated({ + sessionName, +}: SessionHibernatedProps) { const location = useLocation<{ filePath?: string } | undefined>(); 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..b5661f2c59 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 ProgressIndicator from "../../../components/progress/Progress"; +import { SessionV2 } from "../../sessionsV2/sessionsV2.types"; interface StartSessionProgressBarProps { includeStepInTitle?: boolean; @@ -59,6 +61,40 @@ 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 ( +
+ +
{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..b02da4d23d 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 { usePatchSessionMutation, useStopSessionMutation, } from "../session/sessions.api"; -import { Session } from "../session/sessions.types"; -import useWaitForSessionStatus from "../session/useWaitForSessionStatus.hook"; +import { useWaitForSessionStatusV2 } from "../session/useWaitForSessionStatus.hook"; 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; @@ -119,7 +116,7 @@ function AnonymousDeleteSessionModal({ setIsStopping(true); }, [sessionName, stopSession]); - const { isWaiting } = useWaitForSessionStatus({ + const { isWaiting } = useWaitForSessionStatusV2({ desiredStatus: "stopping", sessionName, skip: !isStopping, @@ -234,7 +231,7 @@ function PauseSessionModalContent({ setIsStopping(true); }, [patchSession, sessionName]); - const { isWaiting } = useWaitForSessionStatus({ + const { isWaiting } = useWaitForSessionStatusV2({ desiredStatus: "hibernated", sessionName, skip: !isStopping, @@ -257,22 +254,14 @@ 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"; - + //TODO ANDREA CHECK hibernation format + const now = DateTime.utc(); + const hibernationThreshold = session?.status?.will_hibernate_at + ? toHumanRelativeDuration({ + datetime: session?.status?.will_hibernate_at, + now, + }) + : 0; return ( <> @@ -281,12 +270,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. + + )}