diff --git a/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx b/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx index 5748bd473b..f471e589c8 100644 --- a/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx +++ b/frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx @@ -40,10 +40,11 @@ import { } from './service'; import { checkRequiredFieldsForNotebookStart } from './spawnerUtils'; import { getNotebookDataConnection } from './dataConnection/useNotebookDataConnection'; +import { defaultClusterStorage } from './storage/constants'; type SpawnerFooterProps = { startNotebookData: StartNotebookData; - storageData: StorageData; + storageData: StorageData[]; envVariables: EnvVariable[]; dataConnection: DataConnectionData; canEnablePipelines: boolean; @@ -90,6 +91,11 @@ const SpawnerFooter: React.FC = ({ editNotebook, existingDataConnections, ); + const rootPathStorageData = + storageData.find( + (formData) => formData.creating.mountPath === defaultClusterStorage.mountPath, + ) || storageData[0]; + const afterStart = (name: string, type: 'created' | 'updated') => { const { selectedAcceleratorProfile, notebookSize, image } = startNotebookData; const tep: FormTrackingEventProperties = { @@ -110,8 +116,8 @@ const SpawnerFooter: React.FC = ({ imageName: image.imageStream?.metadata.name, projectName, notebookName: name, - storageType: storageData.storageType, - storageDataSize: storageData.creating.size, + storageType: rootPathStorageData.storageType, + storageDataSize: rootPathStorageData.creating.size, dataConnectionType: dataConnection.creating?.type?.toString(), dataConnectionCategory: dataConnection.creating?.values?.category?.toString(), dataConnectionEnabled: dataConnection.enabled, @@ -144,7 +150,7 @@ const SpawnerFooter: React.FC = ({ const pvcDetails = await replaceRootVolumesForNotebook( projectName, editNotebook, - storageData, + rootPathStorageData, dryRun, ).catch(handleError); @@ -247,7 +253,33 @@ const SpawnerFooter: React.FC = ({ ? [dataConnection.existing] : []; - const pvcDetails = await createPvcDataForNotebook(projectName, storageData).catch(handleError); + const createPvcRequests = storageData.map((pvcData) => + createPvcDataForNotebook(projectName, pvcData), + ); + + const pvcResponses = await Promise.all(createPvcRequests).catch(handleError); + const pvcDetails = pvcResponses?.reduce( + (acc, response) => { + if (response.volumes.length) { + acc.volumes = acc.volumes.concat(response.volumes); + } else { + acc.volumes = response.volumes; + } + + if (response.volumeMounts.length) { + acc.volumeMounts = acc.volumeMounts.concat(response.volumeMounts); + } else { + acc.volumeMounts = response.volumeMounts; + } + + return acc; + }, + { + volumes: [], + volumeMounts: [], + }, + ); + const envFrom = await createConfigMapsAndSecretsForNotebook(projectName, [ ...envVariables, ...newDataConnection, diff --git a/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx b/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx index 9fc14141c0..230073423b 100644 --- a/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx +++ b/frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; import { Link } from 'react-router-dom'; import { - Alert, Breadcrumb, BreadcrumbItem, + Button, + Flex, + FlexItem, Form, FormSection, PageSection, @@ -31,21 +33,23 @@ import K8sNameDescriptionField, { useK8sNameDescriptionFieldData, } from '~/concepts/k8s/K8sNameDescriptionField/K8sNameDescriptionField'; import { LimitNameResourceType } from '~/concepts/k8s/K8sNameDescriptionField/utils'; +import { StorageData, StorageType } from '~/pages/projects/types'; import { SpawnerPageSectionID } from './types'; import { ScrollableSelectorID, SpawnerPageSectionTitles } from './const'; import SpawnerFooter from './SpawnerFooter'; import ImageSelectorField from './imageSelector/ImageSelectorField'; import ContainerSizeSelector from './deploymentSize/ContainerSizeSelector'; -import StorageField from './storage/StorageField'; import EnvironmentVariables from './environmentVariables/EnvironmentVariables'; -import { useStorageDataObject } from './storage/utils'; -import { getCompatibleAcceleratorIdentifiers, useMergeDefaultPVCName } from './spawnerUtils'; +import { getCompatibleAcceleratorIdentifiers, getRootVolumeName } from './spawnerUtils'; import { useNotebookEnvVariables } from './environmentVariables/useNotebookEnvVariables'; import DataConnectionField from './dataConnection/DataConnectionField'; import { useNotebookDataConnection } from './dataConnection/useNotebookDataConnection'; import { useNotebookSizeState } from './useNotebookSizeState'; import useDefaultStorageClass from './storage/useDefaultStorageClass'; import usePreferredStorageClass from './storage/usePreferredStorageClass'; +import { ClusterStorageTable } from './storage/ClusterStorageTable'; +import useDefaultPvcSize from './storage/useDefaultPvcSize'; +import { defaultClusterStorage } from './storage/constants'; type SpawnerPageProps = { existingNotebook?: NotebookKind; @@ -68,19 +72,30 @@ const SpawnerPage: React.FC = ({ existingNotebook }) => { const [supportedAcceleratorProfiles, setSupportedAcceleratorProfiles] = React.useState< string[] | undefined >(); - const [storageDataWithoutDefault, setStorageData] = useStorageDataObject(existingNotebook); - const [defaultStorageClass] = useDefaultStorageClass(); const preferredStorageClass = usePreferredStorageClass(); const isStorageClassesAvailable = useIsAreaAvailable(SupportedArea.STORAGE_CLASSES).status; const defaultStorageClassName = isStorageClassesAvailable ? defaultStorageClass?.metadata.name : preferredStorageClass?.metadata.name; - const storageData = useMergeDefaultPVCName( - storageDataWithoutDefault, - k8sNameDescriptionData.data.name, - defaultStorageClassName, - ); + const defaultNotebookSize = useDefaultPvcSize(); + const [storageData, setStorageData] = React.useState([ + { + storageType: existingNotebook ? StorageType.EXISTING_PVC : StorageType.NEW_PVC, + creating: { + nameDesc: { + name: k8sNameDescriptionData.data.name || defaultClusterStorage.name, + description: defaultClusterStorage.description, + }, + size: defaultClusterStorage.size || defaultNotebookSize, + storageClassName: defaultStorageClassName || defaultClusterStorage.storageClassName, + mountPath: defaultClusterStorage.mountPath, + }, + existing: { + storage: getRootVolumeName(existingNotebook), + }, + }, + ]); const [envVariables, setEnvVariables] = useNotebookEnvVariables(existingNotebook); const [dataConnectionData, setDataConnectionData] = useNotebookDataConnection( @@ -204,19 +219,32 @@ const SpawnerPage: React.FC = ({ existingNotebook }) => { + + {SpawnerPageSectionTitles[SpawnerPageSectionID.CLUSTER_STORAGE]} + + + + + + + } id={SpawnerPageSectionID.CLUSTER_STORAGE} aria-label={SpawnerPageSectionTitles[SpawnerPageSectionID.CLUSTER_STORAGE]} > - ({ ...formData, id: index }))} + setStorageData={setStorageData} + workbenchName={k8sNameDescriptionData.data.k8sName.value} /> - { - const modifiedRef = React.useRef(false); - - if (modifiedRef.current || storageData.creating.nameDesc.name) { - modifiedRef.current = true; - return storageData; - } - - return { - ...storageData, - creating: { - ...storageData.creating, - nameDesc: { - ...storageData.creating.nameDesc, - name: storageData.creating.nameDesc.name || defaultPVCName, - }, - storageClassName: storageData.creating.storageClassName || defaultStorageClassName, - }, - }; -}; - export const getVersion = (version?: string | number, prefix?: string): string => { if (!version) { return ''; @@ -395,12 +369,11 @@ export const isEnvVariableDataValid = (envVariables: EnvVariable[]): boolean => export const checkRequiredFieldsForNotebookStart = ( startNotebookData: StartNotebookData, - storageData: StorageData, + storageData: StorageData[], envVariables: EnvVariable[], dataConnection: DataConnectionData, ): boolean => { const { projectName, notebookData, image } = startNotebookData; - const { storageType, creating, existing } = storageData; const isNotebookDataValid = !!( projectName && isK8sNameDescriptionDataValid(notebookData) && @@ -408,9 +381,7 @@ export const checkRequiredFieldsForNotebookStart = ( image.imageVersion ); - const newStorageFieldInvalid = storageType === StorageType.NEW_PVC && !creating.nameDesc.name; - const existingStorageFieldInvalid = storageType === StorageType.EXISTING_PVC && !existing.storage; - const isStorageDataValid = !newStorageFieldInvalid && !existingStorageFieldInvalid; + const isStorageDataValid = storageData.length > 0; const newDataConnectionInvalid = dataConnection.type === 'creating' && diff --git a/frontend/src/pages/projects/screens/spawner/storage/ClusterStorageEditModal.tsx b/frontend/src/pages/projects/screens/spawner/storage/ClusterStorageEditModal.tsx new file mode 100644 index 0000000000..0158a39196 --- /dev/null +++ b/frontend/src/pages/projects/screens/spawner/storage/ClusterStorageEditModal.tsx @@ -0,0 +1,99 @@ +import React from 'react'; + +import { Form, FormGroup, Modal, ModalVariant, TextArea, TextInput } from '@patternfly/react-core'; + +import DashboardModalFooter from '~/concepts/dashboard/DashboardModalFooter'; +import PVSizeField from '~/pages/projects/components/PVSizeField'; +import { StorageData } from '~/pages/projects/types'; +import StorageClassSelect from './StorageClassSelect'; + +interface ClusterStorageEditModalProps { + storageData: StorageData; + setStorageData: (storageData: StorageData) => void; + onClose: () => void; +} + +export const ClusterStorageEditModal: React.FC = ({ + storageData, + setStorageData, + onClose, +}) => { + const [name, setName] = React.useState(storageData.creating.nameDesc.name); + const [description, setDescription] = React.useState(storageData.creating.nameDesc.description); + const [size, setSize] = React.useState(storageData.creating.size); + const [storageClassName, setStorageClassName] = React.useState( + storageData.creating.storageClassName, + ); + const [mountPath, setMountPath] = React.useState(storageData.creating.mountPath); + + const onSubmit = () => { + setStorageData({ + ...storageData, + creating: { nameDesc: { name, description }, size, storageClassName, mountPath }, + }); + onClose(); + }; + + return ( + + } + data-testid="edit-cluster-storage-modal" + > +
+ + setName(value)} + id="display-name" + data-testid="display-name-input" + /> + + + +