From 0df603e2118a2502176324da2902a23b6cbc6b57 Mon Sep 17 00:00:00 2001
From: Andrea Cordoba
Date: Mon, 2 Sep 2024 16:47:50 +0200
Subject: [PATCH] feat: add/update launcher with custom environment
---
client/.eslintrc.json | 1 +
client/src/components/MoreInfo.tsx | 52 ++
.../sessionsV2/AddSessionLauncherButton.tsx | 5 +-
.../sessionsV2/SessionLauncherFormContent.tsx | 644 ------------------
.../SessionView/EnvironmentCard.tsx | 263 +++++++
.../sessionsV2/SessionView/SessionView.tsx | 105 +--
client/src/features/sessionsV2/SessionsV2.tsx | 2 +-
.../sessionsV2/UpdateSessionLauncherModal.tsx | 192 ------
.../SessionForm/AdvanceSettingsFields.tsx | 252 +++++++
.../SessionForm/CustomEnvironmentFields.tsx | 82 +++
.../SessionForm/EditLauncherFormContent.tsx | 254 +++++++
.../SessionForm/EnvironmentField.tsx | 77 +++
.../SessionForm/EnvironmentKindField.tsx | 67 ++
.../SessionForm/GlobalEnvironmentFields.tsx | 101 +++
.../SessionForm/LauncherDetailsFields.tsx | 125 ++++
.../SessionForm/SessionEnvironmentItem.tsx | 134 ++++
.../SessionLauncherBreadcrumbNavbar.tsx | 69 ++
.../components/SessionModals/AddSession.tsx | 364 ----------
.../SessionModals/NewSessionLauncherModal.tsx | 256 +++++++
.../UpdateSessionLauncherModal.tsx | 174 +++++
.../src/features/sessionsV2/session.utils.ts | 133 +++-
.../features/sessionsV2/sessionsV2.types.ts | 69 +-
.../sessionsV2/useSessionLaunchState.hook.ts | 34 +-
tests/cypress/e2e/projectV2setup.spec.ts | 21 +
.../fixtures/projectV2/session-launchers.json | 8 +-
25 files changed, 2128 insertions(+), 1356 deletions(-)
create mode 100644 client/src/components/MoreInfo.tsx
delete mode 100644 client/src/features/sessionsV2/SessionLauncherFormContent.tsx
create mode 100644 client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx
delete mode 100644 client/src/features/sessionsV2/UpdateSessionLauncherModal.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/AdvanceSettingsFields.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/CustomEnvironmentFields.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/EditLauncherFormContent.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/EnvironmentField.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/EnvironmentKindField.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/GlobalEnvironmentFields.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/SessionEnvironmentItem.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionForm/SessionLauncherBreadcrumbNavbar.tsx
delete mode 100644 client/src/features/sessionsV2/components/SessionModals/AddSession.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx
create mode 100644 client/src/features/sessionsV2/components/SessionModals/UpdateSessionLauncherModal.tsx
diff --git a/client/.eslintrc.json b/client/.eslintrc.json
index f3a3d759ae..2e39b3a202 100644
--- a/client/.eslintrc.json
+++ b/client/.eslintrc.json
@@ -168,6 +168,7 @@
"jupyter",
"katex",
"kernelspec",
+ "kubernetes",
"Keycloak",
"Lausanne",
"linkify",
diff --git a/client/src/components/MoreInfo.tsx b/client/src/components/MoreInfo.tsx
new file mode 100644
index 0000000000..4ac2738ef2
--- /dev/null
+++ b/client/src/components/MoreInfo.tsx
@@ -0,0 +1,52 @@
+/*!
+ * 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 { ReactNode, useRef } from "react";
+import { InfoCircleFill } from "react-bootstrap-icons";
+import { PopoverBody, UncontrolledPopover } from "reactstrap";
+import LazyRenkuMarkdown from "./markdown/LazyRenkuMarkdown";
+
+export function MoreInfo({
+ help,
+ trigger = "hover focus",
+ children,
+}: {
+ help: string;
+ trigger?: string;
+ children?: ReactNode;
+}) {
+ const ref = useRef(null);
+
+ return (
+ <>
+
+
+
+
+
+
+ {children}
+
+
+ >
+ );
+}
diff --git a/client/src/features/sessionsV2/AddSessionLauncherButton.tsx b/client/src/features/sessionsV2/AddSessionLauncherButton.tsx
index f63a5e6464..1bdbc23ab2 100644
--- a/client/src/features/sessionsV2/AddSessionLauncherButton.tsx
+++ b/client/src/features/sessionsV2/AddSessionLauncherButton.tsx
@@ -20,8 +20,7 @@ import cx from "classnames";
import { useCallback, useState } from "react";
import { PlusLg } from "react-bootstrap-icons";
import { Button } from "reactstrap";
-
-import { Step1AddSessionModal } from "./components/SessionModals/AddSession.tsx";
+import NewSessionLauncherModal from "./components/SessionModals/NewSessionLauncherModal.tsx";
export default function AddSessionLauncherButton({
"data-cy": dataCy,
@@ -52,7 +51,7 @@ export default function AddSessionLauncherButton({
)}
-
+
>
);
}
diff --git a/client/src/features/sessionsV2/SessionLauncherFormContent.tsx b/client/src/features/sessionsV2/SessionLauncherFormContent.tsx
deleted file mode 100644
index 26f3ad345b..0000000000
--- a/client/src/features/sessionsV2/SessionLauncherFormContent.tsx
+++ /dev/null
@@ -1,644 +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 { useEffect, useMemo, useState } from "react";
-import { SingleValue } from "react-select";
-import {
- Control,
- Controller,
- ControllerRenderProps,
- FieldErrors,
- FieldNamesMarkedBoolean,
- UseFormResetField,
- UseFormSetValue,
- UseFormWatch,
-} from "react-hook-form";
-import {
- Card,
- CardBody,
- Input,
- Label,
- ListGroup,
- ListGroupItem,
- Row,
-} from "reactstrap";
-import { Globe2 } from "react-bootstrap-icons";
-
-import { Loader } from "../../components/Loader";
-import { TimeCaption } from "../../components/TimeCaption";
-import { RtkErrorAlert } from "../../components/errors/RtkErrorAlert";
-import { useGetSessionEnvironmentsQuery } from "./sessionsV2.api";
-import { EnvironmentKind, SessionEnvironment } from "./sessionsV2.types";
-import { WarnAlert } from "../../components/Alert.jsx";
-import { useGetResourcePoolsQuery } from "../dataServices/computeResources.api";
-import {
- ResourceClass,
- ResourcePool,
-} from "../dataServices/dataServices.types";
-import { SessionClassSelectorV2 } from "../session/components/options/SessionClassOption";
-
-export interface SessionLauncherForm {
- name: string;
- description: string;
- environment_kind: EnvironmentKind;
- environment_id: string;
- container_image: string;
- default_url: string;
- resourceClass: ResourceClass;
-}
-
-/* Edit session launcher */
-interface SessionLauncherFormContentProps {
- control: Control;
- errors: FieldErrors;
- watch: UseFormWatch;
- touchedFields: Partial<
- Readonly>
- >;
-}
-export default function SessionLauncherFormContent({
- control,
- errors,
- watch,
- touchedFields,
-}: SessionLauncherFormContentProps) {
- const {
- data: environments,
- error,
- isLoading,
- } = useGetSessionEnvironmentsQuery();
- const watchEnvironmentKind = watch("environment_kind");
-
- return (
- <>
-
-
- Name
-
-
(
-
- )}
- rules={{ required: true }}
- />
- Please provide a name
-
-
-
- Description
-
- (
-
- )}
- />
-
-
-
-
Environment Type
-
-
(
-
- )}
- />
-
-
-
-
Environment
- {isLoading && (
-
-
- Loading environments...
-
- )}
- {error && (
- <>
-
Cannot load environments
-
- >
- )}
- {environments && environments.length > 0 && (
-
(
- <>
-
- {environments.map((environment) => (
-
- ))}
-
-
-
- Please choose an environment
-
- >
- )}
- rules={{
- required: watchEnvironmentKind === "global_environment",
- }}
- />
- )}
-
-
-
-
- Container Image
-
-
(
-
- )}
- rules={{ required: watchEnvironmentKind === "container_image" }}
- />
- Please provide a container image
-
-
-
-
- Default URL
-
- (
-
- )}
- />
-
- >
- );
-}
-
-/* Add custom session launcher */
-interface CustomEnvFormContentProps {
- control: Control;
- errors: FieldErrors;
- setValue: UseFormSetValue;
-}
-export function CustomEnvFormContent({
- control,
- errors,
- setValue,
-}: CustomEnvFormContentProps) {
- const { data: resourcePools, isLoading: isLoadingResourcesPools } =
- useGetResourcePoolsQuery({});
-
- const onChangeResourceClass = (resourceClass: SingleValue) => {
- if (resourceClass) setValue("resourceClass", resourceClass);
- };
-
- const defaultSessionClass = useMemo(
- () =>
- resourcePools
- ?.filter((pool) => pool.default)
- .flatMap((pool) => pool.classes)
- .find((c) => c.default) ??
- resourcePools?.find(() => true)?.classes[0] ??
- undefined,
- [resourcePools]
- );
-
- return (
- <>
-
-
- Session launcher name
-
-
(
-
- )}
- rules={{ required: true }}
- />
- Please provide a name
-
-
-
- Container Image
-
-
(
-
- )}
- />
- Please provide a container image
-
-
-
- Default URL (Optional)
-
- (
-
- )}
- />
-
-
-
- Compute resources
-
- {!isLoadingResourcesPools &&
- resourcePools &&
- resourcePools?.length > 0 ? (
-
(
- <>
-
- {errors?.resourceClass && (
-
- Please provide a resource class
-
- )}
- >
- )}
- rules={{ required: true }}
- />
- ) : (
-
- There are no one resource pool available to create a session
-
- )}
-
- >
- );
-}
-
-/* Add existing session launcher */
-interface ExistingEnvFormContentProps {
- control: Control;
- errors: FieldErrors;
- watch: UseFormWatch;
- setValue: UseFormSetValue;
- touchedFields: Partial<
- Readonly>
- >;
- resetField: UseFormResetField;
-}
-export function ExistingEnvFormContent({
- control,
- errors,
- watch,
- setValue,
- touchedFields,
- resetField,
-}: ExistingEnvFormContentProps) {
- const {
- data: environments,
- error,
- isLoading,
- } = useGetSessionEnvironmentsQuery();
- const { data: resourcePools, isLoading: isLoadingResourcesPools } =
- useGetResourcePoolsQuery({});
- const watchEnvironmentId = watch("environment_id");
- const defaultSessionClass = useMemo(
- () =>
- resourcePools
- ?.filter((pool) => pool.default)
- .flatMap((pool) => pool.classes)
- .find((c) => c.default) ??
- resourcePools?.find(() => true)?.classes[0] ??
- undefined,
- [resourcePools]
- );
-
- useEffect(() => {
- if (watchEnvironmentId == null) {
- return;
- }
- if (environments && environments.length > 0 && watchEnvironmentId) {
- const selectedEnv = environments.filter(
- (e) => e.id === watchEnvironmentId
- );
- if (selectedEnv) {
- setValue("name", selectedEnv[0].name);
- resetField("resourceClass");
- if (defaultSessionClass) setValue("resourceClass", defaultSessionClass);
- }
- }
- }, [
- watchEnvironmentId,
- setValue,
- environments,
- resetField,
- defaultSessionClass,
- ]);
-
- const onChangeResourceClass = (resourceClass: SingleValue) => {
- if (resourceClass) setValue("resourceClass", resourceClass);
- };
-
- if (error)
- return (
-
-
Cannot load environments
-
-
- );
- if (isLoading)
- return (
-
-
- Loading environments...
-
- );
- if (!environments)
- return (
-
-
Cannot load environments
-
- );
- if (environments && environments.length === 0)
- return (
-
- No existing environments available. Please contact an admin to update
- this list.
-
- );
-
- return (
- <>
- (
- <>
-
- {environments.map((environment) => (
-
- ))}
-
-
- Please choose an environment
- >
- )}
- rules={{ required: true }}
- />
- >
- );
-}
-
-/* Environment Item */
-interface SessionEnvironmentItemProps {
- environment: SessionEnvironment;
- field: ControllerRenderProps;
- touchedFields: Partial<
- Readonly>
- >;
- resourcePools?: ResourcePool[];
- isLoadingResourcesPools?: boolean;
- onChangeResourceClass?: (resourceClass: SingleValue) => void;
- errors: FieldErrors;
- control: Control;
- defaultSessionClass?: ResourceClass;
-}
-
-export function SessionEnvironmentItem({
- environment,
- control,
- defaultSessionClass,
- field,
- touchedFields,
- resourcePools,
- isLoadingResourcesPools,
- onChangeResourceClass,
- errors,
-}: SessionEnvironmentItemProps) {
- const { creation_date, id, name, description } = environment;
- const isSelected = field.value === id;
-
- const [orderCard, setOrderCard] = useState(isSelected);
- const isEnvironmentIdTouched = touchedFields.environment_id;
-
- useEffect(() => {
- if (!orderCard || isEnvironmentIdTouched) setOrderCard(false);
- }, [isSelected, orderCard, isEnvironmentIdTouched]);
-
- const selector = !isLoadingResourcesPools &&
- resourcePools &&
- resourcePools?.length > 0 && (
-
- (
-
- Compute resources
-
- {errors.resourceClass && (
-
- Select compute resource to continue{" "}
-
- )}
-
- )}
- rules={{ required: true }}
- />
-
- );
-
- return (
-
-
-
-
- {name}
-
-
- Global environment
-
- {description ? description
: null}
-
-
-
-
- {isSelected && selector}
-
-
- );
-}
diff --git a/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx b/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx
new file mode 100644
index 0000000000..d9ae82f50c
--- /dev/null
+++ b/client/src/features/sessionsV2/SessionView/EnvironmentCard.tsx
@@ -0,0 +1,263 @@
+/*!
+ * 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 { Clock, Globe2, Link45deg } from "react-bootstrap-icons";
+import { Card, CardBody, Col, Row } from "reactstrap";
+import { CommandCopy } from "../../../components/commandCopy/CommandCopy";
+import { toHumanDateTime } from "../../../utils/helpers/DateTimeUtils";
+import { SessionLauncher } from "../sessionsV2.types";
+
+export function CustomEnvironmentValues({
+ launcher,
+}: {
+ launcher: SessionLauncher;
+}) {
+ const environment = launcher.environment;
+ return (
+ environment.environment_kind === "CUSTOM" && (
+ <>
+
+ Default URL:
+ {`${environment.default_url ?? ""}`}
+
+
+ Port:
+ {`${environment.port ?? ""}`}
+
+
+ Working directory:
+ {`${environment.working_directory ?? ""}`}
+
+
+ Mount directory:
+ {`${environment.mount_directory ?? ""}`}
+
+
+ UID:
+ {`${environment.uid ?? ""}`}
+
+
+ GUI:
+ {`${environment.gid ?? ""}`}
+
+
+ Command:
+ {`${environment.command?.join(" ") ?? "-"}`}
+
+
+ Args:
+ {`${environment.args?.join(" ") ?? "-"}`}
+
+ >
+ )
+ );
+}
+export function EnvironmentCard({ launcher }: { launcher: SessionLauncher }) {
+ const environment = launcher.environment;
+ return (
+ <>
+
+
+
+
+
+
+
+ {environment.environment_kind === "GLOBAL"
+ ? environment?.name || ""
+ : launcher.name}
+
+
+
+
+
+ {environment.environment_kind === "CUSTOM" ? (
+
+
+ Custom image
+
+ ) : (
+
+
+ Global environment
+
+ )}
+
+ {environment.environment_kind === "GLOBAL" ? (
+ <>
+
+ {environment?.description ? (
+ {environment.description}
+ ) : (
+ No description
+ )}
+
+
+ Container image:
+
+
+
+
+ Created by Renku on{" "}
+ {toHumanDateTime({
+ datetime: launcher.creation_date,
+ format: "date",
+ })}
+
+ >
+ ) : (
+ <>
+
+ Container image:
+
+
+ {environment.environment_kind === "CUSTOM" && (
+
+ )}
+ >
+ )}
+
+
+
+ >
+ );
+}
diff --git a/client/src/features/sessionsV2/SessionView/SessionView.tsx b/client/src/features/sessionsV2/SessionView/SessionView.tsx
index 9afb0474df..3d8bbd6dad 100644
--- a/client/src/features/sessionsV2/SessionView/SessionView.tsx
+++ b/client/src/features/sessionsV2/SessionView/SessionView.tsx
@@ -19,21 +19,18 @@ import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
import { ReactNode, useCallback, useMemo, useState } from "react";
import {
- Boxes,
- Clock,
CircleFill,
+ Clock,
Database,
ExclamationTriangleFill,
- Globe2,
- Pencil,
FileCode,
+ Pencil,
} from "react-bootstrap-icons";
import {
Badge,
Button,
Card,
CardBody,
- CardHeader,
Col,
ListGroup,
ListGroupItem,
@@ -45,7 +42,6 @@ import {
import { TimeCaption } from "../../../components/TimeCaption";
import { CommandCopy } from "../../../components/commandCopy/CommandCopy";
-import { toHumanDateTime } from "../../../utils/helpers/DateTimeUtils";
import { RepositoryItem } from "../../ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay";
import { Project } from "../../projectsV2/api/projectV2.api";
import { useGetStoragesV2Query } from "../../projectsV2/api/storagesV2.api";
@@ -60,17 +56,17 @@ import {
SessionStatusV2Label,
SessionStatusV2Title,
} from "../components/SessionStatus/SessionStatus";
-import sessionsV2Api from "../sessionsV2.api";
-import { SessionEnvironment, SessionLauncher } from "../sessionsV2.types";
-
+import { DEFAULT_URL } from "../session.utils.ts";
+import { SessionLauncher } from "../sessionsV2.types";
import MembershipGuard from "../../ProjectPageV2/utils/MembershipGuard";
import {
useGetResourceClassByIdQuery,
useGetResourcePoolsQuery,
} from "../../dataServices/computeResources.api";
import { useGetProjectsByProjectIdMembersQuery } from "../../projectsV2/api/projectV2.enhanced-api";
-import UpdateSessionLauncherModal from "../UpdateSessionLauncherModal";
+import UpdateSessionLauncherModal from "../components/SessionModals/UpdateSessionLauncherModal.tsx";
import { ModifyResourcesLauncherModal } from "../components/SessionModals/ModifyResourcesLauncher";
+import { EnvironmentCard } from "./EnvironmentCard";
interface SessionCardContentProps {
color: string;
@@ -192,68 +188,6 @@ function getSessionColor(state: string) {
: "dark";
}
-function EnvironmentCard({
- launcher,
- environment,
-}: {
- launcher: SessionLauncher;
- environment?: SessionEnvironment;
-}) {
- return (
- <>
-
-
- {launcher.environment_kind === "global_environment"
- ? environment?.name || No name
- : launcher.name}
-
-
-
- {launcher.environment_kind === "container_image" ? (
- <>
-
- Custom image
- >
- ) : (
- <>
-
- Global environment
- >
- )}
-
- {launcher.environment_kind === "global_environment" ? (
- <>
-
- {environment?.description ? (
- environment.description
- ) : (
- No description
- )}
-
-
- Container image:
-
-
-
-
- Created by Renku on{" "}
- {toHumanDateTime({
- datetime: launcher.creation_date,
- format: "date",
- })}
-
- >
- ) : (
-
- Container image:
-
-
- )}
-
-
- >
- );
-}
interface SessionViewProps {
launcher?: SessionLauncher;
sessions?: Sessions;
@@ -279,19 +213,7 @@ export function SessionView({
const { data: members } = useGetProjectsByProjectIdMembersQuery({
projectId: project.id,
});
- const { data: environments, isLoading } =
- sessionsV2Api.endpoints.getSessionEnvironments.useQueryState(
- launcher && launcher.environment_kind === "global_environment"
- ? undefined
- : skipToken
- );
- const environment = useMemo(() => {
- if (!launcher || launcher.environment_kind === "container_image")
- return undefined;
- if (launcher.environment_kind === "global_environment" && environments)
- return environments?.find((env) => env.id === launcher.environment_id);
- return undefined;
- }, [environments, launcher]);
+ const environment = launcher?.environment;
const { data: dataSources } = useGetStoragesV2Query({
storageV2Params: {
@@ -402,7 +324,7 @@ export function SessionView({
)}
- {launcher && !isLoading && (
+ {launcher && (
Session Environment
@@ -428,7 +350,7 @@ export function SessionView({
minimumRole="editor"
/>
-
+
- {launcher && launcher.default_url ? (
-
+ {launcher && launcher.environment.default_url ? (
+
) : environment && environment.default_url ? (
) : (
-
+
)}
diff --git a/client/src/features/sessionsV2/SessionsV2.tsx b/client/src/features/sessionsV2/SessionsV2.tsx
index 7fcc310435..14a55bcafa 100644
--- a/client/src/features/sessionsV2/SessionsV2.tsx
+++ b/client/src/features/sessionsV2/SessionsV2.tsx
@@ -44,7 +44,7 @@ import AddSessionLauncherButton from "./AddSessionLauncherButton";
import DeleteSessionV2Modal from "./DeleteSessionLauncherModal";
import { SessionItemDisplay } from "./SessionList/SessionItemDisplay";
import { SessionView } from "./SessionView/SessionView";
-import UpdateSessionLauncherModal from "./UpdateSessionLauncherModal";
+import UpdateSessionLauncherModal from "./components/SessionModals/UpdateSessionLauncherModal";
import { useGetProjectSessionLaunchersQuery } from "./sessionsV2.api";
import { SessionLauncher } from "./sessionsV2.types";
import SessionItem from "./SessionList/SessionItem";
diff --git a/client/src/features/sessionsV2/UpdateSessionLauncherModal.tsx b/client/src/features/sessionsV2/UpdateSessionLauncherModal.tsx
deleted file mode 100644
index d3ec49be1b..0000000000
--- a/client/src/features/sessionsV2/UpdateSessionLauncherModal.tsx
+++ /dev/null
@@ -1,192 +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, useEffect } from "react";
-import { CheckLg, XLg } from "react-bootstrap-icons";
-import { useForm } from "react-hook-form";
-import {
- Button,
- Form,
- Modal,
- ModalBody,
- ModalFooter,
- ModalHeader,
-} from "reactstrap";
-
-import { Loader } from "../../components/Loader";
-import { RtkErrorAlert } from "../../components/errors/RtkErrorAlert";
-import SessionLauncherFormContent, {
- SessionLauncherForm,
-} from "./SessionLauncherFormContent";
-import {
- useGetSessionEnvironmentsQuery,
- useUpdateSessionLauncherMutation,
-} from "./sessionsV2.api";
-import {
- SessionLauncher,
- SessionLauncherEnvironment,
-} from "./sessionsV2.types";
-
-interface UpdateSessionLauncherModalProps {
- isOpen: boolean;
- launcher: SessionLauncher;
- toggle: () => void;
-}
-
-export default function UpdateSessionLauncherModal({
- isOpen,
- launcher,
- toggle,
-}: UpdateSessionLauncherModalProps) {
- const { data: environments } = useGetSessionEnvironmentsQuery();
- const [updateSessionLauncher, result] = useUpdateSessionLauncherMutation();
-
- const {
- control,
- formState: { errors, isDirty, touchedFields },
- handleSubmit,
- reset,
- setValue,
- watch,
- } = useForm({
- defaultValues: {
- name: launcher.name,
- description: launcher.description ?? "",
- environment_kind: launcher.environment_kind,
- environment_id:
- launcher.environment_kind === "global_environment"
- ? launcher.environment_id
- : "",
- container_image:
- launcher.environment_kind === "container_image"
- ? launcher.container_image
- : "",
- default_url: launcher.default_url ?? "",
- },
- });
- const onSubmit = useCallback(
- (data: SessionLauncherForm) => {
- const { default_url, description, name } = data;
- const environment: SessionLauncherEnvironment =
- data.environment_kind === "global_environment"
- ? {
- environment_kind: "global_environment",
- environment_id: data.environment_id,
- }
- : {
- environment_kind: "container_image",
- container_image: data.container_image,
- };
- updateSessionLauncher({
- launcherId: launcher.id,
- name,
- description: description.trim() ? description : undefined,
- default_url: default_url.trim() ? default_url : undefined,
- ...environment,
- });
- },
- [launcher.id, updateSessionLauncher]
- );
-
- useEffect(() => {
- if (environments == null) {
- return;
- }
- if (environments.length == 0) {
- setValue("environment_kind", "container_image");
- }
- }, [environments, setValue]);
-
- useEffect(() => {
- if (!result.isSuccess) {
- return;
- }
- toggle();
- }, [result.isSuccess, toggle]);
-
- useEffect(() => {
- if (!isOpen) {
- reset();
- result.reset();
- }
- }, [isOpen, reset, result]);
-
- useEffect(() => {
- reset({
- name: launcher.name,
- description: launcher.description ?? "",
- environment_kind: launcher.environment_kind,
- environment_id:
- launcher.environment_kind === "global_environment"
- ? launcher.environment_id
- : "",
- container_image:
- launcher.environment_kind === "container_image"
- ? launcher.container_image
- : "",
- default_url: launcher.default_url ?? "",
- });
- }, [launcher, reset]);
-
- return (
-
- Edit session {launcher.name}
-
-
-
-
-
-
- Cancel
-
-
- {result.isLoading ? (
-
- ) : (
-
- )}
- Update session
-
-
-
- );
-}
diff --git a/client/src/features/sessionsV2/components/SessionForm/AdvanceSettingsFields.tsx b/client/src/features/sessionsV2/components/SessionForm/AdvanceSettingsFields.tsx
new file mode 100644
index 0000000000..8fb397a63f
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/AdvanceSettingsFields.tsx
@@ -0,0 +1,252 @@
+/*!
+ * 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 { Control, Controller, FieldErrors } from "react-hook-form";
+import { Input, Label } from "reactstrap";
+import { MoreInfo } from "../../../../components/MoreInfo";
+import { DEFAULT_URL, getFormCustomValuesDesc } from "../../session.utils";
+import { SessionLauncherForm } from "../../sessionsV2.types";
+
+interface AdvanceSettingsProp {
+ control: Control;
+ errors?: FieldErrors;
+}
+export function AdvanceSettingsFields({
+ control,
+ errors,
+}: AdvanceSettingsProp) {
+ const desc = getFormCustomValuesDesc();
+ return (
+
+
+
+
+
+ Mount directory (Optional)
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+ Docker settings
+
+
+
+
+
+
+ Working directory (Optional)
+
+
+ (
+
+ )}
+ />
+
+
+
+
+
+
(
+
+
+
+ Command ENTRYPOINT (Optional)
+
+
+
+ e.g. {'["python3","main.py"]'}
or{" "}
+ python3 main.py
+
+
+
+
+
+
+ )}
+ />
+
+
+
+ Command Arguments CMD (Optional)
+
+
+
(
+ <>
+
+ e.g. {'["--arg1", "--arg2", "--pwd=/home/user"]'}
+
+
+ or --arg1 --arg2 --pwd=$HOME
+
+
+ >
+ )}
+ />
+
+
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/CustomEnvironmentFields.tsx b/client/src/features/sessionsV2/components/SessionForm/CustomEnvironmentFields.tsx
new file mode 100644
index 0000000000..580c7c8aef
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/CustomEnvironmentFields.tsx
@@ -0,0 +1,82 @@
+/*!
+ * 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 { useState, useCallback } from "react";
+import { Controller } from "react-hook-form";
+import { Label, Input, Collapse } from "reactstrap";
+import ChevronFlippedIcon from "../../../../components/icons/ChevronFlippedIcon";
+import { EnvironmentFieldsProps } from "./EnvironmentField";
+import { AdvanceSettingsFields } from "./AdvanceSettingsFields";
+
+export function CustomEnvironmentFields({
+ watch,
+ control,
+ errors,
+}: EnvironmentFieldsProps) {
+ const watchEnvironmentKind = watch("environment_kind");
+ const [isAdvanceSettingOpen, setIsAdvanceSettingsOpen] = useState(false);
+ const toggleIsOpen = useCallback(
+ () =>
+ setIsAdvanceSettingsOpen((isAdvanceSettingOpen) => !isAdvanceSettingOpen),
+ []
+ );
+ return (
+
+
+ Use a custom image to create a session launcher. Provide an image URL,
+ such as one from https://hub.docker.com.
+
+
+
+ Container Image
+
+
(
+
+ )}
+ rules={{ required: watchEnvironmentKind === "CUSTOM" }}
+ />
+ Please provide a container image
+
+
+
+ Advance settings
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/EditLauncherFormContent.tsx b/client/src/features/sessionsV2/components/SessionForm/EditLauncherFormContent.tsx
new file mode 100644
index 0000000000..088e78d48c
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/EditLauncherFormContent.tsx
@@ -0,0 +1,254 @@
+/*!
+ * 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, useMemo, useState } from "react";
+import {
+ Control,
+ Controller,
+ FieldErrors,
+ FieldNamesMarkedBoolean,
+ UseFormSetValue,
+ UseFormWatch,
+} from "react-hook-form";
+import { Collapse, Input, Label, ListGroup } from "reactstrap";
+import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert";
+import ChevronFlippedIcon from "../../../../components/icons/ChevronFlippedIcon";
+import { Loader } from "../../../../components/Loader";
+import { orderEnvironments } from "../../session.utils";
+import { useGetSessionEnvironmentsQuery } from "../../sessionsV2.api";
+import { SessionLauncherForm } from "../../sessionsV2.types";
+import { AdvanceSettingsFields } from "./AdvanceSettingsFields";
+import { EnvironmentKindField } from "./EnvironmentKindField";
+import { SessionEnvironmentItem } from "./SessionEnvironmentItem";
+
+interface SessionLauncherFormContentProps {
+ control: Control;
+ errors: FieldErrors;
+ watch: UseFormWatch;
+ touchedFields: Partial<
+ Readonly>
+ >;
+}
+
+interface EditLauncherFormContentProps extends SessionLauncherFormContentProps {
+ environmentId?: string;
+ setValue: UseFormSetValue;
+}
+export default function EditLauncherFormContent({
+ control,
+ errors,
+ watch,
+ touchedFields,
+ environmentId,
+ setValue,
+}: EditLauncherFormContentProps) {
+ const {
+ data: environments,
+ error,
+ isLoading,
+ } = useGetSessionEnvironmentsQuery();
+ const watchEnvironmentKind = watch("environment_kind");
+ const [isAdvanceSettingOpen, setIsAdvanceSettingsOpen] = useState(false);
+ const toggleIsOpen = useCallback(
+ () =>
+ setIsAdvanceSettingsOpen((isAdvanceSettingOpen) => !isAdvanceSettingOpen),
+ []
+ );
+
+ const orderEnvironment = useMemo(
+ () => orderEnvironments(environments, environmentId),
+ [environments, environmentId]
+ );
+
+ return (
+
+
+
+ Session launcher name
+
+
(
+
+ )}
+ rules={{ required: true }}
+ />
+ Please provide a name
+
+
+
+ Session launcher description
+
+ (
+
+ )}
+ />
+
+
+
+ {/*
Environment Type
*/}
+ {/*
(*/}
+ {/* */}
+ {/* )}*/}
+ {/*/>*/}
+
+
+
Environment
+ {isLoading && (
+
+
+ Loading environments...
+
+ )}
+ {error && (
+ <>
+
Cannot load environments
+
+ >
+ )}
+ {orderEnvironment && orderEnvironment.length > 0 && (
+
(
+ <>
+
+ {orderEnvironment.map((environment) => (
+
+ ))}
+
+
+
+ Please choose an environment
+
+ >
+ )}
+ rules={{
+ required: watchEnvironmentKind === "GLOBAL",
+ }}
+ />
+ )}
+
+
+
+
+ Container Image
+
+
(
+
+ )}
+ rules={{ required: watchEnvironmentKind === "CUSTOM" }}
+ />
+ Please provide a container image
+
+ {watchEnvironmentKind === "CUSTOM" && (
+ <>
+
+
+ Advance settings{" "}
+
+
+
+
+
+
+ >
+ )}
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/EnvironmentField.tsx b/client/src/features/sessionsV2/components/SessionForm/EnvironmentField.tsx
new file mode 100644
index 0000000000..47818dc51f
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/EnvironmentField.tsx
@@ -0,0 +1,77 @@
+/*!
+ * 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 {
+ Control,
+ FieldErrors,
+ FieldNamesMarkedBoolean,
+ UseFormSetValue,
+ UseFormWatch,
+} from "react-hook-form";
+import { SessionLauncherForm } from "../../sessionsV2.types";
+import { CustomEnvironmentFields } from "./CustomEnvironmentFields";
+import { EnvironmentKindField } from "./EnvironmentKindField";
+import { GlobalEnvironmentFields } from "./GlobalEnvironmentFields";
+
+export interface EnvironmentFieldsProps {
+ control: Control;
+ errors: FieldErrors;
+ watch: UseFormWatch;
+ touchedFields: Partial<
+ Readonly>
+ >;
+ setValue: UseFormSetValue;
+}
+
+export function EnvironmentFields({
+ watch,
+ control,
+ errors,
+ touchedFields,
+ setValue,
+}: EnvironmentFieldsProps) {
+ const watchEnvironmentKind = watch("environment_kind");
+ return (
+
+
+ 1 of 2. Define environment
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/EnvironmentKindField.tsx b/client/src/features/sessionsV2/components/SessionForm/EnvironmentKindField.tsx
new file mode 100644
index 0000000000..177cd5a7b7
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/EnvironmentKindField.tsx
@@ -0,0 +1,67 @@
+/*!
+ * 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 { Control, Controller, UseFormSetValue } from "react-hook-form";
+import { Button, ButtonGroup } from "reactstrap";
+import { SessionLauncherForm } from "../../sessionsV2.types";
+
+interface EnvironmentKindField {
+ control: Control;
+ setValue: UseFormSetValue;
+}
+export function EnvironmentKindField({
+ control,
+ setValue,
+}: EnvironmentKindField) {
+ return (
+ (
+
+
+ setValue("environment_kind", "GLOBAL")}
+ className={cx(
+ field.value === "GLOBAL"
+ ? "text-white bg-primary"
+ : "text-primary bg-white"
+ )}
+ >
+ Global environment
+
+ setValue("environment_kind", "CUSTOM")}
+ className={cx(
+ field.value === "CUSTOM"
+ ? "text-white bg-primary"
+ : "text-primary bg-white"
+ )}
+ >
+ Custom Environment
+
+
+
+ )}
+ />
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/GlobalEnvironmentFields.tsx b/client/src/features/sessionsV2/components/SessionForm/GlobalEnvironmentFields.tsx
new file mode 100644
index 0000000000..864e312640
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/GlobalEnvironmentFields.tsx
@@ -0,0 +1,101 @@
+/*!
+ * 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 { Controller } from "react-hook-form";
+import { Input, ListGroup } from "reactstrap";
+import { WarnAlert } from "../../../../components/Alert";
+import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert";
+import { Loader } from "../../../../components/Loader";
+import { useGetSessionEnvironmentsQuery } from "../../sessionsV2.api";
+import { EnvironmentFieldsProps } from "./EnvironmentField";
+import { SessionEnvironmentItem } from "./SessionEnvironmentItem";
+
+export function GlobalEnvironmentFields({
+ watch,
+ control,
+ touchedFields,
+ errors,
+}: EnvironmentFieldsProps) {
+ const {
+ data: environments,
+ error,
+ isLoading,
+ } = useGetSessionEnvironmentsQuery();
+ const watchEnvironmentKind = watch("environment_kind");
+
+ return (
+
+
+ Reuse an environment already defined on RenkuLab to create an
+ interactive session for your project
+
+ {isLoading && (
+
+
+ Loading environments...
+
+ )}
+ {error && (
+ <>
+
Cannot load environments
+
+ >
+ )}
+ {environments && environments.length === 0 && (
+
+ No existing environments available. Please contact an admin to update
+ this list.
+
+ )}
+ {environments && environments.length > 0 && (
+
(
+
+
+
+ Please choose an environment
+
+
+ {environments.map((environment) => (
+
+ ))}
+
+
+ )}
+ rules={{
+ required: watchEnvironmentKind === "GLOBAL",
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx b/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx
new file mode 100644
index 0000000000..8c185efd0c
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx
@@ -0,0 +1,125 @@
+/*!
+ * 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 { useMemo } from "react";
+import {
+ Control,
+ FieldErrors,
+ UseFormSetValue,
+ Controller,
+} from "react-hook-form";
+import { SingleValue } from "react-select";
+import { Label, Input } from "reactstrap";
+import { WarnAlert } from "../../../../components/Alert";
+import { useGetResourcePoolsQuery } from "../../../dataServices/computeResources.api";
+import { ResourceClass } from "../../../dataServices/dataServices.types";
+import { SessionClassSelectorV2 } from "../../../session/components/options/SessionClassOption";
+import { SessionLauncherForm } from "../../sessionsV2.types";
+
+interface LauncherDetailsFieldsProps {
+ control: Control;
+ errors: FieldErrors;
+ setValue: UseFormSetValue;
+}
+export function LauncherDetailsFields({
+ setValue,
+ control,
+ errors,
+}: LauncherDetailsFieldsProps) {
+ const { data: resourcePools, isLoading: isLoadingResourcesPools } =
+ useGetResourcePoolsQuery({});
+
+ const onChangeResourceClass = (resourceClass: SingleValue) => {
+ if (resourceClass) setValue("resourceClass", resourceClass);
+ };
+
+ const defaultSessionClass = useMemo(
+ () =>
+ resourcePools
+ ?.filter((pool) => pool.default)
+ .flatMap((pool) => pool.classes)
+ .find((c) => c.default) ??
+ resourcePools?.find(() => true)?.classes[0] ??
+ undefined,
+ [resourcePools]
+ );
+
+ return (
+
+
+ 2 of 2. Define launcher details
+
+
+
+ Session launcher name
+
+
(
+
+ )}
+ rules={{ required: true }}
+ />
+ Please provide a name
+
+
+
+ Session launcher compute resources
+
+ {!isLoadingResourcesPools &&
+ resourcePools &&
+ resourcePools?.length > 0 ? (
+
(
+ <>
+
+ {errors?.resourceClass && (
+
+ Please provide a resource class
+
+ )}
+ >
+ )}
+ rules={{ required: true }}
+ />
+ ) : (
+
+ There are no one resource pool available to create a session
+
+ )}
+
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/SessionEnvironmentItem.tsx b/client/src/features/sessionsV2/components/SessionForm/SessionEnvironmentItem.tsx
new file mode 100644
index 0000000000..84bf619fe6
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/SessionEnvironmentItem.tsx
@@ -0,0 +1,134 @@
+/*!
+ * 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 { Globe2 } from "react-bootstrap-icons";
+import {
+ Control,
+ Controller,
+ ControllerRenderProps,
+ FieldErrors,
+ FieldNamesMarkedBoolean,
+} from "react-hook-form";
+import { SingleValue } from "react-select";
+import { Card, CardBody, Input, Label, ListGroupItem } from "reactstrap";
+import { TimeCaption } from "../../../../components/TimeCaption";
+import {
+ ResourceClass,
+ ResourcePool,
+} from "../../../dataServices/dataServices.types";
+import { SessionClassSelectorV2 } from "../../../session/components/options/SessionClassOption";
+import {
+ SessionEnvironment,
+ SessionLauncherForm,
+} from "../../sessionsV2.types";
+
+interface SessionEnvironmentItemProps {
+ environment: SessionEnvironment;
+ field: ControllerRenderProps;
+ touchedFields: Partial<
+ Readonly>
+ >;
+ resourcePools?: ResourcePool[];
+ isLoadingResourcesPools?: boolean;
+ onChangeResourceClass?: (resourceClass: SingleValue) => void;
+ errors: FieldErrors;
+ control: Control;
+ defaultSessionClass?: ResourceClass;
+}
+
+export function SessionEnvironmentItem({
+ environment,
+ control,
+ defaultSessionClass,
+ field,
+ resourcePools,
+ isLoadingResourcesPools,
+ onChangeResourceClass,
+ errors,
+}: SessionEnvironmentItemProps) {
+ const { creation_date, id, name, description } = environment;
+ const isSelected = field.value === id;
+
+ const selector = !isLoadingResourcesPools &&
+ resourcePools &&
+ resourcePools?.length > 0 && (
+
+ (
+
+ Compute resources
+
+ {errors.resourceClass && (
+
+ Select compute resource to continue{" "}
+
+ )}
+
+ )}
+ rules={{ required: true }}
+ />
+
+ );
+
+ return (
+
+
+
+
+ {name}
+
+
+ Global environment
+
+ {description ? description
: null}
+
+
+
+
+ {isSelected && selector}
+
+
+ );
+}
diff --git a/client/src/features/sessionsV2/components/SessionForm/SessionLauncherBreadcrumbNavbar.tsx b/client/src/features/sessionsV2/components/SessionForm/SessionLauncherBreadcrumbNavbar.tsx
new file mode 100644
index 0000000000..5fe5264bf0
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionForm/SessionLauncherBreadcrumbNavbar.tsx
@@ -0,0 +1,69 @@
+/*!
+ * 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 { Breadcrumb, BreadcrumbItem, Button } from "reactstrap";
+
+interface SessionLauncherBreadcrumbNavbarProps {
+ setStep: (newState: "environment" | "launcherDetails") => void;
+ step: "environment" | "launcherDetails";
+ readyToGoNext: boolean;
+}
+export const SessionLauncherBreadcrumbNavbar = ({
+ setStep,
+ step,
+ readyToGoNext,
+}: SessionLauncherBreadcrumbNavbarProps) => {
+ return (
+
+
+ {
+ setStep("environment");
+ }}
+ >
+ {" "}
+ 1. Define Environment
+
+
+
+ {
+ setStep("launcherDetails");
+ }}
+ >
+ {" "}
+ 2. Define launcher details
+
+
+
+ );
+};
diff --git a/client/src/features/sessionsV2/components/SessionModals/AddSession.tsx b/client/src/features/sessionsV2/components/SessionModals/AddSession.tsx
deleted file mode 100644
index d0e767f7e8..0000000000
--- a/client/src/features/sessionsV2/components/SessionModals/AddSession.tsx
+++ /dev/null
@@ -1,364 +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 { skipToken } from "@reduxjs/toolkit/query";
-import cx from "classnames";
-import { useCallback, useEffect, useState } from "react";
-import { Box2, Boxes, PlayCircle, PlusLg, XLg } from "react-bootstrap-icons";
-import { useForm } from "react-hook-form";
-import { useParams } from "react-router-dom-v5-compat";
-import {
- Button,
- Col,
- Form,
- Modal,
- ModalBody,
- ModalFooter,
- ModalHeader,
- Row,
-} from "reactstrap";
-import { Loader } from "../../../../components/Loader";
-import { RtkOrNotebooksError } from "../../../../components/errors/RtkErrorAlert";
-import { useGetProjectsByNamespaceAndSlugQuery } from "../../../projectsV2/api/projectV2.enhanced-api";
-import {
- CustomEnvFormContent,
- ExistingEnvFormContent,
- SessionLauncherForm,
-} from "../../SessionLauncherFormContent";
-import { useAddSessionLauncherMutation } from "../../sessionsV2.api";
-import { SessionLauncherEnvironment } from "../../sessionsV2.types";
-import { InfoAlert } from "../../../../components/Alert";
-
-interface AddSessionLauncherModalProps {
- isOpen: boolean;
- toggle: () => void;
-}
-function AddSessionCustomImageModal({
- isOpen,
- toggle,
-}: AddSessionLauncherModalProps) {
- const { namespace, slug } = useParams<{ namespace: string; slug: string }>();
- const { data: project } = useGetProjectsByNamespaceAndSlugQuery(
- namespace && slug ? { namespace, slug } : skipToken
- );
- const projectId = project?.id;
-
- const [addSessionLauncher, result] = useAddSessionLauncherMutation();
-
- const {
- control,
- formState: { errors },
- handleSubmit,
- reset,
- setValue,
- } = useForm({
- defaultValues: {
- name: "",
- environment_kind: "container_image",
- container_image: "",
- default_url: "",
- },
- });
- const onSubmit = useCallback(
- (data: SessionLauncherForm) => {
- const { default_url, name } = data;
- const environment: SessionLauncherEnvironment = {
- environment_kind: "container_image",
- container_image: data.container_image,
- };
- addSessionLauncher({
- project_id: projectId ?? "",
- resource_class_id: data.resourceClass.id,
- name,
- default_url: default_url.trim() ? default_url : undefined,
- ...environment,
- });
- },
- [addSessionLauncher, projectId]
- );
-
- useEffect(() => {
- if (!result.isSuccess) {
- return;
- }
- toggle();
- }, [result.isSuccess, toggle]);
-
- useEffect(() => {
- if (!isOpen) {
- reset();
- result.reset();
- }
- }, [isOpen, reset, result]);
-
- return (
-
-
-
- );
-}
-function AddSessionExistingEnvModal({
- isOpen,
- toggle,
-}: AddSessionLauncherModalProps) {
- const { namespace, slug } = useParams<{ namespace: string; slug: string }>();
- const { data: project } = useGetProjectsByNamespaceAndSlugQuery(
- namespace && slug ? { namespace, slug } : skipToken
- );
- const projectId = project?.id;
-
- const [addSessionLauncher, result] = useAddSessionLauncherMutation();
-
- const {
- control,
- formState: { errors, touchedFields },
- handleSubmit,
- reset,
- watch,
- setValue,
- resetField,
- } = useForm({
- defaultValues: {
- name: "",
- environment_kind: "global_environment",
- environment_id: "",
- default_url: "",
- },
- });
- const onSubmit = useCallback(
- (data: SessionLauncherForm) => {
- const { default_url, name, resourceClass } = data;
- const environment: SessionLauncherEnvironment = {
- environment_kind: "global_environment",
- environment_id: data.environment_id,
- };
- addSessionLauncher({
- project_id: projectId ?? "",
- resource_class_id: resourceClass.id,
- name,
- default_url: default_url.trim() ? default_url : undefined,
- ...environment,
- });
- },
- [addSessionLauncher, projectId]
- );
-
- useEffect(() => {
- if (!result.isSuccess) {
- return;
- }
- toggle();
- }, [result.isSuccess, toggle]);
-
- useEffect(() => {
- if (!isOpen) {
- reset();
- result.reset();
- }
- }, [isOpen, reset, result]);
-
- return (
-
-
-
- Select an existing environment
-
-
-
-
-
-
-
- Cancel
-
-
- {result.isLoading ? (
- <>
-
- Adding Session launcher
- >
- ) : (
- <>
-
- Add session launcher
- >
- )}
-
-
-
- );
-}
-
-export function Step1AddSessionModal({
- toggleModal,
- isOpen,
-}: {
- toggleModal: () => void;
- isOpen: boolean;
-}) {
- const [isOpenCustomEnv, setIsOpenCustomEnv] = useState(false);
- const [isOpenExistingEnv, setIsOpenExistingEnv] = useState(false);
- const toggleCustom = useCallback(() => {
- setIsOpenCustomEnv((open) => !open);
- }, []);
- const toggleExisting = useCallback(() => {
- setIsOpenExistingEnv((open) => !open);
- }, []);
- const goNextStep = useCallback(
- (isCustom: boolean) => {
- if (isCustom) {
- toggleCustom();
- toggleModal();
- } else {
- toggleExisting();
- toggleModal();
- }
- },
- [toggleModal, toggleCustom, toggleExisting]
- );
-
- return (
- <>
-
-
-
- Add session launcher
-
-
-
- Define an interactive environment in which to do your work and share
- it with others.
-
-
- Everyone who can see the project can launch a session. Running
- sessions are only accessible by the person who launched it.
-
-
- All project code repositories and data sources will be automatically
- mounted in your session.
-
-
-
- goNextStep(false)}
- >
-
- Select an existing environment
-
-
-
- goNextStep(true)}
- >
-
- Provide a custom image
-
-
-
-
-
-
-
- >
- );
-}
diff --git a/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx b/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx
new file mode 100644
index 0000000000..a99c624ed2
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx
@@ -0,0 +1,256 @@
+/*!
+ * 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 { skipToken } from "@reduxjs/toolkit/query";
+import cx from "classnames";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { ArrowRight, CheckLg, XLg } from "react-bootstrap-icons";
+import { useForm } from "react-hook-form";
+import { useParams } from "react-router-dom-v5-compat";
+import {
+ Button,
+ Form,
+ Modal,
+ ModalBody,
+ ModalFooter,
+ ModalHeader,
+} from "reactstrap";
+import { SuccessAlert } from "../../../../components/Alert";
+import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert";
+import { Loader } from "../../../../components/Loader";
+import { useGetProjectsByNamespaceAndSlugQuery } from "../../../projectsV2/api/projectV2.enhanced-api";
+import {
+ DEFAULT_PORT,
+ DEFAULT_URL,
+ getFormattedEnvironmentValues,
+} from "../../session.utils";
+import {
+ useAddSessionLauncherMutation,
+ useGetSessionEnvironmentsQuery,
+} from "../../sessionsV2.api";
+import { SessionLauncherForm } from "../../sessionsV2.types";
+import { EnvironmentFields } from "../SessionForm/EnvironmentField";
+import { LauncherDetailsFields } from "../SessionForm/LauncherDetailsFields";
+import { SessionLauncherBreadcrumbNavbar } from "../SessionForm/SessionLauncherBreadcrumbNavbar";
+
+interface NewSessionLauncherModalProps {
+ isOpen: boolean;
+ toggle: () => void;
+}
+
+export default function NewSessionLauncherModal({
+ isOpen,
+ toggle,
+}: NewSessionLauncherModalProps) {
+ const [step, setStep] = useState<"environment" | "launcherDetails">(
+ "environment"
+ );
+ const { namespace, slug } = useParams<{ namespace: string; slug: string }>();
+ const { data: environments } = useGetSessionEnvironmentsQuery();
+ const [addSessionLauncher, result] = useAddSessionLauncherMutation();
+ const { data: project } = useGetProjectsByNamespaceAndSlugQuery(
+ namespace && slug ? { namespace, slug } : skipToken
+ );
+ const projectId = project?.id;
+
+ const {
+ control,
+ formState: { errors, isDirty, touchedFields },
+ handleSubmit,
+ reset,
+ setValue,
+ watch,
+ trigger,
+ } = useForm({
+ defaultValues: {
+ name: "",
+ environment_kind: "GLOBAL",
+ environment_id: "",
+ container_image: "",
+ default_url: DEFAULT_URL,
+ port: DEFAULT_PORT,
+ },
+ });
+
+ const watchEnvironmentId = watch("environment_id");
+ const watchEnvironmentCustomImage = watch("container_image");
+ const watchEnvironmentKind = watch("environment_kind");
+
+ const isEnvironmentDefined = useMemo(() => {
+ return (
+ (watchEnvironmentKind === "GLOBAL" && !!watchEnvironmentId) ||
+ (watchEnvironmentKind === "CUSTOM" &&
+ watchEnvironmentCustomImage?.length > 0)
+ );
+ }, [watchEnvironmentId, watchEnvironmentCustomImage, watchEnvironmentKind]);
+
+ const onNext = useCallback(() => {
+ trigger(["environment_id", "container_image"]);
+
+ if (isDirty && isEnvironmentDefined) setStep("launcherDetails");
+ }, [isDirty, setStep, trigger, isEnvironmentDefined]);
+
+ const onCancel = useCallback(() => {
+ setStep("environment");
+ reset();
+ toggle();
+ }, [reset, toggle, setStep]);
+
+ const onSubmit = useCallback(
+ (data: SessionLauncherForm) => {
+ const { name, resourceClass } = data;
+ const environment = getFormattedEnvironmentValues(data);
+ addSessionLauncher({
+ project_id: projectId ?? "",
+ resource_class_id: resourceClass.id,
+ name,
+ environment,
+ });
+ },
+ [projectId, addSessionLauncher]
+ );
+
+ useEffect(() => {
+ trigger(["container_image"]);
+ }, [watchEnvironmentCustomImage, trigger]);
+
+ useEffect(() => {
+ trigger(["environment_id"]);
+ if (environments?.length) {
+ const environmentSelected = environments.find(
+ (env) => env.id === watchEnvironmentId
+ );
+ setValue("name", environmentSelected?.name ?? "");
+ }
+ }, [watchEnvironmentId, setValue, environments, trigger]);
+
+ useEffect(() => {
+ if (environments == null) {
+ return;
+ }
+ if (environments.length == 0) {
+ setValue("environment_kind", "CUSTOM");
+ }
+ }, [environments, setValue]);
+
+ useEffect(() => {
+ if (!isOpen) {
+ setStep("environment");
+ reset();
+ result.reset();
+ }
+ }, [isOpen, reset, result, setStep]);
+
+ return (
+
+ Add session launcher
+
+ {result.isSuccess ? (
+
+ ) : (
+
+ {step === "environment" && (
+ <>
+
+ Define an interactive environment in which to do your work and
+ share it with others.
+
+ >
+ )}
+
+
+ )}
+
+
+ {!result.isSuccess && (
+
+
+
+ )}
+
+
+ {result.isSuccess ? "Close" : "Cancel"}
+
+ {!result.isSuccess && step === "environment" && (
+
+ Next
+
+ )}
+ {!result.isSuccess && step === "launcherDetails" && (
+
+ {result.isLoading ? (
+
+ ) : (
+
+ )}
+ Add session launcher
+
+ )}
+
+
+ );
+}
+
+const ConfirmationCreate = () => {
+ return (
+
+ Session launcher was created successfully!
+
+ );
+};
diff --git a/client/src/features/sessionsV2/components/SessionModals/UpdateSessionLauncherModal.tsx b/client/src/features/sessionsV2/components/SessionModals/UpdateSessionLauncherModal.tsx
new file mode 100644
index 0000000000..f80af0defb
--- /dev/null
+++ b/client/src/features/sessionsV2/components/SessionModals/UpdateSessionLauncherModal.tsx
@@ -0,0 +1,174 @@
+/*!
+ * 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, useEffect, useMemo } from "react";
+import { CheckLg, XLg } from "react-bootstrap-icons";
+import { useForm } from "react-hook-form";
+import {
+ Button,
+ Form,
+ Modal,
+ ModalBody,
+ ModalFooter,
+ ModalHeader,
+} from "reactstrap";
+import { SuccessAlert } from "../../../../components/Alert";
+import { Loader } from "../../../../components/Loader";
+import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert";
+import {
+ getFormattedEnvironmentValues,
+ getLauncherDefaultValues,
+} from "../../session.utils";
+import {
+ useGetSessionEnvironmentsQuery,
+ useUpdateSessionLauncherMutation,
+} from "../../sessionsV2.api";
+import { SessionLauncherForm, SessionLauncher } from "../../sessionsV2.types";
+import EditLauncherFormContent from "../SessionForm/EditLauncherFormContent";
+
+interface UpdateSessionLauncherModalProps {
+ isOpen: boolean;
+ launcher: SessionLauncher;
+ toggle: () => void;
+}
+
+export default function UpdateSessionLauncherModal({
+ isOpen,
+ launcher,
+ toggle,
+}: UpdateSessionLauncherModalProps) {
+ const { data: environments } = useGetSessionEnvironmentsQuery();
+ const [updateSessionLauncher, result] = useUpdateSessionLauncherMutation();
+ const defaultValues = useMemo(
+ () => getLauncherDefaultValues(launcher),
+ [launcher]
+ );
+
+ const {
+ control,
+ formState: { errors, isDirty, touchedFields },
+ handleSubmit,
+ reset,
+ setValue,
+ watch,
+ } = useForm({
+ defaultValues,
+ });
+ const onSubmit = useCallback(
+ (data: SessionLauncherForm) => {
+ const { description, name } = data;
+ const environment = getFormattedEnvironmentValues(data);
+ updateSessionLauncher({
+ launcherId: launcher.id,
+ name,
+ description: description.trim() ? description : undefined,
+ environment,
+ });
+ },
+ [launcher.id, updateSessionLauncher]
+ );
+
+ useEffect(() => {
+ if (environments == null) {
+ return;
+ }
+ if (environments.length == 0) {
+ setValue("environment_kind", "CUSTOM");
+ }
+ }, [environments, setValue]);
+
+ useEffect(() => {
+ if (!isOpen) {
+ reset();
+ result.reset();
+ }
+ }, [isOpen, reset, result]);
+
+ useEffect(() => {
+ reset(defaultValues);
+ }, [launcher, reset, defaultValues]);
+
+ return (
+
+
+ Edit session launcher {launcher.name}
+
+
+ {result.isSuccess ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {result.isSuccess ? "Close" : "Cancel"}
+
+ {!result.isSuccess && (
+
+ {result.isLoading ? (
+
+ ) : (
+
+ )}
+ Update session launcher
+
+ )}
+
+
+ );
+}
+
+const ConfirmationUpdate = () => {
+ return (
+
+ Session launcher updated successfully!
+
+ The changes will take effect the next time you launch a session with
+ this launcher. Current sessions will not be affected.
+
+
+ );
+};
diff --git a/client/src/features/sessionsV2/session.utils.ts b/client/src/features/sessionsV2/session.utils.ts
index f4f277320c..eec94584d7 100644
--- a/client/src/features/sessionsV2/session.utils.ts
+++ b/client/src/features/sessionsV2/session.utils.ts
@@ -16,30 +16,33 @@
* limitations under the License
*/
import faviconICO from "../../styles/assets/favicon/Favicon.ico";
-import faviconErrorICO from "../../styles/assets/favicon/FaviconError.ico";
-import faviconPauseICO from "../../styles/assets/favicon/FaviconPause.ico";
-import faviconRunningICO from "../../styles/assets/favicon/FaviconRunning.ico";
-import faviconWaitingICO from "../../styles/assets/favicon/FaviconWaiting.ico";
-import { FaviconStatus } from "../display/display.types";
-import { SessionStatusState } from "../session/sessions.types";
-
import faviconSVG from "../../styles/assets/favicon/Favicon.svg";
-import faviconErrorSVG from "../../styles/assets/favicon/FaviconError.svg";
-import faviconPauseSVG from "../../styles/assets/favicon/FaviconPause.svg";
-import faviconRunningSVG from "../../styles/assets/favicon/FaviconRunning.svg";
-import faviconWaitingSVG from "../../styles/assets/favicon/FaviconWaiting.svg";
-
import favicon16px from "../../styles/assets/favicon/Favicon16px.png";
-import faviconError16px from "../../styles/assets/favicon/FaviconError16px.png";
-import faviconPause16px from "../../styles/assets/favicon/FaviconPause16px.png";
-import faviconRunning16px from "../../styles/assets/favicon/FaviconRunning16px.png";
-import faviconWaiting16px from "../../styles/assets/favicon/FaviconWaiting16px.png";
-
import favicon32px from "../../styles/assets/favicon/Favicon32px.png";
+import faviconErrorICO from "../../styles/assets/favicon/FaviconError.ico";
+import faviconErrorSVG from "../../styles/assets/favicon/FaviconError.svg";
+import faviconError16px from "../../styles/assets/favicon/FaviconError16px.png";
import faviconError32px from "../../styles/assets/favicon/FaviconError32px.png";
+import faviconPauseICO from "../../styles/assets/favicon/FaviconPause.ico";
+import faviconPauseSVG from "../../styles/assets/favicon/FaviconPause.svg";
+import faviconPause16px from "../../styles/assets/favicon/FaviconPause16px.png";
import faviconPause32px from "../../styles/assets/favicon/FaviconPause32px.png";
+import faviconRunningICO from "../../styles/assets/favicon/FaviconRunning.ico";
+import faviconRunningSVG from "../../styles/assets/favicon/FaviconRunning.svg";
+import faviconRunning16px from "../../styles/assets/favicon/FaviconRunning16px.png";
import faviconRunning32px from "../../styles/assets/favicon/FaviconRunning32px.png";
+import faviconWaitingICO from "../../styles/assets/favicon/FaviconWaiting.ico";
+import faviconWaitingSVG from "../../styles/assets/favicon/FaviconWaiting.svg";
+import faviconWaiting16px from "../../styles/assets/favicon/FaviconWaiting16px.png";
import faviconWaiting32px from "../../styles/assets/favicon/FaviconWaiting32px.png";
+import { FaviconStatus } from "../display/display.types";
+import { SessionStatusState } from "../session/sessions.types";
+import {
+ SessionEnvironmentList,
+ SessionLauncher,
+ SessionLauncherEnvironmentParams,
+ SessionLauncherForm,
+} from "./sessionsV2.types";
export function getSessionFavicon(
sessionState?: SessionStatusState,
@@ -99,3 +102,99 @@ export const FAVICON_BY_SESSION_STATUS = {
svg: faviconPauseSVG,
},
};
+
+export function getFormCustomValuesDesc() {
+ return {
+ urlPath: `Specify a subpath for your Renku session. By default, the session opens at the path defined by the environment variable \`RENKU_SESION_PATH\`. If you set a subpath (e.g., "foo"), the session will open at \`/foo\`.`,
+ port: `The network port that your application will use to listen for incoming connections.
+Default: \`8080\`.`,
+ workingDirectory: `Set the directory where your session will open. If not specified, Renku uses the Docker image setting. Renku will also create the project inside this directory including any data sources and repositories.`,
+ uid: `The identifier assigned to the user that will run the application. This determines file permissions and ownership.
+Default: \`1000\`.`,
+ gid: `The identifier assigned to the group that will run the application. This helps manage group-based permissions.
+Default: \`1000\`.`,
+ mountDirectory: `Renku will provide persistent storage for your session even when you pause or resume it. Set the location where this storage should be mounted. It should be the same as or a parent of the working directory to avoid data loss. Defaults to the working directory if not specified.`,
+ command: `The command that will be run i.e. will overwrite the image Dockerfile \`ENTRYPOINT\`.`,
+ args: `The arguments that will follow the command, i.e. will overwrite the image Dockerfile \`CMD\`.`,
+ };
+}
+export const DEFAULT_URL = "/";
+export const DEFAULT_PORT = 8888;
+
+export function orderEnvironments(
+ environments?: SessionEnvironmentList,
+ environmentId?: string
+): SessionEnvironmentList | undefined {
+ if (!environments || !environmentId) return environments;
+ const targetEnvironment = environments.find(
+ (env) => env.id === environmentId
+ );
+
+ if (!targetEnvironment) {
+ return environments;
+ }
+ const otherEnvironments = environments.filter(
+ (env) => env.id !== environmentId
+ );
+ return [targetEnvironment, ...otherEnvironments];
+}
+
+export function getFormattedEnvironmentValues(
+ data: SessionLauncherForm
+): SessionLauncherEnvironmentParams {
+ const {
+ container_image,
+ default_url,
+ name,
+ port,
+ working_directory,
+ uid,
+ gid,
+ mount_directory,
+ environment_id,
+ environment_kind,
+ command,
+ args,
+ } = data;
+ return environment_kind === "GLOBAL"
+ ? {
+ id: environment_id,
+ }
+ : {
+ environment_kind: "CUSTOM",
+ container_image,
+ name,
+ default_url: default_url.trim() ? default_url : DEFAULT_URL,
+ port,
+ working_directory,
+ mount_directory,
+ uid,
+ gid,
+ command: command?.length > 0 ? [`${command}`] : undefined,
+ args: args?.length > 0 ? [`${args}`] : undefined,
+ };
+}
+
+export function getLauncherDefaultValues(launcher: SessionLauncher) {
+ return {
+ name: launcher.name,
+ description: launcher.description ?? "",
+ environment_kind: launcher.environment.environment_kind,
+ environment_id:
+ launcher.environment.environment_kind === "GLOBAL"
+ ? launcher.environment.id
+ : "",
+ container_image:
+ launcher.environment.environment_kind === "CUSTOM"
+ ? launcher.environment.container_image
+ : "",
+ default_url: launcher.environment.default_url ?? DEFAULT_URL,
+ port: launcher.environment.port,
+ working_directory: launcher.environment.working_directory,
+ mount_directory: launcher.environment.mount_directory,
+ uid: launcher.environment.uid,
+ gid: launcher.environment.gid,
+ command: launcher.environment.command,
+ args: launcher.environment.args,
+ };
+}
diff --git a/client/src/features/sessionsV2/sessionsV2.types.ts b/client/src/features/sessionsV2/sessionsV2.types.ts
index 12f10bd97d..21d009b233 100644
--- a/client/src/features/sessionsV2/sessionsV2.types.ts
+++ b/client/src/features/sessionsV2/sessionsV2.types.ts
@@ -16,6 +16,8 @@
* limitations under the License.
*/
+import { ResourceClass } from "../dataServices/dataServices.types";
+
export interface SessionEnvironment {
container_image: string;
creation_date: string;
@@ -32,22 +34,46 @@ export type SessionLauncher = {
project_id: string;
name: string;
creation_date: string;
- default_url?: string;
description?: string;
resource_class_id?: number;
- environment_kind: EnvironmentKind;
-} & SessionLauncherEnvironment;
+ environment: SessionLauncherEnvironment;
+};
-export type EnvironmentKind = "global_environment" | "container_image";
+export type EnvironmentKind = "GLOBAL" | "CUSTOM";
+
+export type SessionLauncherEnvironment = {
+ id?: string;
+ name: string;
+ description?: string;
+ container_image: string;
+ default_url?: string;
+ uid?: number;
+ gid?: number;
+ working_directory?: string;
+ mount_directory?: string;
+ port?: number;
+ environment_kind: EnvironmentKind;
+ command?: string[];
+ args?: string[];
+};
-export type SessionLauncherEnvironment =
+export type SessionLauncherEnvironmentParams =
| {
- environment_kind: Extract;
- environment_id: string;
+ id: string;
}
| {
- environment_kind: Extract;
+ name: string;
+ description?: string;
container_image: string;
+ default_url?: string;
+ uid?: number;
+ gid?: number;
+ working_directory?: string;
+ mount_directory?: string;
+ port?: number;
+ environment_kind: EnvironmentKind;
+ command?: string[];
+ args?: string[];
};
export type SessionLauncherList = SessionLauncher[];
@@ -57,25 +83,38 @@ export interface GetProjectSessionLaunchersParams {
}
export type AddSessionLauncherParams = {
- default_url?: string;
description?: string;
name: string;
project_id: string;
resource_class_id?: number;
- environment_kind: EnvironmentKind;
-} & SessionLauncherEnvironment;
+ environment: SessionLauncherEnvironmentParams;
+};
export interface UpdateSessionLauncherParams {
launcherId?: string;
- default_url?: string;
description?: string;
name?: string;
- environment_kind?: EnvironmentKind;
- environment_id?: string;
resource_class_id?: number;
- container_image?: string;
+ environment?: SessionLauncherEnvironmentParams;
}
export interface DeleteSessionLauncherParams {
launcherId: string;
}
+
+export interface SessionLauncherForm {
+ name: string;
+ container_image: string;
+ description: string;
+ default_url: string;
+ environment_kind: EnvironmentKind;
+ environment_id: string;
+ resourceClass: ResourceClass;
+ port: number;
+ working_directory: string;
+ uid: number;
+ gid: number;
+ mount_directory: string;
+ command: string[];
+ args: string[];
+}
diff --git a/client/src/features/sessionsV2/useSessionLaunchState.hook.ts b/client/src/features/sessionsV2/useSessionLaunchState.hook.ts
index 5de80c6cc0..f85c2dde13 100644
--- a/client/src/features/sessionsV2/useSessionLaunchState.hook.ts
+++ b/client/src/features/sessionsV2/useSessionLaunchState.hook.ts
@@ -18,10 +18,8 @@
import { skipToken } from "@reduxjs/toolkit/query";
import { useEffect, useMemo } from "react";
-
import useAppDispatch from "../../utils/customHooks/useAppDispatch.hook";
import useAppSelector from "../../utils/customHooks/useAppSelector.hook";
-
import { useGetResourcePoolsQuery } from "../dataServices/computeResources.api";
import useDataSourceConfiguration from "../ProjectPageV2/ProjectPageContent/DataSources/useDataSourceConfiguration.hook";
import type { Project } from "../projectsV2/api/projectV2.api";
@@ -29,8 +27,7 @@ import { useGetStoragesV2Query } from "../projectsV2/api/storagesV2.api";
import { useGetDockerImageQuery } from "../session/sessions.api";
import { SESSION_CI_PIPELINE_POLLING_INTERVAL_MS } from "../session/startSessionOptions.constants";
import { DockerImageStatus } from "../session/startSessionOptions.types";
-
-import { useGetSessionEnvironmentsQuery } from "./sessionsV2.api";
+import { DEFAULT_URL } from "./session.utils";
import { SessionLauncher } from "./sessionsV2.types";
import startSessionOptionsV2Slice from "./startSessionOptionsV2.slice";
import useSessionResourceClass from "./useSessionResourceClass.hook";
@@ -46,7 +43,7 @@ export default function useSessionLauncherState({
project,
isCustomLaunch,
}: StartSessionFromLauncherProps) {
- const { environment_kind, default_url } = launcher;
+ const { default_url } = launcher.environment;
const {
data: storages,
@@ -64,23 +61,7 @@ export default function useSessionLauncherState({
resourcePools,
});
- const { data: environments } = useGetSessionEnvironmentsQuery(
- environment_kind === "global_environment" ? undefined : skipToken
- );
-
- const environment = useMemo(
- () =>
- launcher.environment_kind === "global_environment" &&
- environments?.find((env) => env.id === launcher.environment_id),
- [environments, launcher]
- );
-
- const containerImage =
- environment_kind === "global_environment" && environment
- ? environment.container_image
- : environment_kind === "global_environment"
- ? "unknown"
- : launcher.container_image;
+ const containerImage = launcher.environment.container_image ?? "";
const startSessionOptionsV2 = useAppSelector(
({ startSessionOptionsV2 }) => startSessionOptionsV2
@@ -123,16 +104,11 @@ export default function useSessionLauncherState({
// Set the default URL
useEffect(() => {
- const defaultUrl = default_url
- ? default_url
- : environment && environment.default_url
- ? environment.default_url
- : "/lab";
-
+ const defaultUrl = default_url ?? DEFAULT_URL;
if (startSessionOptionsV2.defaultUrl !== defaultUrl) {
dispatch(startSessionOptionsV2Slice.actions.setDefaultUrl(defaultUrl));
}
- }, [environment, default_url, dispatch, startSessionOptionsV2.defaultUrl]);
+ }, [default_url, dispatch, startSessionOptionsV2.defaultUrl]);
// Set the image status
useEffect(() => {
diff --git a/tests/cypress/e2e/projectV2setup.spec.ts b/tests/cypress/e2e/projectV2setup.spec.ts
index 49d53d2b4c..c838547b58 100644
--- a/tests/cypress/e2e/projectV2setup.spec.ts
+++ b/tests/cypress/e2e/projectV2setup.spec.ts
@@ -246,4 +246,25 @@ describe("Set up project components", () => {
cy.getDataCy("start-session-button").should("contain.text", "Launch");
});
});
+
+ it("create a launcher ON PROGRESS", () => {
+ // TODO: update e2e tests
+ cy.intercept("/ui-server/api/notebooks/servers*", {
+ body: { servers: {} },
+ }).as("getSessions");
+ fixtures
+ .readProjectV2({ fixture: "projectV2/read-projectV2-empty.json" })
+ .sessionLaunchers()
+ .newLauncher()
+ .editLauncher()
+ .resourcePoolsTest()
+ .getResourceClass()
+ .environments();
+ cy.visit("/v2/projects/user1-uuid/test-2-v2-project");
+ cy.wait("@readProjectV2");
+ cy.wait("@getSessions");
+ cy.wait("@sessionLaunchers");
+ // ADD SESSION CUSTOM IMAGE
+ cy.getDataCy("add-session-launcher").click();
+ });
});
diff --git a/tests/cypress/fixtures/projectV2/session-launchers.json b/tests/cypress/fixtures/projectV2/session-launchers.json
index 56422ad89a..fe08d4e70f 100644
--- a/tests/cypress/fixtures/projectV2/session-launchers.json
+++ b/tests/cypress/fixtures/projectV2/session-launchers.json
@@ -6,6 +6,12 @@
"creation_date": "2024-05-23T09:59:59Z",
"environment_kind": "container_image",
"container_image": "renku/renkulab-py:latest",
- "resource_class_id": 2
+ "resource_class_id": 2,
+ "port": "8080",
+ "workingDirectory": "/",
+ "uid": "1000",
+ "gid": "1000",
+ "mountDirectory": "",
+ "default_url": "/jdbqksdgsahd/bsajhfdjhsdv/jasgdjhf/lab"
}
]