From 88d44e71573ca5215b4d3a261718528d48088816 Mon Sep 17 00:00:00 2001
From: Flora Thiebaut
Date: Tue, 14 Jan 2025 09:32:41 +0100
Subject: [PATCH] feat: configure disk storage for Renku 2.0 sessions (#3463)
Closes #3451.
Add an option to configure disk storage in 3 locations:
* Add launcher
* Modify launcher's resource class
* Custom launch
**Breaking change**: requires update to `renku-data-services`.
---
.../session/components/SessionsList.tsx | 4 +-
.../sessionsV2/SessionView/SessionView.tsx | 76 ++++---
.../SessionForm/LauncherDetailsFields.tsx | 169 ++++++++++----
.../SessionModals/ModifyResourcesLauncher.tsx | 206 +++++++++++++++---
.../SessionModals/NewSessionLauncherModal.tsx | 11 +-
.../SessionModals/SelectResourceClass.tsx | 174 ++++++++++++---
.../src/features/sessionsV2/sessionsV2.api.ts | 2 +-
.../features/sessionsV2/sessionsV2.types.ts | 6 +-
.../useSessionResourceClass.hook.ts | 10 +-
tests/cypress/e2e/projectV2Session.spec.ts | 24 +-
tests/cypress/e2e/projectV2setup.spec.ts | 2 +-
.../support/renkulab-fixtures/newSession.ts | 16 +-
.../support/renkulab-fixtures/projectV2.ts | 2 +-
.../support/renkulab-fixtures/sessions.ts | 12 +-
14 files changed, 538 insertions(+), 176 deletions(-)
diff --git a/client/src/features/session/components/SessionsList.tsx b/client/src/features/session/components/SessionsList.tsx
index b040873521..245426a67f 100644
--- a/client/src/features/session/components/SessionsList.tsx
+++ b/client/src/features/session/components/SessionsList.tsx
@@ -340,7 +340,9 @@ export function SessionRowResourceRequests({
{entries.map(([key, value], index) => (
- {value}
+
+ {value} {(key === "memory" || key === "storage") && "GB "}
+
{key !== "name" && key}
{entries.length - 1 === index ? " " : " | "}
diff --git a/client/src/features/sessionsV2/SessionView/SessionView.tsx b/client/src/features/sessionsV2/SessionView/SessionView.tsx
index 67406a475c..bcc85bb8d1 100644
--- a/client/src/features/sessionsV2/SessionView/SessionView.tsx
+++ b/client/src/features/sessionsV2/SessionView/SessionView.tsx
@@ -265,8 +265,9 @@ export function SessionView({
resourceRequests={{
name: launcherResourceClass.name,
cpu: launcherResourceClass.cpu,
- memory: `${launcherResourceClass.memory}G`,
- storage: `${launcherResourceClass.default_storage}G`,
+ memory: launcherResourceClass.memory,
+ storage:
+ launcher?.disk_storage ?? launcherResourceClass.default_storage,
gpu: launcherResourceClass.gpu,
}}
/>
@@ -374,27 +375,29 @@ export function SessionView({
Default Resource Class
-
-
-
-
-
- Set resource class
-
- >
- }
- requestedPermission="write"
- userPermissions={permissions}
- />
+ {launcher && (
+
+
+
+
+
+ Set resource class
+
+ >
+ }
+ requestedPermission="write"
+ userPermissions={permissions}
+ />
+ )}
{resourceDetails}
{launcherResourceClass && !userLauncherResourceClass && (
@@ -403,12 +406,27 @@ export function SessionView({
You do not have access to this resource class.
)}
-
+ {launcher &&
+ launcherResourceClass &&
+ launcher.disk_storage &&
+ launcher.disk_storage > launcherResourceClass.max_storage && (
+
+
+ The selected disk storage exceeds the maximum value allowed (
+ {launcherResourceClass.max_storage} GB).
+
+ )}
+ {launcher && (
+
+ )}
diff --git a/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx b/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx
index 626b63ab18..6d0876f140 100644
--- a/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx
+++ b/client/src/features/sessionsV2/components/SessionForm/LauncherDetailsFields.tsx
@@ -15,44 +15,39 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import cx from "classnames";
import { useMemo } from "react";
+import { Control, Controller, useWatch } from "react-hook-form";
import {
- Control,
- Controller,
- FieldErrors,
- UseFormSetValue,
-} from "react-hook-form";
-import { SingleValue } from "react-select";
-import { Input, Label } from "reactstrap";
+ FormText,
+ Input,
+ InputGroup,
+ InputGroupText,
+ Label,
+ UncontrolledTooltip,
+} from "reactstrap";
import { WarnAlert } from "../../../../components/Alert";
import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert";
import { Loader } from "../../../../components/Loader";
import { useGetResourcePoolsQuery } from "../../../dataServices/computeResources.api";
-import { ResourceClass } from "../../../dataServices/dataServices.types";
import { SessionClassSelectorV2 } from "../../../session/components/options/SessionClassOption";
import { SessionLauncherForm } from "../../sessionsV2.types";
+import {
+ MIN_SESSION_STORAGE_GB,
+ STEP_SESSION_STORAGE_GB,
+} from "../../../session/startSessionOptions.constants";
interface LauncherDetailsFieldsProps {
- control: Control
;
- errors: FieldErrors;
- setValue: UseFormSetValue;
+ control: Control;
}
-export function LauncherDetailsFields({
- setValue,
- control,
- errors,
-}: LauncherDetailsFieldsProps) {
+export function LauncherDetailsFields({ control }: LauncherDetailsFieldsProps) {
const {
data: resourcePools,
isLoading: isLoadingResourcesPools,
error: resourcePoolsError,
} = useGetResourcePoolsQuery({});
- const onChangeResourceClass = (resourceClass: SingleValue) => {
- if (resourceClass) setValue("resourceClass", resourceClass);
- };
-
const defaultSessionClass = useMemo(
() =>
resourcePools
@@ -64,6 +59,13 @@ export function LauncherDetailsFields({
[resourcePools]
);
+ const watchCurrentSessionClass = useWatch({
+ control,
+ name: "resourceClass",
+ defaultValue: defaultSessionClass,
+ });
+ const watchCurrentDiskStorage = useWatch({ control, name: "diskStorage" });
+
return (
@@ -76,9 +78,9 @@ export function LauncherDetailsFields({
(
+ render={({ field, fieldState: { error } }) => (
0 ? (
- (
- <>
-
- {errors?.resourceClass && (
-
- Please provide a resource class
-
- )}
- >
- )}
- rules={{ required: true }}
- />
+ <>
+ (
+ <>
+
+ {error && (
+
+ Please provide a resource class
+
+ )}
+ >
+ )}
+ rules={{ required: true }}
+ />
+ >
) : (
There are no one resource pool available to create a session
)}
+
+ {watchCurrentSessionClass && (
+
+
+ Disk Storage:{" "}
+
+ {watchCurrentDiskStorage &&
+ watchCurrentDiskStorage !=
+ watchCurrentSessionClass.default_storage ? (
+ <>{watchCurrentDiskStorage} GB>
+ ) : (
+ <>{watchCurrentSessionClass?.default_storage} GB (default)>
+ )}
+
+
+
(
+ <>
+
+ {
+ if (isNaN(event.target.valueAsNumber)) {
+ field.onChange(event.target.value);
+ } else {
+ field.onChange(event.target.valueAsNumber);
+ }
+ }}
+ />
+
+ GB
+
+
+ Gigabytes
+
+
+
+ Default: {watchCurrentSessionClass.default_storage} GB, max:{" "}
+ {watchCurrentSessionClass.max_storage} GB
+
+
+ {error?.message ||
+ "Please provide a valid value for disk storage."}
+
+ >
+ )}
+ rules={{
+ min: {
+ value: MIN_SESSION_STORAGE_GB,
+ message: `Please select a value greater than or equal to ${MIN_SESSION_STORAGE_GB}.`,
+ },
+ max: {
+ value: watchCurrentSessionClass.max_storage,
+ message: `Selected disk storage exceeds maximum allowed value (${watchCurrentSessionClass.max_storage} GB).`,
+ },
+ validate: {
+ integer: (value: unknown) =>
+ value == null ||
+ value === "" ||
+ (!isNaN(parseInt(`${value}`, 10)) &&
+ parseInt(`${value}`, 10) == parseFloat(`${value}`)),
+ },
+ deps: ["resourceClass"],
+ }}
+ />
+
+ )}
);
diff --git a/client/src/features/sessionsV2/components/SessionModals/ModifyResourcesLauncher.tsx b/client/src/features/sessionsV2/components/SessionModals/ModifyResourcesLauncher.tsx
index 9938cc98b4..d897d56035 100644
--- a/client/src/features/sessionsV2/components/SessionModals/ModifyResourcesLauncher.tsx
+++ b/client/src/features/sessionsV2/components/SessionModals/ModifyResourcesLauncher.tsx
@@ -1,13 +1,29 @@
import cx from "classnames";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useMemo } from "react";
import { CheckLg, XLg } from "react-bootstrap-icons";
-import { SingleValue } from "react-select";
-import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap";
+import { Controller, useForm } from "react-hook-form";
+import {
+ Button,
+ FormText,
+ Input,
+ InputGroup,
+ InputGroupText,
+ Modal,
+ ModalBody,
+ ModalFooter,
+ ModalHeader,
+ UncontrolledTooltip,
+} from "reactstrap";
+
import { SuccessAlert } from "../../../../components/Alert";
import { Loader } from "../../../../components/Loader";
import { useGetResourcePoolsQuery } from "../../../dataServices/computeResources.api";
import { ResourceClass } from "../../../dataServices/dataServices.types";
import { SessionClassSelectorV2 } from "../../../session/components/options/SessionClassOption";
+import {
+ MIN_SESSION_STORAGE_GB,
+ STEP_SESSION_STORAGE_GB,
+} from "../../../session/startSessionOptions.constants";
import { useUpdateSessionLauncherMutation } from "../../sessionsV2.api";
import {
ErrorOrNotAvailableResourcePools,
@@ -18,7 +34,8 @@ interface ModifyResourcesLauncherModalProps {
isOpen: boolean;
toggleModal: () => void;
resourceClassId?: number;
- sessionLauncherId?: string;
+ diskStorage?: number;
+ sessionLauncherId: string;
}
export function ModifyResourcesLauncherModal({
@@ -26,6 +43,7 @@ export function ModifyResourcesLauncherModal({
sessionLauncherId,
toggleModal,
resourceClassId,
+ diskStorage,
}: ModifyResourcesLauncherModalProps) {
const [updateSessionLauncher, result] = useUpdateSessionLauncherMutation();
const {
@@ -34,43 +52,92 @@ export function ModifyResourcesLauncherModal({
isError: isErrorResources,
} = useGetResourcePoolsQuery({});
- const [currentSessionClass, setCurrentSessionClass] = useState<
- ResourceClass | undefined
- >(undefined);
-
- const onChange = useCallback((newValue: SingleValue) => {
- if (newValue) {
- setCurrentSessionClass(newValue);
- }
- }, []);
+ const {
+ control,
+ formState: { isDirty },
+ handleSubmit,
+ reset,
+ watch,
+ } = useForm({
+ defaultValues: {
+ diskStorage,
+ },
+ });
- const onModifyResources = useCallback(() => {
- if (currentSessionClass) {
- updateSessionLauncher({
- launcherId: sessionLauncherId,
- resource_class_id: currentSessionClass?.id,
- });
- }
- }, [sessionLauncherId, updateSessionLauncher, currentSessionClass]);
+ const onSubmitInner = useCallback(
+ (data: ModifyResourcesLauncherForm) => {
+ if (data.resourceClass) {
+ const diskStorage =
+ data.diskStorage &&
+ data.diskStorage != data.resourceClass.default_storage
+ ? data.diskStorage
+ : null;
+ updateSessionLauncher({
+ launcherId: sessionLauncherId,
+ resource_class_id: data.resourceClass.id,
+ disk_storage: diskStorage,
+ });
+ }
+ },
+ [sessionLauncherId, updateSessionLauncher]
+ );
+ const onSubmit = useMemo(
+ () => handleSubmit(onSubmitInner),
+ [handleSubmit, onSubmitInner]
+ );
useEffect(() => {
const currentSessionClass = resourcePools
?.flatMap((pool) => pool.classes)
.find((c) => c.id === resourceClassId);
- setCurrentSessionClass(currentSessionClass);
- }, [resourceClassId, resourcePools]);
+ reset({
+ resourceClass: currentSessionClass,
+ diskStorage,
+ });
+ }, [diskStorage, reset, resourceClassId, resourcePools]);
+
+ useEffect(() => {
+ if (!isOpen) {
+ const currentSessionClass = resourcePools
+ ?.flatMap((pool) => pool.classes)
+ .find((c) => c.id === resourceClassId);
+ reset({
+ resourceClass: currentSessionClass,
+ diskStorage,
+ });
+ }
+ }, [diskStorage, isOpen, reset, resourceClassId, resourcePools]);
+
+ const watchCurrentSessionClass = watch("resourceClass");
+ const watchCurrentDiskStorage = watch("diskStorage");
const selector = isLoadingResources ? (
) : !resourcePools || resourcePools.length == 0 || isErrorResources ? (
) : (
- (
+ <>
+
+ {error && (
+
+ {error.message || "Please provide a valid resource class."}
+
+ )}
+ >
+ )}
+ rules={{ required: "Please provide a resource class." }}
/>
);
+
return (
{selector}
+ {watchCurrentSessionClass && (
+
+
+ Disk Storage:{" "}
+
+ {watchCurrentDiskStorage &&
+ watchCurrentDiskStorage !=
+ watchCurrentSessionClass.default_storage ? (
+ <>{watchCurrentDiskStorage} GB>
+ ) : (
+ <>{watchCurrentSessionClass?.default_storage} GB (default)>
+ )}
+
+
+
(
+ <>
+
+ {
+ if (isNaN(event.target.valueAsNumber)) {
+ field.onChange(event.target.value);
+ } else {
+ field.onChange(event.target.valueAsNumber);
+ }
+ }}
+ />
+
+ GB
+
+
+ Gigabytes
+
+
+
+ Default: {watchCurrentSessionClass.default_storage} GB, max:{" "}
+ {watchCurrentSessionClass.max_storage} GB
+
+
+ {error?.message ||
+ "Please provide a valid value for disk storage."}
+
+ >
+ )}
+ rules={{
+ min: {
+ value: MIN_SESSION_STORAGE_GB,
+ message: `Please select a value greater than or equal to ${MIN_SESSION_STORAGE_GB}.`,
+ },
+ max: {
+ value: watchCurrentSessionClass.max_storage,
+ message: `Selected disk storage exceeds maximum allowed value (${watchCurrentSessionClass.max_storage} GB).`,
+ },
+ validate: {
+ integer: (value: unknown) =>
+ value == null ||
+ value === "" ||
+ (!isNaN(parseInt(`${value}`, 10)) &&
+ parseInt(`${value}`, 10) == parseFloat(`${value}`)),
+ },
+ deps: ["resourceClass"],
+ }}
+ />
+
+ )}
@@ -115,11 +256,9 @@ export function ModifyResourcesLauncherModal({
!resourcePools ||
resourcePools.length == 0 ||
isErrorResources ||
- currentSessionClass == null ||
- (resourceClassId != null &&
- resourceClassId === currentSessionClass?.id)
+ !isDirty
}
- onClick={onModifyResources}
+ onClick={onSubmit}
type="submit"
>
{result.isLoading ? (
@@ -133,3 +272,8 @@ export function ModifyResourcesLauncherModal({
);
}
+
+interface ModifyResourcesLauncherForm {
+ resourceClass: ResourceClass | undefined;
+ diskStorage: number | undefined;
+}
diff --git a/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx b/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx
index 33dbbd6092..27a49f4bd3 100644
--- a/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx
+++ b/client/src/features/sessionsV2/components/SessionModals/NewSessionLauncherModal.tsx
@@ -114,10 +114,15 @@ export default function NewSessionLauncherModal({
(data: SessionLauncherForm) => {
const { name, resourceClass } = data;
const environment = getFormattedEnvironmentValues(data);
+ const diskStorage =
+ data.diskStorage && data.diskStorage != resourceClass.default_storage
+ ? data.diskStorage
+ : undefined;
if (environment.success && environment.data)
addSessionLauncher({
project_id: projectId ?? "",
resource_class_id: resourceClass.id,
+ disk_storage: diskStorage,
name,
environment: environment.data,
});
@@ -192,11 +197,7 @@ export default function NewSessionLauncherModal({
/>
)}
{step === "launcherDetails" && (
-
+
)}
diff --git a/client/src/features/sessionsV2/components/SessionModals/SelectResourceClass.tsx b/client/src/features/sessionsV2/components/SessionModals/SelectResourceClass.tsx
index 6a92d7cc7e..6abed19b13 100644
--- a/client/src/features/sessionsV2/components/SessionModals/SelectResourceClass.tsx
+++ b/client/src/features/sessionsV2/components/SessionModals/SelectResourceClass.tsx
@@ -18,11 +18,23 @@
import { skipToken } from "@reduxjs/toolkit/query";
import cx from "classnames";
-import { useCallback, useState } from "react";
+import { useCallback, useMemo } from "react";
import { XLg } from "react-bootstrap-icons";
+import { Controller, useForm } from "react-hook-form";
import { Link } from "react-router-dom-v5-compat";
-import { SingleValue } from "react-select";
-import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "reactstrap";
+import {
+ Button,
+ FormText,
+ Input,
+ InputGroup,
+ InputGroupText,
+ Modal,
+ ModalBody,
+ ModalFooter,
+ ModalHeader,
+ UncontrolledTooltip,
+} from "reactstrap";
+
import {
useGetResourceClassByIdQuery,
useGetResourcePoolsQuery,
@@ -30,6 +42,10 @@ import {
import { ResourceClass } from "../../../dataServices/dataServices.types";
import { SessionRowResourceRequests } from "../../../session/components/SessionsList";
import { SessionClassSelectorV2 } from "../../../session/components/options/SessionClassOption";
+import {
+ MIN_SESSION_STORAGE_GB,
+ STEP_SESSION_STORAGE_GB,
+} from "../../../session/startSessionOptions.constants";
import {
ErrorOrNotAvailableResourcePools,
FetchingResourcePools,
@@ -37,7 +53,7 @@ import {
interface SelectResourceClassModalProps {
isOpen: boolean;
- onContinue: (env: ResourceClass) => void;
+ onContinue: (env: ResourceClass, diskStorage: number | undefined) => void;
projectUrl: string;
resourceClassId?: number;
isCustom: boolean;
@@ -55,34 +71,61 @@ export function SelectResourceClassModal({
isError,
} = useGetResourcePoolsQuery({});
- const [currentSessionClass, setCurrentSessionClass] = useState<
- ResourceClass | undefined
- >(undefined);
-
const { data: launcherClass, isLoading: isLoadingLauncherClass } =
useGetResourceClassByIdQuery(resourceClassId ?? skipToken);
- const onChange = useCallback((newValue: SingleValue) => {
- if (newValue) {
- setCurrentSessionClass(newValue);
- }
- }, []);
+ const {
+ control,
+ formState: { isDirty },
+ handleSubmit,
+ watch,
+ } = useForm();
+
+ const onSubmitInner = useCallback(
+ (data: SelectResourceClassForm) => {
+ if (data.resourceClass) {
+ const diskStorage =
+ data.diskStorage != null &&
+ data.diskStorage != data.resourceClass.default_storage
+ ? data.diskStorage
+ : undefined;
+ onContinue(data.resourceClass, diskStorage);
+ }
+ },
+ [onContinue]
+ );
+ const onSubmit = useMemo(
+ () => handleSubmit(onSubmitInner),
+ [handleSubmit, onSubmitInner]
+ );
- const onClick = useCallback(() => {
- if (currentSessionClass) {
- onContinue(currentSessionClass);
- }
- }, [currentSessionClass, onContinue]);
+ const watchCurrentSessionClass = watch("resourceClass");
+ const watchCurrentDiskStorage = watch("diskStorage");
const selector = isLoading ? (
) : !resourcePools || resourcePools.length == 0 || isError ? (
) : (
- (
+ <>
+
+ {error && (
+
+ {error.message || "Please provide a valid resource class."}
+
+ )}
+ >
+ )}
+ rules={{ required: "Please provide a resource class." }}
/>
);
@@ -116,7 +159,7 @@ export function SelectResourceClassModal({
You do not have access to the default resource class of this session
launcher. Please select one of your available resource classes to
- continue.”
+ continue.
)}
{launcherClass && (
@@ -128,6 +171,80 @@ export function SelectResourceClassModal({
)}
{selector}
+ {watchCurrentSessionClass && (
+
+
+ Disk Storage:{" "}
+
+ {watchCurrentDiskStorage &&
+ watchCurrentDiskStorage !=
+ watchCurrentSessionClass.default_storage ? (
+ <>{watchCurrentDiskStorage} GB>
+ ) : (
+ <>{watchCurrentSessionClass?.default_storage} GB (default)>
+ )}
+
+
+
(
+ <>
+
+ {
+ if (isNaN(event.target.valueAsNumber)) {
+ field.onChange(event.target.value);
+ } else {
+ field.onChange(event.target.valueAsNumber);
+ }
+ }}
+ />
+
+ GB
+
+
+ Gigabytes
+
+
+
+ Default: {watchCurrentSessionClass.default_storage} GB, max:{" "}
+ {watchCurrentSessionClass.max_storage} GB
+
+
+ {error?.message ||
+ "Please provide a valid value for disk storage."}
+
+ >
+ )}
+ rules={{
+ min: {
+ value: MIN_SESSION_STORAGE_GB,
+ message: `Please select a value greater than or equal to ${MIN_SESSION_STORAGE_GB}.`,
+ },
+ max: {
+ value: watchCurrentSessionClass.max_storage,
+ message: `Selected disk storage exceeds maximum allowed value (${watchCurrentSessionClass.max_storage} GB).`,
+ },
+ validate: {
+ integer: (value: unknown) =>
+ value == null ||
+ value === "" ||
+ (!isNaN(parseInt(`${value}`, 10)) &&
+ parseInt(`${value}`, 10) == parseFloat(`${value}`)),
+ },
+ deps: ["resourceClass"],
+ }}
+ />
+
+ )}
Cancel launch
-
+
Continue
);
}
+
+interface SelectResourceClassForm {
+ resourceClass: ResourceClass | undefined;
+ diskStorage: number | undefined;
+}
diff --git a/client/src/features/sessionsV2/sessionsV2.api.ts b/client/src/features/sessionsV2/sessionsV2.api.ts
index 7854de9e72..003793113c 100644
--- a/client/src/features/sessionsV2/sessionsV2.api.ts
+++ b/client/src/features/sessionsV2/sessionsV2.api.ts
@@ -40,7 +40,7 @@ import {
const sessionsV2Api = createApi({
reducerPath: "sessionsV2Api",
baseQuery: fetchBaseQuery({
- baseUrl: "/ui-server/api/data",
+ baseUrl: "/api/data",
}),
tagTypes: ["Environment", "Launcher", "SessionsV2"],
endpoints: (builder) => ({
diff --git a/client/src/features/sessionsV2/sessionsV2.types.ts b/client/src/features/sessionsV2/sessionsV2.types.ts
index 087dddbefc..d111671ae0 100644
--- a/client/src/features/sessionsV2/sessionsV2.types.ts
+++ b/client/src/features/sessionsV2/sessionsV2.types.ts
@@ -45,6 +45,7 @@ export type SessionLauncher = {
creation_date: string;
description?: string;
resource_class_id?: number;
+ disk_storage?: number;
environment: SessionLauncherEnvironment;
};
@@ -98,14 +99,16 @@ export type AddSessionLauncherParams = {
name: string;
project_id: string;
resource_class_id?: number;
+ disk_storage?: number;
environment: SessionLauncherEnvironmentParams;
};
export interface UpdateSessionLauncherParams {
- launcherId?: string;
+ launcherId: string;
description?: string;
name?: string;
resource_class_id?: number;
+ disk_storage?: number | null;
environment?: SessionLauncherEnvironmentParams;
}
@@ -121,6 +124,7 @@ export interface SessionLauncherForm {
environment_kind: EnvironmentKind;
environment_id: string;
resourceClass: ResourceClass;
+ diskStorage: number | undefined;
port: number;
working_directory: string;
uid: number;
diff --git a/client/src/features/sessionsV2/useSessionResourceClass.hook.ts b/client/src/features/sessionsV2/useSessionResourceClass.hook.ts
index 4499624f8f..7b1b0bf2e2 100644
--- a/client/src/features/sessionsV2/useSessionResourceClass.hook.ts
+++ b/client/src/features/sessionsV2/useSessionResourceClass.hook.ts
@@ -47,14 +47,17 @@ export default function useSessionResourceClass({
const [isPendingResourceClass, setIsPendingResourceClass] =
useState(false);
const setResourceClass = useCallback(
- (envClass: ResourceClass) => {
+ (envClass: ResourceClass, diskStorage: number | undefined) => {
if (envClass) {
dispatch(
startSessionOptionsV2Slice.actions.setSessionClass(envClass.id)
);
+ const cappedStorage = diskStorage
+ ? Math.min(diskStorage, envClass.max_storage)
+ : diskStorage;
dispatch(
startSessionOptionsV2Slice.actions.setStorage(
- envClass.default_storage
+ cappedStorage ?? envClass.default_storage
)
);
setIsPendingResourceClass(false);
@@ -86,10 +89,11 @@ export default function useSessionResourceClass({
}
if (initialSessionClass && !isCustomLaunch)
- setResourceClass(initialSessionClass);
+ setResourceClass(initialSessionClass, launcher.disk_storage);
}, [
isCustomLaunch,
isLoadingLauncherClass,
+ launcher.disk_storage,
launcherClass,
resourcePools,
setResourceClass,
diff --git a/tests/cypress/e2e/projectV2Session.spec.ts b/tests/cypress/e2e/projectV2Session.spec.ts
index 7bd7f67039..39fdd9a241 100644
--- a/tests/cypress/e2e/projectV2Session.spec.ts
+++ b/tests/cypress/e2e/projectV2Session.spec.ts
@@ -98,7 +98,7 @@ describe("launch sessions with data connectors", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(1);
req.reply({ body: session, delay: 2000 });
@@ -157,7 +157,7 @@ describe("launch sessions with data connectors", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(1);
const storage = csConfig[0];
@@ -238,7 +238,7 @@ describe("launch sessions with data connectors", () => {
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(1);
const storage = csConfig[0];
@@ -331,7 +331,7 @@ describe("launch sessions with data connectors", () => {
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(1);
const storage = csConfig[0];
@@ -394,7 +394,7 @@ describe("launch sessions with data connectors", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(1);
const storage = csConfig[0];
@@ -433,7 +433,7 @@ describe("launch sessions with data connectors", () => {
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(1);
const storage = csConfig[0];
@@ -523,7 +523,7 @@ describe("launch sessions with data connectors", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(3);
const s3Storage = csConfig[0];
@@ -607,7 +607,7 @@ describe("launch sessions with data connectors", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(3);
const s3Storage = csConfig[0];
@@ -702,7 +702,7 @@ describe("launch sessions with data connectors", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
const csConfig = req.body.cloudstorage;
expect(csConfig.length).equal(3);
const s3Storage = csConfig[0];
@@ -824,7 +824,7 @@ describe("launch sessions with secrets", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
req.reply({ body: session, delay: 2000 });
}).as("createSession");
});
@@ -861,7 +861,7 @@ describe("launch sessions with secrets", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
req.reply({ body: session, delay: 2000 });
}).as("createSession");
});
@@ -909,7 +909,7 @@ describe("launch sessions with secrets", () => {
// start session
cy.fixture("sessions/sessionV2.json").then((session) => {
// eslint-disable-next-line max-nested-callbacks
- cy.intercept("POST", "/ui-server/api/data/sessions", (req) => {
+ cy.intercept("POST", "/api/data/sessions", (req) => {
req.reply({ body: session, delay: 2000 });
}).as("createSession");
});
diff --git a/tests/cypress/e2e/projectV2setup.spec.ts b/tests/cypress/e2e/projectV2setup.spec.ts
index 3f5b71a5d7..3fe6647f7c 100644
--- a/tests/cypress/e2e/projectV2setup.spec.ts
+++ b/tests/cypress/e2e/projectV2setup.spec.ts
@@ -76,7 +76,7 @@ describe("Set up project components", () => {
});
it("set up sessions", () => {
- cy.intercept("/ui-server/api/data/sessions*", {
+ cy.intercept("/api/data/sessions*", {
body: [],
}).as("getSessionsV2");
fixtures
diff --git a/tests/cypress/support/renkulab-fixtures/newSession.ts b/tests/cypress/support/renkulab-fixtures/newSession.ts
index 283b159406..072a24a125 100644
--- a/tests/cypress/support/renkulab-fixtures/newSession.ts
+++ b/tests/cypress/support/renkulab-fixtures/newSession.ts
@@ -111,22 +111,14 @@ export function NewSession(Parent: T) {
newLauncher(args?: SimpleFixture) {
const { fixture = "", name = "newLauncher" } = args ?? {};
const response = { fixture, statusCode: 201 };
- cy.intercept(
- "POST",
- "/ui-server/api/data/session_launchers",
- response
- ).as(name);
+ cy.intercept("POST", "/api/data/session_launchers", response).as(name);
return this;
}
editLauncher(args?: SimpleFixture) {
const { fixture = "", name = "editLauncher" } = args ?? {};
const response = { fixture, statusCode: 201 };
- cy.intercept(
- "PATCH",
- "/ui-server/api/data/session_launchers/*",
- response
- ).as(name);
+ cy.intercept("PATCH", "/api/data/session_launchers/*", response).as(name);
return this;
}
@@ -136,9 +128,7 @@ export function NewSession(Parent: T) {
name = "getEnvironments",
} = args ?? {};
const response = { fixture };
- cy.intercept("GET", `/ui-server/api/data/environments`, response).as(
- name
- );
+ cy.intercept("GET", `/api/data/environments`, response).as(name);
return this;
}
};
diff --git a/tests/cypress/support/renkulab-fixtures/projectV2.ts b/tests/cypress/support/renkulab-fixtures/projectV2.ts
index 2a8842c35e..b20c917ca5 100644
--- a/tests/cypress/support/renkulab-fixtures/projectV2.ts
+++ b/tests/cypress/support/renkulab-fixtures/projectV2.ts
@@ -420,7 +420,7 @@ export function ProjectV2(Parent: T) {
const response = { fixture };
cy.intercept(
"GET",
- `/ui-server/api/data/projects/*/session_launchers`,
+ `/api/data/projects/*/session_launchers`,
response
).as(name);
return this;
diff --git a/tests/cypress/support/renkulab-fixtures/sessions.ts b/tests/cypress/support/renkulab-fixtures/sessions.ts
index f2053d3a56..69bb313ca9 100644
--- a/tests/cypress/support/renkulab-fixtures/sessions.ts
+++ b/tests/cypress/support/renkulab-fixtures/sessions.ts
@@ -39,7 +39,7 @@ export function Sessions(Parent: T) {
const { fixture = "sessions/sessions.json", name = "getSessionsV2" } =
args ?? {};
const response = { fixture };
- cy.intercept("GET", "/ui-server/api/data/sessions*", response).as(name);
+ cy.intercept("GET", "/api/data/sessions*", response).as(name);
return this;
}
@@ -231,11 +231,9 @@ export function Sessions(Parent: T) {
sessionImage(args?: NameOnlyFixture) {
const { name = "getSessionImage" } = args ?? {};
const response = { status: 200 };
- cy.intercept(
- "GET",
- "/ui-server/api/data/sessions/images?image_url=*",
- response
- ).as(name);
+ cy.intercept("GET", "/api/data/sessions/images?image_url=*", response).as(
+ name
+ );
return this;
}
@@ -251,7 +249,7 @@ export function Sessions(Parent: T) {
sessionServersEmptyV2(args?: NameOnlyFixture) {
const { name = "sessionServersEmptyV2" } = args ?? {};
const response = { body: [] };
- cy.intercept("GET", "/ui-server/api/data/sessions", response).as(name);
+ cy.intercept("GET", "/api/data/sessions", response).as(name);
return this;
}