{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 (
+