diff --git a/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts b/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts index 6407d0bf2f..7d64ac3899 100644 --- a/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts +++ b/client/src/features/ProjectPageV2/utils/useProjectPermissions.hook.ts @@ -30,7 +30,7 @@ interface UseProjectPermissionsArgs { export default function useProjectPermissions({ projectId, }: UseProjectPermissionsArgs): PermissionsWithLoadingState { - const { currentData, isLoading, isError, isUninitialized } = + const { currentData, isLoading, isError, isUninitialized, error } = projectV2Api.endpoints.getProjectsByProjectIdPermissions.useQueryState( projectId ? { projectId } : skipToken, ); @@ -44,17 +44,25 @@ export default function useProjectPermissions({ }, [fetchPermissions, isUninitialized, projectId]); const isLoadingPermissions = isLoading || !!(projectId && isUninitialized); + const arePermissionsResolved = + !isLoadingPermissions && !isError && currentData != null; if (isLoading || isError || !currentData) { return { ...DEFAULT_PERMISSIONS, + arePermissionsResolved: false, isLoadingPermissions, + isPermissionsError: isError, + permissionsError: isError ? error : undefined, }; } return { ...DEFAULT_PERMISSIONS, ...currentData, + arePermissionsResolved, isLoadingPermissions: false, + isPermissionsError: false, + permissionsError: undefined, }; } diff --git a/client/src/features/permissionsV2/permissions.types.ts b/client/src/features/permissionsV2/permissions.types.ts index ae9234206d..8c3657fbd5 100644 --- a/client/src/features/permissionsV2/permissions.types.ts +++ b/client/src/features/permissionsV2/permissions.types.ts @@ -23,5 +23,10 @@ export type Permissions = { }; export type PermissionsWithLoadingState = Permissions & { + arePermissionsResolved: boolean; isLoadingPermissions: boolean; + isPermissionsError: boolean; + permissionsError?: unknown; }; + +export type ProjectPermissions = PermissionsWithLoadingState; diff --git a/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx b/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx index 9c675d2003..33dafb74e9 100644 --- a/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx +++ b/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx @@ -59,6 +59,13 @@ const CONTEXT_STRINGS = { testError: "The data connector could not be mounted. Please retry with different credentials, or skip the data connector. If you skip, the data connector will not be mounted in the session.", }, + job: { + continueButton: "Continue", + dataCy: "job-data-connector-credentials-modal", + header: "Job Storage Credentials", + testError: + "The data connector could not be mounted. Please retry with different credentials, or skip the data connector. If you skip, the data connector will not be mounted in the job.", + }, storage: { continueButton: "Test and Save", dataCy: "data-connector-credentials-modal", @@ -158,7 +165,7 @@ function DataConnectorSecrets({ } interface DataConnectorSecretsModalProps { - context?: "session" | "storage"; + context?: "session" | "job" | "storage"; isOpen: boolean; onCancel: () => void; onStart: (dataConnectorConfigs: DataConnectorConfiguration[]) => void; @@ -352,7 +359,9 @@ function CredentialsButtons({ Cancel - {context === "session" && } + {(context === "session" || context === "job") && ( + + )} {context === "storage" && ( ) { +}: Pick) { const skipButtonRef = useRef(null); + const targetLabel = context === "job" ? "job" : "session"; return ( <> @@ -624,7 +635,7 @@ function SkipConnectionTestButton({ - Skip the data connector. It will not be mounted in the session. + {`Skip the data connector. It will not be mounted in the ${targetLabel}.`} ); diff --git a/client/src/features/sessionsV2/SaveCloudStorageCredentials.tsx b/client/src/features/sessionsV2/SaveCloudStorageCredentials.tsx new file mode 100644 index 0000000000..4844c9b26c --- /dev/null +++ b/client/src/features/sessionsV2/SaveCloudStorageCredentials.tsx @@ -0,0 +1,184 @@ +/*! + * Copyright 2026 - 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 { useEffect, useMemo, useRef, useState } from "react"; + +import RtkOrDataServicesError from "~/components/errors/RtkOrDataServicesError"; +import ProgressStepsIndicator, { + ProgressStyle, + ProgressType, + StatusStepProgressBar, + StepsProgressBar, +} from "~/components/progress/ProgressSteps"; +import { storageDefinitionAfterSavingCredentialsFromConfig } from "../cloudStorage/projectCloudStorage.utils"; +import { usePatchDataConnectorsByDataConnectorIdSecretsMutation } from "../dataConnectorsV2/api/data-connectors.enhanced-api"; +import { shouldCloudStorageSaveCredentials } from "./sessionLaunchValidation.utils"; +import type { SessionStartDataConnectorConfiguration } from "./startSessionOptionsV2.types"; + +import progressBoxStyles from "~/components/progress/ProgressBox.module.scss"; + +interface SaveCloudStorageCredentialsProps { + dataConnectors: SessionStartDataConnectorConfiguration[]; + onComplete: (configs: SessionStartDataConnectorConfiguration[]) => void; + title?: string; +} + +export default function SaveCloudStorageCredentials({ + dataConnectors, + onComplete, + title = "Saving credentials", +}: SaveCloudStorageCredentialsProps) { + const [steps, setSteps] = useState([]); + const [saveCredentials, saveCredentialsResult] = + usePatchDataConnectorsByDataConnectorIdSecretsMutation(); + + const credentialsToSave = useMemo(() => { + return dataConnectors + .filter(shouldCloudStorageSaveCredentials) + .map((cs) => ({ + storageName: cs.dataConnector.name, + storageId: cs.dataConnector.id, + secrets: cs.sensitiveFieldValues, + })); + }, [dataConnectors]); + + const [results, setResults] = useState( + credentialsToSave.map(() => StatusStepProgressBar.WAITING), + ); + + const [index, setIndex] = useState(0); + const [hasFailed, setHasFailed] = useState(false); + const [failedError, setFailedError] = + useState(undefined); + const hasCompletedRef = useRef(false); + + useEffect(() => { + const theSteps = credentialsToSave.map((cs, i) => ({ + id: i, + status: results[i], + step: `Saving credentials for ${cs.storageName}`, + })); + // TODO: fix react-hooks/set-state-in-effect + // eslint-disable-next-line react-hooks/set-state-in-effect + setSteps(theSteps); + }, [credentialsToSave, results]); + + useEffect(() => { + if ( + hasFailed || + credentialsToSave.length < 1 || + index >= credentialsToSave.length + ) + return; + // TODO: fix react-hooks/set-state-in-effect + // eslint-disable-next-line react-hooks/set-state-in-effect + setResults((prev) => { + const newResults = [...prev]; + newResults[index] = StatusStepProgressBar.EXECUTING; + return newResults; + }); + const storage = credentialsToSave[index]; + saveCredentials({ + dataConnectorId: storage.storageId, + dataConnectorSecretPatchList: Object.entries(storage.secrets).map( + ([key, value]) => ({ + name: key, + value, + }), + ), + }); + }, [credentialsToSave, hasFailed, index, saveCredentials]); + + useEffect(() => { + if ( + saveCredentialsResult.isUninitialized || + saveCredentialsResult.isLoading + ) + return; + if (saveCredentialsResult.data != null) { + // TODO: fix react-hooks/set-state-in-effect + // eslint-disable-next-line react-hooks/set-state-in-effect + setResults((prev) => { + const newResults = [...prev]; + newResults[index] = StatusStepProgressBar.READY; + return newResults; + }); + saveCredentialsResult.reset(); + setIndex((prev) => prev + 1); + return; + } + if (saveCredentialsResult.error != null) { + setResults((prev) => { + const newResults = [...prev]; + newResults[index] = StatusStepProgressBar.FAILED; + return newResults; + }); + setFailedError(saveCredentialsResult.error); + setHasFailed(true); + saveCredentialsResult.reset(); + } + }, [index, saveCredentialsResult]); + + useEffect(() => { + if ( + hasFailed || + saveCredentialsResult.isLoading || + hasCompletedRef.current + ) { + return; + } + if (index >= credentialsToSave.length) { + hasCompletedRef.current = true; + const cloudStorageConfigs = dataConnectors.map((cs) => + storageDefinitionAfterSavingCredentialsFromConfig(cs), + ); + onComplete(cloudStorageConfigs); + } + }, [ + credentialsToSave.length, + dataConnectors, + hasFailed, + index, + onComplete, + saveCredentialsResult.isLoading, + ]); + + return ( +
+ {hasFailed && failedError != null && ( + + )} + +
+ ); +} diff --git a/client/src/features/sessionsV2/SessionList/SessionCard.tsx b/client/src/features/sessionsV2/SessionList/SessionCard.tsx index eb8a767673..1d2df4c8ac 100644 --- a/client/src/features/sessionsV2/SessionList/SessionCard.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionCard.tsx @@ -19,6 +19,10 @@ import cx from "classnames"; import { Col, Row } from "reactstrap"; +import { + getLauncherCategoryDefinition, + sessionLauncherKindToCategory, +} from "~/features/sessionsV2/session.utils"; import { Project } from "../../projectsV2/api/projectV2.api"; import ActiveSessionButton from "../components/SessionButton/ActiveSessionButton"; import { @@ -38,7 +42,9 @@ interface SessionCardProps { export default function SessionCard({ project, session }: SessionCardProps) { if (!session) return null; + const launcherCategory = sessionLauncherKindToCategory(session.session_type); const stylesPerSession = getSessionStatusStyles(session); + const launcherDefinition = getLauncherCategoryDefinition(launcherCategory); return (
- Session line indicator -
+ {launcherCategory === "session" && ( + Session line indicator + )} +
@@ -71,7 +85,8 @@ export default function SessionCard({ project, session }: SessionCardProps) { )} > - Session + {launcherDefinition.text.display} + {session.submission_id ? `: ${session.submission_id}` : ""} - {isGlobalEnvironment ? ( - - - Global environment - - ) : isCodeEnvironment ? ( - - - Code based environment - - ) : isCustomImageEnvironment ? ( - - - External image environment - - ) : null} + {launcher?.environment && + getEnvironmentKindLabel(launcher.environment) != null && ( + + + {getEnvironmentKindLabel(launcher.environment)} + + )} )} @@ -224,7 +215,9 @@ export default function SessionLauncherCard({ {name ? ( name ) : ( - Orphan session + + Orphan {launcherDefinition.text.display} + )} @@ -315,9 +308,8 @@ export default function SessionLauncherCard({ hasSession={hasSession} lastBuild={lastBuild} launcher={launcher} - namespace={project.namespace} otherActions={otherLauncherActions} - slug={project.slug} + project={project} /> {useOldImage && lastSuccessfulBuild && ( @@ -95,7 +98,9 @@ export function SessionLauncherDisplay({ toggleUpdateEnvironment={toggleUpdateEnvironment} toggleDelete={toggleDelete} toggleShareLink={ - launcher.launcher_type === "interactive" ? toggleShareLink : undefined + launcher.launcher_type === SESSION_LAUNCHER_KIND.INTERACTIVE + ? toggleShareLink + : undefined } toggleSessionView={toggleSessionView} /> diff --git a/client/src/features/sessionsV2/SessionRepositoriesModal.tsx b/client/src/features/sessionsV2/SessionRepositoriesModal.tsx index ad03fbed7c..56aa3f2662 100644 --- a/client/src/features/sessionsV2/SessionRepositoriesModal.tsx +++ b/client/src/features/sessionsV2/SessionRepositoriesModal.tsx @@ -53,19 +53,30 @@ import startSessionOptionsV2Slice from "./startSessionOptionsV2.slice"; interface SessionRepositoriesModalProps { isOpen: boolean; project: Project; + onSkip?: () => void; + onCancel?: () => void; + continueLabel?: string; + title?: string; + warningIntro?: string; } export default function SessionRepositoriesModal({ isOpen, project, + onSkip: onSkipProp, + onCancel: onCancelProp, + continueLabel = "Launch anyway", + title = "Session repositories not accessible", + warningIntro = "your attention before launching the session", }: SessionRepositoriesModalProps) { const navigate = useNavigate(); - const onCancel = useCallback(() => { + const defaultOnCancel = useCallback(() => { const url = generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, { namespace: project.namespace, slug: project.slug, }); navigate(url); }, [navigate, project.namespace, project.slug]); + const onCancel = onCancelProp ?? defaultOnCancel; const projectPermissions = useProjectPermissions({ projectId: project.id }); const { data, error, isLoading } = useGetRepositoriesQuery( @@ -85,9 +96,10 @@ export default function SessionRepositoriesModal({ }, [data, isLoading, projectPermissions?.write]); const dispatch = useAppDispatch(); - const onSkip = useCallback(() => { + const defaultOnSkip = useCallback(() => { dispatch(startSessionOptionsV2Slice.actions.setRepositoriesReady(true)); }, [dispatch]); + const onSkip = onSkipProp ?? defaultOnSkip; const content = isLoading || !data ? ( @@ -107,7 +119,7 @@ export default function SessionRepositoriesModal({ {repoWithInterruptions.length === 1 ? `is ${repoWithInterruptions.length} repository that requires` : `are ${repoWithInterruptions.length} repositories that require`}{" "} - your attention before launching the session: + your attention {warningIntro}:

{repoWithInterruptions.map((repository) => ( @@ -129,7 +141,7 @@ export default function SessionRepositoriesModal({ isOpen={isOpen} size="lg" > - Session repositories not accessible + {title} {content} diff --git a/client/src/features/sessionsV2/SessionSecretsModal.tsx b/client/src/features/sessionsV2/SessionSecretsModal.tsx index cfa38e8b59..fbf95c560e 100644 --- a/client/src/features/sessionsV2/SessionSecretsModal.tsx +++ b/client/src/features/sessionsV2/SessionSecretsModal.tsx @@ -65,29 +65,37 @@ interface SessionSecretsModalProps { isOpen: boolean; project: Project; sessionSecretSlotsWithSecrets: SessionSecretSlotWithSecret[]; + onSkip?: () => void; + onCancel?: () => void; + title?: string; } export default function SessionSecretsModal({ isOpen, project, sessionSecretSlotsWithSecrets, + onSkip: onSkipProp, + onCancel: onCancelProp, + title = "Session secrets", }: SessionSecretsModalProps) { const { data: user } = useGetUserQueryState(); const navigate = useNavigate(); - const onCancel = useCallback(() => { + const defaultOnCancel = useCallback(() => { const url = generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, { namespace: project.namespace, slug: project.slug, }); navigate(url); }, [navigate, project.namespace, project.slug]); + const onCancel = onCancelProp ?? defaultOnCancel; const dispatch = useAppDispatch(); - const onSkip = useCallback(() => { + const defaultOnSkip = useCallback(() => { dispatch(startSessionOptionsV2Slice.actions.setUserSecretsReady(true)); }, [dispatch]); + const onSkip = onSkipProp ?? defaultOnSkip; const loginUrl = useLoginUrl(); const content = user?.isLoggedIn ? ( @@ -135,7 +143,7 @@ export default function SessionSecretsModal({ isOpen={isOpen} size="lg" > - Session secrets + {title} {content}
} @@ -197,22 +196,24 @@ function SessionCardNotRunning({ ); } -function getSessionColor(state: string, launcherCategory: LauncherCategory) { +function getSessionColor(state: string, launcherCategory?: LauncherCategory) { return state === "running" && launcherCategory === "session" ? "success" : state === "running" && launcherCategory === "job" ? "warning" - : state === "starting" + : state === "starting" && launcherCategory === "session" ? "warning" - : state === "succeeded" - ? "success" + : state === "starting" && launcherCategory === "job" + ? "info" : state === "stopping" ? "warning" : state === "hibernated" ? "dark" : state === "failed" ? "danger" - : "dark"; + : state === "succeeded" + ? "success" + : "dark"; } interface SessionViewProps { @@ -383,11 +384,7 @@ export function SessionView({ session={session} launcher={launcher} /> - +
)) ) : ( diff --git a/client/src/features/sessionsV2/SessionsV2.tsx b/client/src/features/sessionsV2/SessionsV2.tsx index b0bd45f894..8b95176d4a 100644 --- a/client/src/features/sessionsV2/SessionsV2.tsx +++ b/client/src/features/sessionsV2/SessionsV2.tsx @@ -110,7 +110,7 @@ export default function SessionsV2({ project }: SessionsV2Props) {

{totalSessions > 0 ? "Launchers are available to everyone who can see the project. Only you can see your running sessions and jobs." - : "Define interactive environments in which to do your work and share it with others."} + : "Define interactive or not environments in which to do your work and share it with others."}

{loading} {totalSessions > 0 && !isLoading && ( diff --git a/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.actions.tsx b/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.actions.tsx new file mode 100644 index 0000000000..37adb1168c --- /dev/null +++ b/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.actions.tsx @@ -0,0 +1,272 @@ +/*! + * Copyright 2026 - 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 { ReactNode } from "react"; +import { + ArrowRightCircle, + FileEarmarkText, + PauseCircle, + PlayFill, + Tools, + Trash, +} from "react-bootstrap-icons"; +import { Link } from "react-router"; +import { Button } from "reactstrap"; + +import { Loader } from "~/components/Loader"; +import { SessionStatusState } from "../../sessionsV2.types"; + +export interface ActiveSessionActionContext { + status: SessionStatusState; + isStopping: boolean; + isHibernating: boolean; + isResuming: boolean; + failedScheduling: boolean; + isUserLoggedIn: boolean; + showSessionUrl: string; + buttonClassName: string; + onHibernateSession: () => void; + onStopSession: () => void; + onResumeSession: () => void; + toggleLogsModal: () => void; + toggleModifySession: () => void; +} + +function StoppingStatusButton({ label }: { label: string }) { + return ( + + ); +} + +function PausingStatusButton() { + return ( + + ); +} + +function ResumeStatusButton({ + isResuming, + onResumeSession, +}: { + isResuming: boolean; + onResumeSession: () => void; +}) { + return ( + + ); +} + +function LogsStatusButton({ + onClick, + label, +}: { + onClick: () => void; + label: string; +}) { + return ( + + ); +} + +function OpenSessionButton({ showSessionUrl }: { showSessionUrl: string }) { + return ( + + + Open + + ); +} + +function PauseOrDeleteButton({ + buttonClassName, + color = "outline-primary", + isUserLoggedIn, + onHibernateSession, + onStopSession, +}: { + buttonClassName?: string; + color?: "outline-primary" | "primary"; + isUserLoggedIn: boolean; + onHibernateSession: () => void; + onStopSession: () => void; +}) { + return ( + + ); +} + +export function getInteractiveSessionDefaultAction( + ctx: ActiveSessionActionContext, +): ReactNode { + const { + status, + isStopping, + isHibernating, + isResuming, + failedScheduling, + isUserLoggedIn, + showSessionUrl, + buttonClassName, + onHibernateSession, + onStopSession, + onResumeSession, + toggleLogsModal, + toggleModifySession, + } = ctx; + + if (status === "stopping" || isStopping) { + return ; + } + if (isHibernating) { + return ; + } + if (status === "starting") { + return ; + } + if (status === "running") { + return ( + <> + + + + ); + } + if (status === "hibernated") { + return ( + + ); + } + if (failedScheduling) { + return ( + <> + + + + ); + } + return ( + <> + + + + ); +} + +export function getJobDefaultAction( + ctx: ActiveSessionActionContext, +): ReactNode { + const { + status, + isStopping, + isHibernating, + isResuming, + onResumeSession, + toggleLogsModal, + } = ctx; + + if (status === "stopping" || isStopping) { + return ; + } + if (isHibernating) { + return ; + } + if (status === "starting" || status === "running" || status === "succeeded") { + return ; + } + if (status === "hibernated") { + return ( + + ); + } + return ; +} diff --git a/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx b/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx index bf5c4f4566..0d1a6afee4 100644 --- a/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx +++ b/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx @@ -19,7 +19,6 @@ import cx from "classnames"; import { useCallback, useEffect, useState } from "react"; import { - ArrowRightCircle, BoxArrowUpRight, CheckLg, FileEarmarkText, @@ -29,7 +28,7 @@ import { Trash, XLg, } from "react-bootstrap-icons"; -import { Link, useNavigate } from "react-router"; +import { useNavigate } from "react-router"; import { SingleValue } from "react-select"; import { Button, @@ -44,7 +43,6 @@ import { import { WarnAlert } from "~/components/Alert"; import { ButtonWithMenuV2 } from "~/components/buttons/Button"; -import { Loader } from "~/components/Loader"; import useRenkuToast from "~/components/toast/useRenkuToast"; import SessionLogsModal from "~/features/logsDisplay/SessionLogsModal"; import { useGetUserQueryState } from "~/features/usersV2/api/users.api"; @@ -57,6 +55,7 @@ import { usePatchSessionsBySessionIdMutation as usePatchSessionMutation, useDeleteSessionsBySessionIdMutation as useStopSessionMutation, } from "../../api/sessionsV2.api"; +import { sessionLauncherKindToCategory } from "../../session.utils"; import { SessionResources, SessionStatus, @@ -71,6 +70,10 @@ import { } from "../SessionModals/ResourceClassWarning"; import ShutdownSessionContent from "../SessionModals/ShoutdownSessionContent"; import { SessionRowResourceRequests } from "../SessionsList"; +import { + getInteractiveSessionDefaultAction, + getJobDefaultAction, +} from "./ActiveSessionButton.actions"; interface ActiveSessionButtonProps { className?: string; @@ -121,10 +124,7 @@ export default function ActiveSessionButton({ // TODO: fix react-hooks/set-state-in-effect // eslint-disable-next-line react-hooks/set-state-in-effect setIsResuming(false); - navigate(showSessionUrl); - // TODO: fix react-hooks/set-state-in-effect - - setIsResuming(false); + if (session.session_type === "interactive") navigate(showSessionUrl); } }, [ isResuming, @@ -132,6 +132,7 @@ export default function ActiveSessionButton({ isWaitingForResumedSession, navigate, showSessionUrl, + session.session_type, ]); useEffect(() => { if (errorResumeSession) { @@ -266,121 +267,30 @@ export default function ActiveSessionButton({ "btn-outline-primary", ); + const launcherCategory = sessionLauncherKindToCategory(session.session_type); + + const actionContext = { + status, + isStopping, + isHibernating, + isResuming, + failedScheduling, + isUserLoggedIn, + showSessionUrl, + buttonClassName, + onHibernateSession, + onStopSession, + onResumeSession, + toggleLogsModal, + toggleModifySession, + }; + const defaultAction = - status === "stopping" || isStopping ? ( - - ) : isHibernating ? ( - - ) : status === "starting" ? ( - - - Open - - ) : status === "running" ? ( - <> - - - - Open - - - ) : status === "hibernated" ? ( - - ) : failedScheduling ? ( - <> - - - - ) : ( - <> - - - - ); + launcherCategory === "session" + ? getInteractiveSessionDefaultAction(actionContext) + : launcherCategory === "job" + ? getJobDefaultAction(actionContext) + : null; const hibernateAction = status !== "stopping" && (status !== "failed" || failedScheduling) && @@ -407,6 +317,13 @@ export default function ActiveSessionButton({ ); + const dismissAction = launcherCategory === "job" && ( + + + Dismiss job + + ); + const modifyAction = (status === "hibernated" || status === "failed") && !isStopping && !isHibernating && @@ -435,6 +352,27 @@ export default function ActiveSessionButton({ ); + if (launcherCategory === "job") { + return ( +
+ + {dismissAction} + + +
+ ); + } + return (
{ isOptional?: boolean; dataCy?: string; id?: string; + disabled?: boolean; } export function JsonField({ @@ -240,6 +241,7 @@ export function JsonField({ isOptional = true, dataCy, id, + disabled = false, }: JsonFieldProps) { const rules = isOptional ? { @@ -273,6 +275,7 @@ export function JsonField({