Skip to content

Commit

Permalink
[RHOAIENG-1101] Cannot remove additional storage from the workbench c…
Browse files Browse the repository at this point in the history
…onfiguration
  • Loading branch information
jpuzz0 committed Oct 10, 2024
1 parent 65ddbb9 commit 8e91e1d
Show file tree
Hide file tree
Showing 9 changed files with 385 additions and 157 deletions.
42 changes: 37 additions & 5 deletions frontend/src/pages/projects/screens/spawner/SpawnerFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -90,6 +91,11 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
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 = {
Expand All @@ -110,8 +116,8 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
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,
Expand Down Expand Up @@ -144,7 +150,7 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
const pvcDetails = await replaceRootVolumesForNotebook(
projectName,
editNotebook,
storageData,
rootPathStorageData,
dryRun,
).catch(handleError);

Expand Down Expand Up @@ -247,7 +253,33 @@ const SpawnerFooter: React.FC<SpawnerFooterProps> = ({
? [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,
Expand Down
68 changes: 48 additions & 20 deletions frontend/src/pages/projects/screens/spawner/SpawnerPage.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -68,19 +72,30 @@ const SpawnerPage: React.FC<SpawnerPageProps> = ({ 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<StorageData[]>([
{
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(
Expand Down Expand Up @@ -204,19 +219,32 @@ const SpawnerPage: React.FC<SpawnerPageProps> = ({ existingNotebook }) => {
<EnvironmentVariables envVariables={envVariables} setEnvVariables={setEnvVariables} />
</FormSection>
<FormSection
title={SpawnerPageSectionTitles[SpawnerPageSectionID.CLUSTER_STORAGE]}
title={
<Flex
spaceItems={{ default: 'spaceItemsMd' }}
alignItems={{ default: 'alignItemsCenter' }}
>
<FlexItem spacer={{ default: 'spacerLg' }}>
{SpawnerPageSectionTitles[SpawnerPageSectionID.CLUSTER_STORAGE]}
</FlexItem>

<Button variant="secondary" data-testid="existing-storage-button">
Attach existing storage
</Button>

<Button variant="secondary" data-testid="create-storage-button">
Create storage
</Button>
</Flex>
}
id={SpawnerPageSectionID.CLUSTER_STORAGE}
aria-label={SpawnerPageSectionTitles[SpawnerPageSectionID.CLUSTER_STORAGE]}
>
<Alert
data-testid="cluster-storage-alert"
component="h2"
variant="info"
isPlain
isInline
title="Cluster storage will mount to /"
<ClusterStorageTable
storageData={storageData.map((formData, index) => ({ ...formData, id: index }))}
setStorageData={setStorageData}
workbenchName={k8sNameDescriptionData.data.k8sName.value}
/>
<StorageField storageData={storageData} setStorageData={setStorageData} />
</FormSection>
<FormSection
title={SpawnerPageSectionTitles[SpawnerPageSectionID.DATA_CONNECTIONS]}
Expand Down
33 changes: 2 additions & 31 deletions frontend/src/pages/projects/screens/spawner/spawnerUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from 'react';
import compareVersions from 'compare-versions';
import { NotebookSize, Volume, VolumeMount } from '~/types';
import {
Expand Down Expand Up @@ -30,31 +29,6 @@ import {
import { FAILED_PHASES, PENDING_PHASES, IMAGE_ANNOTATIONS } from './const';

/******************* Common utils *******************/
export const useMergeDefaultPVCName = (
storageData: StorageData,
defaultPVCName: string,
defaultStorageClassName?: string,
): StorageData => {
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 '';
Expand Down Expand Up @@ -395,22 +369,19 @@ 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) &&
image.imageStream &&
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' &&
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ClusterStorageEditModalProps> = ({
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 (
<Modal
isOpen
variant={ModalVariant.small}
title="Edit storage"
onClose={onClose}
footer={
<DashboardModalFooter
onCancel={onClose}
onSubmit={onSubmit}
submitLabel="Save"
isSubmitDisabled={!name}
alertTitle="Error updating storage class"
/>
}
data-testid="edit-cluster-storage-modal"
>
<Form id="edit-cluster-storage-form">
<FormGroup label="Display name" fieldId="name" isRequired>
<TextInput
isRequired
value={name}
onChange={(_, value) => setName(value)}
id="display-name"
data-testid="display-name-input"
/>
</FormGroup>

<FormGroup label="Description" fieldId="description">
<TextArea
value={description}
onChange={(_, value) => setDescription(value)}
resizeOrientation="vertical"
autoResize
id="description"
data-testid="description-textarea"
/>
</FormGroup>

<StorageClassSelect
storageClassName={storageClassName}
setStorageClassName={(newName) => setStorageClassName(newName)}
/>

<PVSizeField
fieldID="storage-size"
size={size}
setSize={(storageSize) => setSize(storageSize)}
/>

<FormGroup label="Mount path" fieldId="mount-path" isRequired>
<TextInput
isRequired
value={mountPath}
onChange={(_, value) => setMountPath(value)}
id="mount-path"
data-testid="mount-path-input"
/>
</FormGroup>
</Form>
</Modal>
);
};
Loading

0 comments on commit 8e91e1d

Please sign in to comment.