From ec224b158ce1f9869e24e7025156ba6ef43763f7 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Tue, 26 May 2026 07:51:12 +0200 Subject: [PATCH 1/3] feat: submit a job --- .../dashboardV2/DashboardV2Sessions.tsx | 24 +- client/src/features/logsDisplay/LogsModal.tsx | 17 +- .../projectsV2/api/projectV2.openapi.json | 2 +- .../sessionsV2/SessionList/SessionCard.tsx | 52 +- .../SessionList/SessionLauncherCard.tsx | 1 + .../SessionList/SessionLauncherDisplay.tsx | 9 +- .../sessionsV2/SessionStyles.constants.ts | 42 ++ .../sessionsV2/SessionView/SessionView.tsx | 14 +- client/src/features/sessionsV2/SessionsV2.tsx | 8 +- .../api/computeResources.openapi.json | 32 +- .../SessionButton/ActiveSessionButton.tsx | 305 ++++++---- .../SessionModals/SubmitJobModal.tsx | 520 ++++++++++++++++++ .../SessionModals/useSubmitJobForm.ts | 85 +++ .../SessionStatus/SessionStatus.tsx | 168 ++++-- .../components/SubmitJobLauncherAction.tsx | 18 +- .../features/sessionsV2/session.constants.tsx | 41 +- .../src/features/sessionsV2/session.utils.ts | 158 +++++- .../features/sessionsV2/sessionsV2.types.ts | 22 +- tests/cypress/e2e/projectV2SubmitJob.spec.ts | 122 ++++ .../sessions-with-submission-id.json | 25 + 20 files changed, 1467 insertions(+), 198 deletions(-) create mode 100644 client/src/features/sessionsV2/components/SessionModals/SubmitJobModal.tsx create mode 100644 client/src/features/sessionsV2/components/SessionModals/useSubmitJobForm.ts create mode 100644 tests/cypress/e2e/projectV2SubmitJob.spec.ts create mode 100644 tests/cypress/fixtures/projectV2/sessions-with-submission-id.json diff --git a/client/src/features/dashboardV2/DashboardV2Sessions.tsx b/client/src/features/dashboardV2/DashboardV2Sessions.tsx index cad92358f8..db4a14ca91 100644 --- a/client/src/features/dashboardV2/DashboardV2Sessions.tsx +++ b/client/src/features/dashboardV2/DashboardV2Sessions.tsx @@ -22,6 +22,7 @@ import cx from "classnames"; import { generatePath, Link } from "react-router"; import { Col, ListGroup, Row } from "reactstrap"; +import { sessionLauncherKindToCategory } from "~/features/sessionsV2/session.utils.ts"; import RtkOrDataServicesError from "../../components/errors/RtkOrDataServicesError"; import { Loader } from "../../components/Loader"; import { ABSOLUTE_ROUTES } from "../../routing/routes.constants"; @@ -112,10 +113,10 @@ interface DashboardSessionProps { function DashboardSession({ session }: DashboardSessionProps) { const { project_id: projectId, launcher_id: launcherId } = session; const { data: project } = useGetProjectsByProjectIdQuery( - projectId ? { projectId } : skipToken, + projectId ? { projectId } : skipToken ); const { data: launcher } = useGetProjectSessionLauncherQuery( - launcherId ? { launcherId } : skipToken, + launcherId ? { launcherId } : skipToken ); const projectUrl = project @@ -124,10 +125,10 @@ function DashboardSession({ session }: DashboardSessionProps) { slug: project.slug, }) : projectId - ? generatePath(ABSOLUTE_ROUTES.v2.projects.showById, { - id: projectId, - }) - : ABSOLUTE_ROUTES.v2.index; + ? generatePath(ABSOLUTE_ROUTES.v2.projects.showById, { + id: projectId, + }) + : ABSOLUTE_ROUTES.v2.index; const showSessionUrl = project ? generatePath(ABSOLUTE_ROUTES.v2.projects.show.sessions.show, { namespace: project.namespace, @@ -136,7 +137,8 @@ function DashboardSession({ session }: DashboardSessionProps) { }) : ABSOLUTE_ROUTES.v2.index; - const sessionStyles = getSessionStatusStyles(session); + const launcherCategory = sessionLauncherKindToCategory(session.session_type); + const sessionStyles = getSessionStatusStyles(session, launcherCategory); const state = session.status.state; return ( @@ -151,7 +153,7 @@ function DashboardSession({ session }: DashboardSessionProps) { "gap-3", "link-primary", "text-body", - "text-decoration-none", + "text-decoration-none" )} to={{ pathname: projectUrl }} > @@ -165,7 +167,7 @@ function DashboardSession({ session }: DashboardSessionProps) { "cursor-pointer", "d-inline-block", "link-primary", - "text-body", + "text-body" )} data-cy="list-session-link" > @@ -176,7 +178,7 @@ function DashboardSession({ session }: DashboardSessionProps) { {launcher?.environment?.name} ) : ( - (projectId ?? "Unknown") + projectId ?? "Unknown" )} @@ -195,7 +197,7 @@ function DashboardSession({ session }: DashboardSessionProps) { "mt-2", "d-block", "d-sm-flex", - "gap-5", + "gap-5" )} xs={12} > diff --git a/client/src/features/logsDisplay/LogsModal.tsx b/client/src/features/logsDisplay/LogsModal.tsx index 196515147b..bccdd99a65 100644 --- a/client/src/features/logsDisplay/LogsModal.tsx +++ b/client/src/features/logsDisplay/LogsModal.tsx @@ -100,10 +100,13 @@ export default function LogsModal({ "fs-4", "fst-italic", "mb-0", - getSessionStatusStyles({ - status: { state: sessionState }, - image: "url", - })["textColorCard"], + getSessionStatusStyles( + { + status: { state: sessionState }, + image: "url", + }, + "session" + )["textColorCard"] )} > Session status: {sessionState} @@ -217,7 +220,7 @@ function TabbedLogs({ data, defaultTab }: TabbedLogsProps) { }, [data, defaultTab]); const [activeTab, setActiveTab] = useState( - sortedLogs.at(0)?.tab ?? "", + sortedLogs.at(0)?.tab ?? "" ); const preRef = useRef(null); @@ -301,7 +304,7 @@ function ModalFooterButtons({ const [isDownloading, triggerDownload] = useDownloadLogs( name, refetch, - downloadQueryTrigger, + downloadQueryTrigger ); const canDownload = !isFetching && @@ -350,7 +353,7 @@ function ModalFooterButtons({ function useDownloadLogs( name: string, refetch: LogsQuery["refetch"], - downloadQueryTrigger: DownloadLogsLazyQueryTrigger | undefined | null, + downloadQueryTrigger: DownloadLogsLazyQueryTrigger | undefined | null ): [boolean, () => void] { const [isDownloading, setIsDownloading] = useState(false); diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json index 60e3fe95d0..81114656e2 100644 --- a/client/src/features/projectsV2/api/projectV2.openapi.json +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -1197,7 +1197,7 @@ "description": "A container image", "type": "string", "maxLength": 500, - "pattern": "^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$", + "minLength": 3, "example": "renku/renkulab-py:3.10-0.18.1" }, "DefaultUrl": { diff --git a/client/src/features/sessionsV2/SessionList/SessionCard.tsx b/client/src/features/sessionsV2/SessionList/SessionCard.tsx index eb8a767673..16cbcbc1bf 100644 --- a/client/src/features/sessionsV2/SessionList/SessionCard.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionCard.tsx @@ -19,6 +19,7 @@ import cx from "classnames"; import { Col, Row } from "reactstrap"; +import { getLauncherCategoryDefinition } from "~/features/sessionsV2/session.utils.ts"; import { Project } from "../../projectsV2/api/projectV2.api"; import ActiveSessionButton from "../components/SessionButton/ActiveSessionButton"; import { @@ -27,18 +28,24 @@ import { SessionStatusV2Label, } from "../components/SessionStatus/SessionStatus"; import { getShowSessionUrlByProject } from "../SessionsV2"; -import { SessionV2 } from "../sessionsV2.types"; +import { LauncherCategory, SessionV2 } from "../sessionsV2.types"; import styles from "./Session.module.scss"; interface SessionCardProps { project: Project; session?: SessionV2; + launcherCategory: LauncherCategory; } -export default function SessionCard({ project, session }: SessionCardProps) { +export default function SessionCard({ + project, + session, + launcherCategory, +}: SessionCardProps) { if (!session) return null; - const stylesPerSession = getSessionStatusStyles(session); + const stylesPerSession = getSessionStatusStyles(session, launcherCategory); + const launcherDefinition = getLauncherCategoryDefinition(launcherCategory); return (
- Session line indicator -
+ {launcherCategory === "session" && ( + Session line indicator + )} +
@@ -67,11 +82,14 @@ export default function SessionCard({ project, session }: SessionCardProps) { "d-inline-block", "link-primary", "text-body", - "mt-1", + "mt-1" )} > - Session + + {launcherDefinition.text.display} + + {session.submission_id ? `: ${session.submission_id}` : ""} - +
diff --git a/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx b/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx index 514adb2803..5f684bdc9b 100644 --- a/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionLauncherCard.tsx @@ -369,6 +369,7 @@ export default function SessionLauncherCard({ key={`session-item-${session.name}`} project={project} session={session} + launcherCategory={launcherCategory} /> ))}
diff --git a/client/src/features/sessionsV2/SessionList/SessionLauncherDisplay.tsx b/client/src/features/sessionsV2/SessionList/SessionLauncherDisplay.tsx index e48b7ba62e..a68da7d3e1 100644 --- a/client/src/features/sessionsV2/SessionList/SessionLauncherDisplay.tsx +++ b/client/src/features/sessionsV2/SessionList/SessionLauncherDisplay.tsx @@ -25,6 +25,7 @@ import { useGetSessionsQuery as useGetSessionsQueryV2 } from "../api/sessionsV2. import UpdateSessionLauncherMetadataModal from "../components/SessionModals/UpdateSessionLauncherMetadataModal"; import UpdateSessionLauncherEnvironmentModal from "../components/SessionModals/UpdateSessionLauncherModal"; import DeleteSessionV2Modal from "../DeleteSessionLauncherModal"; +import { SESSION_LAUNCHER_KIND } from "../sessionsV2.types"; import SessionLaunchLinkModal from "../SessionView/SessionLaunchLinkModal"; import { SessionView } from "../SessionView/SessionView"; import SessionLauncherCard from "./SessionLauncherCard"; @@ -69,7 +70,9 @@ export function SessionLauncherDisplay({ }); }, [launcherHash, setHash]); - const { data: sessions } = useGetSessionsQueryV2({}); + const { data: sessions } = useGetSessionsQueryV2({ + sessionType: launcher.launcher_type, + }); const filteredSessions = useMemo( () => @@ -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/SessionStyles.constants.ts b/client/src/features/sessionsV2/SessionStyles.constants.ts index eabe384032..37212945f0 100644 --- a/client/src/features/sessionsV2/SessionStyles.constants.ts +++ b/client/src/features/sessionsV2/SessionStyles.constants.ts @@ -16,6 +16,13 @@ * limitations under the License. */ +import { + Check2Circle, + ExclamationDiamond, + Gear, + Hourglass, +} from "react-bootstrap-icons"; + import blockIcon from "../../styles/assets/block.svg"; import failedIcon from "../../styles/assets/failed.svg"; import lineBlock from "../../styles/assets/lineBlock.svg"; @@ -45,6 +52,7 @@ export const SESSION_STYLES = { borderColor: "border-warning", sessionLine: linePlaying, sessionIcon: playingIcon, + jobIcon: ExclamationDiamond, }, SUCCESS: { textColorCard: "text-success-emphasis", @@ -54,6 +62,7 @@ export const SESSION_STYLES = { borderColor: "border-success", sessionLine: linePlaying, sessionIcon: playingIcon, + jobIcon: Check2Circle, }, HIBERNATED: { textColorCard: "text-dark-emphasis", @@ -63,6 +72,7 @@ export const SESSION_STYLES = { borderColor: "border-dark-subtle", sessionLine: linePaused, sessionIcon: pausedIcon, + jobIcon: Hourglass, }, FAILED: { textColorCard: "text-danger-emphasis", @@ -72,6 +82,7 @@ export const SESSION_STYLES = { borderColor: "border-danger", sessionLine: lineFailed, sessionIcon: failedIcon, + jobIcon: ExclamationDiamond, }, STOPPING: { textColorCard: "text-warning-emphasis", @@ -81,6 +92,17 @@ export const SESSION_STYLES = { borderColor: "border-warning", sessionLine: lineStopped, sessionIcon: stoppedIcon, + jobIcon: Hourglass, + }, + RUNNING_JOB: { + textColorCard: "text-warning-emphasis", + textColorList: "text-warning-emphasis", + bgColor: "warning", + bgOpacity: 10, + borderColor: "border-warning", + sessionLine: linePlaying, + sessionIcon: playingIcon, + jobIcon: Gear, }, DEFAULT: { textColorCard: "text-warning-emphasis", @@ -90,6 +112,7 @@ export const SESSION_STYLES = { borderColor: "border-warning", sessionLine: lineBlock, sessionIcon: blockIcon, + jobIcon: Gear, }, } as const; @@ -102,6 +125,15 @@ export const SESSION_TITLE = { [SESSION_STATES.SUCCEEDED]: "Session succeeded (TBD)", default: "Unknown status", }; +export const JOB_TITLE = { + [SESSION_STATES.RUNNING]: "My running job", + [SESSION_STATES.STARTING]: "My running job...", + [SESSION_STATES.STOPPING]: "Dismissing my job...", + [SESSION_STATES.HIBERNATED]: "Paused job", + [SESSION_STATES.FAILED]: "Errored job", //eslint-disable-line spellcheck/spell-checker + [SESSION_STATES.SUCCEEDED]: "Completed job", + default: "Unknown status", +}; export const SESSION_TITLE_DASHBOARD = { [SESSION_STATES.RUNNING]: "Running session", @@ -112,3 +144,13 @@ export const SESSION_TITLE_DASHBOARD = { [SESSION_STATES.SUCCEEDED]: "Session succeeded (TBD)", default: "Unknown status", }; + +export const JOB_TITLE_DASHBOARD = { + [SESSION_STATES.RUNNING]: "Running job", + [SESSION_STATES.STARTING]: "Submitting job", + [SESSION_STATES.STOPPING]: "Dismissing job...", + [SESSION_STATES.HIBERNATED]: "Paused job", + [SESSION_STATES.FAILED]: "Errored job", //eslint-disable-line spellcheck/spell-checker + [SESSION_STATES.SUCCEEDED]: "Completed job", + default: "Unknown status", +}; diff --git a/client/src/features/sessionsV2/SessionView/SessionView.tsx b/client/src/features/sessionsV2/SessionView/SessionView.tsx index f2d898e73b..72d6b908db 100644 --- a/client/src/features/sessionsV2/SessionView/SessionView.tsx +++ b/client/src/features/sessionsV2/SessionView/SessionView.tsx @@ -77,9 +77,10 @@ import { DEFAULT_URL } from "../session.constants"; import { getLauncherCategory, getLauncherCategoryDefinition, + sessionLauncherKindToCategory, } from "../session.utils"; import { getShowSessionUrlByProject, SessionV2Actions } from "../SessionsV2"; -import { SessionV2 } from "../sessionsV2.types"; +import { LauncherCategory, SessionV2 } from "../sessionsV2.types"; import StartSessionButton from "../StartSessionButton"; import EnvironmentItem from "./EnvironmentItem"; import EnvVariablesCard from "./EnvVariablesCard"; @@ -128,9 +129,10 @@ function SessionCard({ session: SessionV2; project: Project; }) { + const launcherCategory = sessionLauncherKindToCategory(session.session_type); return ( } contentLabel={} contentSession={ @@ -193,17 +195,21 @@ function SessionCardNotRunning({ ); } -function getSessionColor(state: string) { +function getSessionColor(state: string, launcherCategory?: LauncherCategory) { return state === "running" ? "success" - : state === "starting" + : state === "starting" && launcherCategory === "session" ? "warning" + : state === "starting" && launcherCategory === "job" + ? "info" : state === "stopping" ? "warning" : state === "hibernated" ? "dark" : state === "failed" ? "danger" + : state === "succeeded" + ? "success" : "dark"; } diff --git a/client/src/features/sessionsV2/SessionsV2.tsx b/client/src/features/sessionsV2/SessionsV2.tsx index 502f322145..7ebf610136 100644 --- a/client/src/features/sessionsV2/SessionsV2.tsx +++ b/client/src/features/sessionsV2/SessionsV2.tsx @@ -44,7 +44,7 @@ import { useGetSessionsQuery as useGetSessionsQueryV2 } from "./api/sessionsV2.a import { LauncherEnvironmentIcon } from "./components/SessionForm/LauncherEnvironmentIcon"; import SessionLauncherCard from "./SessionList/SessionLauncherCard"; import { SessionLauncherDisplay } from "./SessionList/SessionLauncherDisplay"; -import { SessionV2 } from "./sessionsV2.types"; +import { SESSION_LAUNCHER_KIND, SessionV2 } from "./sessionsV2.types"; import { SessionView } from "./SessionView/SessionView"; export function getShowSessionUrlByProject( @@ -76,7 +76,9 @@ export default function SessionsV2({ project }: SessionsV2Props) { data: sessions, error: sessionsError, isLoading: isLoadingSessions, - } = useGetSessionsQueryV2({}); + } = useGetSessionsQueryV2({ + sessionType: SESSION_LAUNCHER_KIND.NON_INTERACTIVE, + }); const isLoading = isLoadingLaunchers || isLoadingSessions; const error = launchersError || sessionsError; @@ -110,7 +112,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/api/computeResources.openapi.json b/client/src/features/sessionsV2/api/computeResources.openapi.json index 49c011fdd0..b1cf539b6b 100644 --- a/client/src/features/sessionsV2/api/computeResources.openapi.json +++ b/client/src/features/sessionsV2/api/computeResources.openapi.json @@ -1723,6 +1723,9 @@ }, "node_affinities": { "$ref": "#/components/schemas/NodeAffinityList" + }, + "quota_enforced": { + "$ref": "#/components/schemas/QuotaEnforced" } } }, @@ -1756,6 +1759,14 @@ }, "node_affinities": { "$ref": "#/components/schemas/NodeAffinityList" + }, + "quota_enforced": { + "allOf": [ + { + "$ref": "#/components/schemas/QuotaEnforced" + } + ], + "default": false } }, "required": [ @@ -1774,7 +1785,8 @@ "memory": 2, "gpu": 0, "max_storage": 100, - "default_storage": 10 + "default_storage": 10, + "quota_enforced": false } }, "ResourceClassPatch": { @@ -1841,6 +1853,14 @@ }, "id": { "$ref": "#/components/schemas/IntegerId" + }, + "quota_enforced": { + "allOf": [ + { + "$ref": "#/components/schemas/QuotaEnforced" + } + ], + "default": false } }, "required": [ @@ -1861,6 +1881,7 @@ "gpu": 0, "max_storage": 100, "default_storage": 10, + "quota_enforced": false, "id": 1 } }, @@ -1917,6 +1938,7 @@ "gpu": 0, "max_storage": 100, "default_storage": 10, + "quota_enforced": true, "id": 1 }, { @@ -1927,6 +1949,7 @@ "gpu": 2, "max_storage": 10000, "default_storage": 10, + "quota_enforced": true, "id": 2 } ] @@ -1946,6 +1969,7 @@ "gpu": 0, "max_storage": 100, "default_storage": 10, + "quota_enforced": true, "id": 1 }, { @@ -1956,6 +1980,7 @@ "gpu": 2, "max_storage": 10000, "default_storage": 10, + "quota_enforced": true, "id": 2 } ] @@ -2570,6 +2595,11 @@ "description": "A default selection for resource classes or resource pools", "example": false }, + "QuotaEnforced": { + "type": "boolean", + "description": "Whether sessions in this resource class should be hibernated when quota is exhausted", + "example": true + }, "PublicFlag": { "type": "boolean", "description": "A resource pool whose classes can be accessed by anyone", diff --git a/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx b/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx index bf5c4f4566..ad60470171 100644 --- a/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx +++ b/client/src/features/sessionsV2/components/SessionButton/ActiveSessionButton.tsx @@ -57,6 +57,7 @@ import { usePatchSessionsBySessionIdMutation as usePatchSessionMutation, useDeleteSessionsBySessionIdMutation as useStopSessionMutation, } from "../../api/sessionsV2.api"; +import { sessionLauncherKindToCategory } from "../../session.utils"; import { SessionResources, SessionStatus, @@ -208,7 +209,7 @@ export default function ActiveSessionButton({ const [showModalStopSession, setShowModalStopSession] = useState(false); const toggleStopSession = useCallback( () => setShowModalStopSession((show) => !show), - [], + [] ); // Handle modifying session @@ -227,7 +228,7 @@ export default function ActiveSessionButton({ }); } }, - [modifySession, onResumeSession, session.name, session.status.state], + [modifySession, onResumeSession, session.name, session.status.state] ); useEffect(() => { if (errorModifySession) { @@ -241,19 +242,19 @@ export default function ActiveSessionButton({ const [showModalModifySession, setShowModalModifySession] = useState(false); const toggleModifySession = useCallback( () => setShowModalModifySession((show) => !show), - [], + [] ); const status = session.status.state; const failedScheduling = status === "failed" && (!!session.status.message?.includes( - "The resource quota has been exceeded.", + "The resource quota has been exceeded." ) || !!session.status.message?.includes( // TODO: fix spelling in notebooks // eslint-disable-next-line spellcheck/spell-checker - "Your session cannot be scheduled due to insufficent resources.", + "Your session cannot be scheduled due to insufficent resources." )); const buttonClassName = cx( @@ -263,48 +264,24 @@ export default function ActiveSessionButton({ "start-session-button", "py-1", "px-2", - "btn-outline-primary", + "btn-outline-primary" ); + const launcherCategory = sessionLauncherKindToCategory(session.session_type); + const defaultAction = - status === "stopping" || isStopping ? ( - - ) : isHibernating ? ( - - ) : status === "starting" ? ( - - - Open - - ) : status === "running" ? ( - <> - + ) : isHibernating ? ( + + ) : status === "starting" ? ( Open - - ) : status === "hibernated" ? ( - - ) : failedScheduling ? ( - <> + ) : status === "running" ? ( + <> + + + + Open + + + ) : status === "hibernated" ? ( + + ) : failedScheduling ? ( + <> + + + + ) : ( + <> + + + + ) + ) : launcherCategory === "job" ? ( + status === "stopping" || isStopping ? ( + + ) : isHibernating ? ( + + ) : status === "starting" ? ( + <> + + + ) : status === "running" ? ( + ) : status === "hibernated" ? ( - - ) : ( - <> + ) : failedScheduling ? ( + <> + + + ) : status === "succeeded" ? ( - - - ); + ) : ( + <> + + + ) + ) : null; const hibernateAction = status !== "stopping" && (status !== "failed" || failedScheduling) && @@ -407,6 +494,13 @@ export default function ActiveSessionButton({ ); + const dismissAction = launcherCategory === "job" && ( + + + Dismiss job + + ); + const modifyAction = (status === "hibernated" || status === "failed") && !isStopping && !isHibernating && @@ -435,6 +529,27 @@ export default function ActiveSessionButton({ ); + if (launcherCategory === "job") { + return ( +
+ + {dismissAction} + + +
+ ); + } + return (
{ diff --git a/client/src/features/sessionsV2/components/SessionModals/SubmitJobModal.tsx b/client/src/features/sessionsV2/components/SessionModals/SubmitJobModal.tsx new file mode 100644 index 0000000000..7801c30556 --- /dev/null +++ b/client/src/features/sessionsV2/components/SessionModals/SubmitJobModal.tsx @@ -0,0 +1,520 @@ +/*! + * 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 { skipToken } from "@reduxjs/toolkit/query"; +import cx from "classnames"; +import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { + Button, + Form, + FormText, + Input, + Label, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "reactstrap"; + +import { SuccessAlert, WarnAlert } from "~/components/Alert"; +import RtkOrDataServicesError from "~/components/errors/RtkOrDataServicesError"; +import { Loader } from "~/components/Loader"; +import useProjectPermissions from "~/features/ProjectPageV2/utils/useProjectPermissions.hook"; +import AppContext from "~/utils/context/appContext"; +import { DEFAULT_APP_PARAMS } from "~/utils/context/appParams.constants"; +import { useGetResourcePoolsQuery } from "../../api/computeResources.api"; +import { + useGetEnvironmentsByEnvironmentIdBuildsQuery as useGetBuildsQuery, + type SessionLauncher, +} from "../../api/sessionLaunchersV2.api"; +import { + useGetSessionsImagesQuery, + useGetSessionsQuery, + usePostSessionsMutation, +} from "../../api/sessionsV2.api"; +import { SUBMISSION_ID_VALIDATION_MESSAGE } from "../../session.constants"; +import { + buildJobSessionPostRequest, + generateSubmissionId, + getJSONStringArray, + getSubmitJobEnvironmentKindLabel, + isSubmissionIdTaken, + isValidJSONStringArray, + validateSubmissionId, +} from "../../session.utils"; +import { BuildStatusDescription } from "../BuildStatusComponents"; +import SessionClassSelector from "../SessionClassSelector"; +import { LauncherEnvironmentIcon } from "../SessionForm/LauncherEnvironmentIcon"; +import { + ErrorOrNotAvailableResourcePools, + FetchingResourcePools, +} from "./ResourceClassWarning"; +import { + getSubmitJobDefaultValues, + resolveDefaultResourceClass, + useSubmitJobEnvironmentFlags, + type SubmitJobForm, +} from "./useSubmitJobForm"; + +interface SubmitJobModalProps { + isOpen: boolean; + launcher: SessionLauncher; + toggle: () => void; +} + +function SubmitJobJsonField({ + control, + name, + label, + helpText, + info, + isOptional, + dataCy, + id, + rules, +}: { + control: ReturnType>["control"]; + name: "command" | "args"; + label: string; + helpText: string; + info: string; + isOptional?: boolean; + dataCy: string; + id: string; + rules?: Record; +}) { + return ( +
+ + isValidJSONStringArray(value?.toString()), + ...rules, + }} + render={({ field, fieldState: { error } }) => ( + <> +