From cd327535c7b901ceaceb439396dbdbdcdeb72ba2 Mon Sep 17 00:00:00 2001 From: Jeff Puzzo Date: Thu, 26 Sep 2024 12:03:55 -0400 Subject: [PATCH] [RHOAIENG-13320] Storage class duplication isDefault edge case --- .../cypress/cypress/pages/clusterStorage.ts | 2 +- .../table/TableRowTitleDescription.tsx | 31 +-- .../screens/detail/storage/StorageTable.tsx | 2 +- .../detail/storage/StorageTableRow.tsx | 14 +- .../spawner/storage/StorageClassSelect.tsx | 7 +- .../ResetCorruptConfigValueAlert.tsx | 8 +- .../StorageClassConfigValue.tsx | 14 ++ .../StorageClassDefaultRadio.tsx | 37 ++-- .../StorageClassEnableSwitch.tsx | 7 +- .../storageClasses/StorageClassesContext.tsx | 176 +++++++++++++++++ .../storageClasses/StorageClassesPage.tsx | 178 +++++++----------- .../storageClasses/StorageClassesTable.tsx | 37 +--- .../storageClasses/StorageClassesTableRow.tsx | 81 ++++---- frontend/src/pages/storageClasses/utils.ts | 6 +- 14 files changed, 378 insertions(+), 222 deletions(-) create mode 100644 frontend/src/pages/storageClasses/StorageClassConfigValue.tsx create mode 100644 frontend/src/pages/storageClasses/StorageClassesContext.tsx diff --git a/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts b/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts index 967d4ad98f..45b0cc3b30 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts @@ -140,7 +140,7 @@ class ClusterStorage { .findByTestId('storage-class-deprecated-alert') .should( 'contain.text', - 'Warning alert:Deprecated storage classA storage class has been deprecated by your administrator, but the cluster storage using it is still active. If you want to migrate your data to cluster storage instance using a different storage class, contact your administrator.', + 'Warning alert:Deprecated storage classA storage class has been deprecated by your administrator, but the cluster storage using it is still active. If you want to migrate your data to a cluster storage instance using a different storage class, contact your administrator.', ); } diff --git a/frontend/src/components/table/TableRowTitleDescription.tsx b/frontend/src/components/table/TableRowTitleDescription.tsx index e4db1e2b33..a80076bef1 100644 --- a/frontend/src/components/table/TableRowTitleDescription.tsx +++ b/frontend/src/components/table/TableRowTitleDescription.tsx @@ -9,7 +9,7 @@ type TableRowTitleDescriptionProps = { boldTitle?: boolean; resource?: K8sResourceCommon; subtitle?: React.ReactNode; - description?: string; + description?: React.ReactNode; descriptionAsMarkdown?: boolean; truncateDescriptionLines?: number; label?: React.ReactNode; @@ -29,20 +29,21 @@ const TableRowTitleDescription: React.FC = ({ }) => { let descriptionNode: React.ReactNode; if (description) { - descriptionNode = descriptionAsMarkdown ? ( - - ) : ( - - {truncateDescriptionLines !== undefined ? ( - - ) : ( - description - )} - - ); + descriptionNode = + descriptionAsMarkdown && typeof description === 'string' ? ( + + ) : ( + + {truncateDescriptionLines !== undefined && typeof description === 'string' ? ( + + ) : ( + description + )} + + ); } return ( diff --git a/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx b/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx index 6216b6e198..8df01df21c 100644 --- a/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx +++ b/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx @@ -62,7 +62,7 @@ const StorageTable: React.FC = ({ pvcs, refresh, onAddPVC }) } > A storage class has been deprecated by your administrator, but the cluster storage using - it is still active. If you want to migrate your data to cluster storage instance using a + it is still active. If you want to migrate your data to a cluster storage instance using a different storage class, contact your administrator. )} diff --git a/frontend/src/pages/projects/screens/detail/storage/StorageTableRow.tsx b/frontend/src/pages/projects/screens/detail/storage/StorageTableRow.tsx index bee910e800..6cbd8d19d3 100644 --- a/frontend/src/pages/projects/screens/detail/storage/StorageTableRow.tsx +++ b/frontend/src/pages/projects/screens/detail/storage/StorageTableRow.tsx @@ -15,6 +15,7 @@ import { Text, TextVariants, Tooltip, + Truncate, } from '@patternfly/react-core'; import { ExclamationTriangleIcon, HddIcon } from '@patternfly/react-icons'; import { PersistentVolumeClaimKind } from '~/k8sTypes'; @@ -100,17 +101,20 @@ const StorageTableRow: React.FC = ({ {isStorageClassesAvailable && ( - + - - {storageClassConfig?.displayName ?? + + obj.pvc.spec.storageClassName ?? + '' + } + /> {storageClassesLoaded && ( diff --git a/frontend/src/pages/projects/screens/spawner/storage/StorageClassSelect.tsx b/frontend/src/pages/projects/screens/spawner/storage/StorageClassSelect.tsx index e9127c4f12..c8eb591f90 100644 --- a/frontend/src/pages/projects/screens/spawner/storage/StorageClassSelect.tsx +++ b/frontend/src/pages/projects/screens/spawner/storage/StorageClassSelect.tsx @@ -28,6 +28,7 @@ const StorageClassSelect: React.FC = ({ menuAppendTo, }) => { const [storageClasses, storageClassesLoaded] = useStorageClasses(); + const hasStorageClassConfigs = storageClasses.some((sc) => !!getStorageClassConfig(sc)); const enabledStorageClasses = storageClasses .filter((sc) => getStorageClassConfig(sc)?.isEnabled) @@ -76,7 +77,7 @@ const StorageClassSelect: React.FC = ({ }; }); - return ( + return hasStorageClassConfigs ? ( = ({ setStorageClassName(selection); }} isDisabled={ - disableStorageClassSelect || !storageClassesLoaded || storageClasses.length <= 1 + disableStorageClassSelect || !storageClassesLoaded || enabledStorageClasses.length <= 1 } placeholder="Select storage class" popperProps={{ appendTo: menuAppendTo }} @@ -114,7 +115,7 @@ const StorageClassSelect: React.FC = ({ )} - ); + ) : null; }; export default StorageClassSelect; diff --git a/frontend/src/pages/storageClasses/ResetCorruptConfigValueAlert.tsx b/frontend/src/pages/storageClasses/ResetCorruptConfigValueAlert.tsx index be29e9253b..ac2ce14c47 100644 --- a/frontend/src/pages/storageClasses/ResetCorruptConfigValueAlert.tsx +++ b/frontend/src/pages/storageClasses/ResetCorruptConfigValueAlert.tsx @@ -10,7 +10,7 @@ import { CorruptedMetadataAlert } from './CorruptedMetadataAlert'; interface ResetCorruptConfigValueAlertProps extends Pick { storageClassName: string; storageClassConfig: StorageClassConfig; - refresh: () => Promise; + onSuccess: () => Promise; popoverText?: string; } @@ -19,7 +19,7 @@ export const ResetCorruptConfigValueAlert: React.FC { const [error, setError] = React.useState(); const [isUpdating, setIsUpdating] = React.useState(false); @@ -29,13 +29,13 @@ export const ResetCorruptConfigValueAlert: React.FC = ({ + alert, + children, +}) => { + if (!children) { + return alert; + } + + return children; +}; diff --git a/frontend/src/pages/storageClasses/StorageClassDefaultRadio.tsx b/frontend/src/pages/storageClasses/StorageClassDefaultRadio.tsx index 8e056140f0..c5584bcc66 100644 --- a/frontend/src/pages/storageClasses/StorageClassDefaultRadio.tsx +++ b/frontend/src/pages/storageClasses/StorageClassDefaultRadio.tsx @@ -1,6 +1,8 @@ import React from 'react'; -import { Flex, Radio, Spinner, Tooltip } from '@patternfly/react-core'; + +import { Flex, FlexItem, Radio, Spinner, Tooltip } from '@patternfly/react-core'; import { updateStorageClassConfig } from '~/services/StorageClassService'; +import { useStorageClassContext } from './StorageClassesContext'; interface StorageClassDefaultRadioProps { storageClassName: string; @@ -15,6 +17,7 @@ export const StorageClassDefaultRadio: React.FC = isDisabled, onChange, }) => { + const { setIsLoadingDefault } = useStorageClassContext(); const [isChecked, setIsChecked] = React.useState(isInitialChecked); const [isUpdating, setIsUpdating] = React.useState(false); const id = `${storageClassName}-default-radio`; @@ -26,16 +29,18 @@ export const StorageClassDefaultRadio: React.FC = const update = React.useCallback(async () => { setIsUpdating(true); + setIsLoadingDefault(true); try { await updateStorageClassConfig(storageClassName, { isDefault: true }); await onChange(); } finally { setIsUpdating(false); + setIsChecked(true); + // Delay table loading state for smoother transition between default selections + setTimeout(() => setIsLoadingDefault(false), 250); } - - setIsChecked(true); - }, [storageClassName, onChange]); + }, [storageClassName, setIsLoadingDefault, onChange]); const radioInput = React.useMemo( () => ( @@ -54,17 +59,21 @@ export const StorageClassDefaultRadio: React.FC = return ( - {isDisabled ? ( - {radioInput} - ) : ( - radioInput - )} + + {isDisabled ? ( + {radioInput} + ) : ( + radioInput + )} + - + + + ); }; diff --git a/frontend/src/pages/storageClasses/StorageClassEnableSwitch.tsx b/frontend/src/pages/storageClasses/StorageClassEnableSwitch.tsx index dcbe33a62e..3829295952 100644 --- a/frontend/src/pages/storageClasses/StorageClassEnableSwitch.tsx +++ b/frontend/src/pages/storageClasses/StorageClassEnableSwitch.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { Flex, FlexItem, Switch, Tooltip } from '@patternfly/react-core'; import { updateStorageClassConfig } from '~/services/StorageClassService'; -import { StorageClassKind } from '~/k8sTypes'; +import { ResponseStatus } from '~/types'; interface StorageClassEnableSwitchProps { storageClassName: string; isChecked: boolean; isDisabled: boolean; - onChange: () => Promise; + onChange: (update: () => Promise) => Promise; } export const StorageClassEnableSwitch: React.FC = ({ @@ -29,8 +29,7 @@ export const StorageClassEnableSwitch: React.FC = setIsUpdating(true); try { - await updateStorageClassConfig(storageClassName, { isEnabled: checked }); - await onChange(); + await onChange(() => updateStorageClassConfig(storageClassName, { isEnabled: checked })); } finally { setIsUpdating(false); } diff --git a/frontend/src/pages/storageClasses/StorageClassesContext.tsx b/frontend/src/pages/storageClasses/StorageClassesContext.tsx new file mode 100644 index 0000000000..2cf7037983 --- /dev/null +++ b/frontend/src/pages/storageClasses/StorageClassesContext.tsx @@ -0,0 +1,176 @@ +import React from 'react'; + +import { StorageClassConfig, StorageClassKind } from '~/k8sTypes'; +import { FetchStateRefreshPromise } from '~/utilities/useFetchState'; +import { ResponseStatus } from '~/types'; +import { updateStorageClassConfig } from '~/services/StorageClassService'; +import { allSettledPromises } from '~/utilities/allSettledPromises'; +import { getStorageClassConfig, isOpenshiftDefaultStorageClass } from './utils'; + +export interface StorageClassContextProps { + storageClasses: StorageClassKind[]; + storageClassConfigs: Record; + refresh: FetchStateRefreshPromise; + isUpdatingConfigs: boolean; + isLoadingDefault: boolean; + setIsLoadingDefault: (isUpdating: boolean) => void; +} + +const defaultContextValues = { + storageClasses: [], + storageClassConfigs: {}, + refresh: () => Promise.resolve(undefined), + isUpdatingConfigs: false, + isLoadingDefault: false, + setIsLoadingDefault: () => undefined, +}; + +export const StorageClassContext = + React.createContext(defaultContextValues); + +export interface StorageClassContextProviderProps { + storageClasses: StorageClassKind[]; + loaded: boolean; + refresh: FetchStateRefreshPromise; + children: ( + isAlertOpen: boolean, + setIsAlertOpen: React.Dispatch>, + ) => React.ReactNode; +} + +export const StorageClassContextProvider: React.FC = ({ + storageClasses, + refresh, + loaded, + children, +}) => { + const [isUpdatingConfigs, setIsUpdatingConfigs] = React.useState(true); + const [isLoadingDefault, setIsLoadingDefault] = React.useState(false); + const [isAutoDefaultAlertOpen, setIsAutoDefaultAlertOpen] = React.useState(false); + + const storageClassConfigs = React.useMemo( + () => + storageClasses.reduce((acc: Record, storageClass) => { + acc[storageClass.metadata.name] = getStorageClassConfig(storageClass); + + return acc; + }, {}), + [storageClasses], + ); + + const [defaultStorageClassName] = + Object.entries(storageClassConfigs).find(([, config]) => config?.isDefault) || []; + + const openshiftDefaultScName = storageClasses.find((storageClass) => + isOpenshiftDefaultStorageClass(storageClass), + )?.metadata.name; + + const updateConfigs = React.useCallback(async () => { + let hasDefaultConfig = false; + + const updateRequests = Object.entries(storageClassConfigs).reduce( + (acc: Promise[], [name, config], index) => { + const isFirstConfig = index === 0; + const isOpenshiftDefault = openshiftDefaultScName === name; + + // Add a default config annotation when one doesn't exist + if (!config) { + let isDefault = isOpenshiftDefault; + let isEnabled = isDefault; + + if (!openshiftDefaultScName) { + isDefault = isFirstConfig; + isEnabled = true; + } + + acc.push( + updateStorageClassConfig(name, { + isDefault, + isEnabled, + displayName: name, + }), + ); + } + // If multiple defaults are set via OpenShift's dashboard, + // unset all except the first indexed storage class + else { + if (config.isDefault) { + if (!hasDefaultConfig) { + hasDefaultConfig = true; + } else { + acc.push(updateStorageClassConfig(name, { isDefault: false })); + } + } + + // Set a default storage class (OpenShift default or first indexed storage class) + // if none exists and notify the user + if ( + !defaultStorageClassName && + ((isFirstConfig && !openshiftDefaultScName) || isOpenshiftDefault) + ) { + acc.push( + updateStorageClassConfig(name, { + isDefault: true, + isEnabled: true, + }), + ); + } + + // If the default storage class coming from OpenShift is disabled, update to enable + if (defaultStorageClassName && !storageClassConfigs[defaultStorageClassName]?.isEnabled) { + acc.push( + updateStorageClassConfig(defaultStorageClassName, { + isEnabled: true, + }), + ); + } + } + + return acc; + }, + [], + ); + + if (loaded) { + try { + const [successResponses] = await allSettledPromises(updateRequests); + + if (successResponses.length) { + await refresh(); + + if (!defaultStorageClassName) { + setIsAutoDefaultAlertOpen(true); + } + } + } finally { + setIsUpdatingConfigs(false); + } + } + }, [storageClassConfigs, loaded, openshiftDefaultScName, defaultStorageClassName, refresh]); + + // Initialize storage class configs + React.useEffect(() => { + updateConfigs(); + }, [defaultStorageClassName, openshiftDefaultScName, storageClassConfigs, updateConfigs]); + + const value: StorageClassContextProps = React.useMemo( + () => ({ + storageClasses, + storageClassConfigs, + refresh, + isUpdatingConfigs, + isLoadingDefault, + setIsLoadingDefault, + }), + [storageClasses, storageClassConfigs, refresh, isUpdatingConfigs, isLoadingDefault], + ); + + return ( + + {children(isAutoDefaultAlertOpen, setIsAutoDefaultAlertOpen)} + + ); +}; + +export const useStorageClassContext = (): StorageClassContextProps => + React.useContext(StorageClassContext); diff --git a/frontend/src/pages/storageClasses/StorageClassesPage.tsx b/frontend/src/pages/storageClasses/StorageClassesPage.tsx index 4bf691f521..4737038523 100644 --- a/frontend/src/pages/storageClasses/StorageClassesPage.tsx +++ b/frontend/src/pages/storageClasses/StorageClassesPage.tsx @@ -10,129 +10,93 @@ import { AlertActionCloseButton, } from '@patternfly/react-core'; -import { MetadataAnnotation } from '~/k8sTypes'; -import useStorageClasses from '~/concepts/k8s/useStorageClasses'; import { ProjectObjectType, typedEmptyImage } from '~/concepts/design/utils'; import ApplicationsPage from '~/pages/ApplicationsPage'; -import { updateStorageClassConfig } from '~/services/StorageClassService'; -import { ResponseStatus } from '~/types'; +import useStorageClasses from '~/concepts/k8s/useStorageClasses'; import { StorageClassesTable } from './StorageClassesTable'; -import { getStorageClassConfig, isOpenshiftDefaultStorageClass } from './utils'; - -const StorageClassesPage: React.FC = () => { - const [isAlertOpen, setIsAlertOpen] = React.useState(false); - const [isUpdating, setIsUpdating] = React.useState(false); - const [storageClasses, storageClassesLoaded, storageClassesError, refreshStorageClasses] = - useStorageClasses(); - const storageClassesWithoutConfigs = React.useMemo( - () => - storageClasses.filter( - (storageClass) => - !storageClass.metadata.annotations?.[MetadataAnnotation.OdhStorageClassConfig], - ), - [storageClasses], - ); - - const defaultStorageClass = storageClasses.find( - (storageClass) => - isOpenshiftDefaultStorageClass(storageClass) || - getStorageClassConfig(storageClass)?.isDefault, - ); - - const updateStorageClasses = React.useCallback( - async (updateRequests: Promise[]) => { - setIsUpdating(true); - - try { - const updateResponses = await Promise.all(updateRequests); - if (updateResponses.some((response) => response.success)) { - await refreshStorageClasses(); - } - - if (!defaultStorageClass) { - setIsAlertOpen(true); - } - } finally { - setIsUpdating(false); - } - }, - [defaultStorageClass, refreshStorageClasses], - ); - - // Add storage class config annotations automatically for all storage classes without them - React.useEffect(() => { - if (storageClassesWithoutConfigs.length > 0) { - const updateRequests = storageClassesWithoutConfigs.map((storageClass, index) => { - const { metadata } = storageClass; - const { name: storageClassName } = metadata; - - let isDefault = defaultStorageClass?.metadata.uid === metadata.uid; - let isEnabled = isDefault; - - if (!defaultStorageClass) { - isDefault = index === 0; - isEnabled = true; - } +import { StorageClassContextProvider, useStorageClassContext } from './StorageClassesContext'; - return updateStorageClassConfig(storageClassName, { - isDefault, - isEnabled, - displayName: storageClassName, - }); - }); +interface StorageClassesPageInternalProps { + loaded: boolean; + error: Error | undefined; + alert: React.ReactNode; +} - updateStorageClasses(updateRequests); - } - }, [defaultStorageClass, storageClassesWithoutConfigs, updateStorageClasses]); - - const emptyStatePage = ( - - - - - - Configure storage classes - - - At least one OpenShift storage class is required to use OpenShift AI. Configure a storage - class in OpenShift, or request that your admin configure one. - - - - ); +const StorageClassesPageInternal: React.FC = ({ + loaded, + error, + alert, +}) => { + const { isUpdatingConfigs, storageClasses } = useStorageClassContext(); return ( + + + + + Configure storage classes + + + At least one OpenShift storage class is required to use OpenShift AI. Configure a + storage class in OpenShift, or request that your admin configure one. + + + + } provideChildrenPadding > - {isAlertOpen && ( - setIsAlertOpen(false)} />} - > - Some OpenShift AI features won't work without a default storage class. No OpenShift - default exists, so an OpenShift AI default was set automatically. Review the default - storage class, and set a new one if needed. - - )} - + {alert} + ); }; +const StorageClassesPage: React.FC = () => { + const [storageClasses, storageClassesLoaded, error, refresh] = useStorageClasses(); + + return ( + + {(isAlertOpen, setIsAlertOpen) => ( + setIsAlertOpen(false)} />} + > + Some OpenShift AI features won't work without a default storage class. No + OpenShift default exists, so an OpenShift AI default was set automatically. Review + the default storage class, and set a new one if needed. + + ) + } + /> + )} + + ); +}; + export default StorageClassesPage; diff --git a/frontend/src/pages/storageClasses/StorageClassesTable.tsx b/frontend/src/pages/storageClasses/StorageClassesTable.tsx index 309d12aa7b..540d4d35e0 100644 --- a/frontend/src/pages/storageClasses/StorageClassesTable.tsx +++ b/frontend/src/pages/storageClasses/StorageClassesTable.tsx @@ -1,41 +1,23 @@ import React from 'react'; -import { StorageClassConfig, StorageClassKind } from '~/k8sTypes'; -import { FetchStateRefreshPromise } from '~/utilities/useFetchState'; import { Table } from '~/components/table'; import DashboardEmptyTableView from '~/concepts/dashboard/DashboardEmptyTableView'; import { columns, initialScFilterData, StorageClassFilterData } from './constants'; -import { getStorageClassConfig, isValidConfigValue } from './utils'; +import { isValidConfigValue } from './utils'; import { StorageClassesTableRow } from './StorageClassesTableRow'; import { StorageClassFilterToolbar } from './StorageClassFilterToolbar'; +import { useStorageClassContext } from './StorageClassesContext'; -interface StorageClassesTableProps { - storageClasses: StorageClassKind[]; - refresh: FetchStateRefreshPromise; -} - -export const StorageClassesTable: React.FC = ({ - storageClasses, - refresh, -}) => { +export const StorageClassesTable: React.FC = () => { + const { storageClasses, storageClassConfigs } = useStorageClassContext(); const [filterData, setFilterData] = React.useState(initialScFilterData); - const storageClassConfigMap = React.useMemo( - () => - storageClasses.reduce((acc: Record, sc) => { - acc[sc.metadata.name] = getStorageClassConfig(sc); - - return acc; - }, {}), - [storageClasses], - ); - const filteredStorageClasses = React.useMemo( () => storageClasses.filter((sc) => { const displayNameFilter = filterData.displayName.toLowerCase(); const openshiftScNameFilter = filterData.openshiftScName.toLowerCase(); - const configDisplayName = storageClassConfigMap[sc.metadata.name]?.displayName; + const configDisplayName = storageClassConfigs[sc.metadata.name]?.displayName; if ( displayNameFilter && @@ -51,7 +33,7 @@ export const StorageClassesTable: React.FC = ({ !openshiftScNameFilter || sc.metadata.name.toLowerCase().includes(openshiftScNameFilter) ); }), - [filterData.displayName, filterData.openshiftScName, storageClasses, storageClassConfigMap], + [filterData.displayName, filterData.openshiftScName, storageClasses, storageClassConfigs], ); return ( @@ -66,12 +48,7 @@ export const StorageClassesTable: React.FC = ({ } data-testid="storage-classes-table" rowRenderer={(storageClass) => ( - + )} toolbarContent={ diff --git a/frontend/src/pages/storageClasses/StorageClassesTableRow.tsx b/frontend/src/pages/storageClasses/StorageClassesTableRow.tsx index 3d9f1167fb..40f114aec8 100644 --- a/frontend/src/pages/storageClasses/StorageClassesTableRow.tsx +++ b/frontend/src/pages/storageClasses/StorageClassesTableRow.tsx @@ -14,12 +14,12 @@ import { import { Tr, Td, ActionsColumn, TableText } from '@patternfly/react-table'; import { PencilAltIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons'; -import { MetadataAnnotation, StorageClassConfig, StorageClassKind } from '~/k8sTypes'; +import { MetadataAnnotation, StorageClassKind } from '~/k8sTypes'; import { TableRowTitleDescription } from '~/components/table'; -import { FetchStateRefreshPromise } from '~/utilities/useFetchState'; import { updateStorageClassConfig } from '~/services/StorageClassService'; import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; import { NoValue } from '~/components/NoValue'; +import { ResponseStatus } from '~/types'; import { ColumnLabel } from './constants'; import { isOpenshiftDefaultStorageClass, isValidConfigValue } from './utils'; import { StorageClassEnableSwitch } from './StorageClassEnableSwitch'; @@ -28,26 +28,29 @@ import { StorageClassEditModal } from './StorageClassEditModal'; import { OpenshiftDefaultLabel } from './OpenshiftDefaultLabel'; import { CorruptedMetadataAlert } from './CorruptedMetadataAlert'; import { ResetCorruptConfigValueAlert } from './ResetCorruptConfigValueAlert'; +import { useStorageClassContext } from './StorageClassesContext'; +import { StrorageClassConfigValue } from './StorageClassConfigValue'; interface StorageClassesTableRowProps { storageClass: StorageClassKind; - storageClassConfigMap: Record; - refresh: FetchStateRefreshPromise; } -export const StorageClassesTableRow: React.FC = ({ - storageClass, - storageClassConfigMap, - refresh, -}) => { +export const StorageClassesTableRow: React.FC = ({ storageClass }) => { + const { storageClassConfigs, isLoadingDefault, refresh } = useStorageClassContext(); const [isEditModalOpen, setIsEditModalOpen] = React.useState(false); + const [isTogglingEnabled, setIsTogglingEnabled] = React.useState(false); const isOpenshiftDefault = isOpenshiftDefaultStorageClass(storageClass); + const editModalAlertRef = + React.useRef['alert']>(); + const { metadata, provisioner, reclaimPolicy, volumeBindingMode, allowVolumeExpansion } = storageClass; - const storageClassConfig = storageClassConfigMap[metadata.name]; + const storageClassConfig = storageClassConfigs[metadata.name]; const hasReadableConfig = storageClassConfig !== undefined; - const editModalAlertRef = - React.useRef['alert']>(); + const isDefaultRadioDisabled = + (!storageClassConfig?.isDefault && !storageClassConfig?.isEnabled) || + isLoadingDefault || + isTogglingEnabled; const storageClassInfoItems = [ { @@ -77,7 +80,7 @@ export const StorageClassesTableRow: React.FC = ({ ]; const isEnableSwitchDisabled = React.useMemo(() => { - const hasOtherStorageClassesEnabled = Object.entries(storageClassConfigMap).some( + const hasOtherStorageClassesEnabled = Object.entries(storageClassConfigs).some( ([storageClassName, config]) => storageClassName !== metadata.name && config?.isEnabled, ); @@ -95,24 +98,35 @@ export const StorageClassesTableRow: React.FC = ({ metadata.name, storageClassConfig?.isDefault, storageClassConfig?.isEnabled, - storageClassConfigMap, + storageClassConfigs, ]); const onDefaultRadioChange = React.useCallback(async () => { - const existingDefaultConfigMap = Object.entries(storageClassConfigMap).find( + const existingDefaultConfigs = Object.entries(storageClassConfigs).find( ([name, config]) => metadata.name !== name && config?.isDefault, ); - if (existingDefaultConfigMap) { - const [name, config] = existingDefaultConfigMap; + if (existingDefaultConfigs) { + const [name, config] = existingDefaultConfigs; await updateStorageClassConfig(name, { ...config, isDefault: false }); refresh(); } - }, [metadata.name, storageClassConfigMap, refresh]); + }, [metadata.name, storageClassConfigs, refresh]); + + const onEnableSwitchChange = React.useCallback( + async (update: () => Promise) => { + setIsTogglingEnabled(true); + + await update(); + await refresh(); + setIsTogglingEnabled(false); + }, + [refresh], + ); return ( - + {hasReadableConfig ? ( = ({ title={ {storageClassConfig.displayName} } - description={storageClassConfig.description} + description={ + storageClassConfig.description && ( + + {storageClassConfig.description} + + ) + } /> )} @@ -200,7 +220,7 @@ export const StorageClassesTableRow: React.FC = ({ }} variant="danger" popoverText="This storage class is temporarily unavailable for use in new cluster storage. Refresh the field to correct the corrupted metadata." - refresh={refresh} + onSuccess={refresh} /> } > @@ -209,7 +229,7 @@ export const StorageClassesTableRow: React.FC = ({ storageClassName={metadata.name} isChecked={storageClassConfig.isEnabled} isDisabled={isEnableSwitchDisabled} - onChange={refresh} + onChange={onEnableSwitchChange} /> )} @@ -228,7 +248,7 @@ export const StorageClassesTableRow: React.FC = ({ ...storageClassConfig, isDefault: false, }} - refresh={refresh} + onSuccess={refresh} /> } > @@ -236,7 +256,7 @@ export const StorageClassesTableRow: React.FC = ({ )} @@ -253,7 +273,7 @@ export const StorageClassesTableRow: React.FC = ({ } > @@ -311,14 +331,3 @@ export const StorageClassesTableRow: React.FC = ({ ); }; - -const StrorageClassConfigValue: React.FC = ({ - alert, - children, -}) => { - if (!children) { - return alert; - } - - return children; -}; diff --git a/frontend/src/pages/storageClasses/utils.ts b/frontend/src/pages/storageClasses/utils.ts index 64b5fb5fda..5513305774 100644 --- a/frontend/src/pages/storageClasses/utils.ts +++ b/frontend/src/pages/storageClasses/utils.ts @@ -15,8 +15,10 @@ export const getStorageClassConfig = ( } }; -export const isOpenshiftDefaultStorageClass = (storageClass: StorageClassKind): boolean => - storageClass.metadata.annotations?.[MetadataAnnotation.StorageClassIsDefault] === 'true'; +export const isOpenshiftDefaultStorageClass = ( + storageClass: StorageClassKind | undefined, +): boolean => + storageClass?.metadata.annotations?.[MetadataAnnotation.StorageClassIsDefault] === 'true'; export const isValidConfigValue = ( configKey: keyof StorageClassConfig,