diff --git a/frontend/src/__mocks__/mockPVCK8sResource.ts b/frontend/src/__mocks__/mockPVCK8sResource.ts index b1d4c1f092..aeb905b018 100644 --- a/frontend/src/__mocks__/mockPVCK8sResource.ts +++ b/frontend/src/__mocks__/mockPVCK8sResource.ts @@ -5,6 +5,7 @@ type MockResourceConfigType = { name?: string; namespace?: string; storage?: string; + storageClassName?: string; displayName?: string; uid?: string; }; @@ -13,6 +14,7 @@ export const mockPVCK8sResource = ({ name = 'test-storage', namespace = 'test-project', storage = '5Gi', + storageClassName = 'gp3', displayName = 'Test Storage', uid = genUID('pvc'), }: MockResourceConfigType): PersistentVolumeClaimKind => ({ @@ -38,7 +40,7 @@ export const mockPVCK8sResource = ({ }, }, volumeName: 'pvc-8644e33b-a710-45a3-9d54-7f987494643a', - storageClassName: 'gp3', + storageClassName, volumeMode: 'Filesystem', }, status: { diff --git a/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts b/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts index 285b5e5bee..1574717ae2 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts @@ -15,6 +15,19 @@ class ClusterStorageRow extends TableRow { this.find().findByRole('button', { name: 'Details' }).click(); } + findDeprecatedLabel() { + return this.find().findByTestId('storage-class-deprecated'); + } + + shouldHaveDeprecatedTooltip() { + cy.findByTestId('storage-class-deprecated-tooltip').should('be.visible'); + return this; + } + + findStorageClassColumn() { + return this.find().find('[data-label="Storage class"]'); + } + shouldHaveStorageSize(name: string) { this.find().siblings().find('[data-label=Size]').contains(name).should('exist'); return this; @@ -118,6 +131,19 @@ class ClusterStorage { return this.findClusterStorageTable().find('thead').findByRole('button', { name }); } + shouldHaveDeprecatedAlertMessage() { + return cy + .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.', + ); + } + + closeDeprecatedAlert() { + cy.findByTestId('storage-class-deprecated-alert-close-button').click(); + } + getClusterStorageRow(name: string) { return new ClusterStorageRow(() => this.findClusterStorageTable().find(`[data-label=Name]`).contains(name).parents('tr'), diff --git a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts index 149c6f8f40..be0981068e 100644 --- a/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts +++ b/frontend/src/__tests__/cypress/cypress/tests/mocked/projects/clusterStorage.cy.ts @@ -1,10 +1,13 @@ import { buildMockStorageClass, + mockDashboardConfig, mockK8sResourceList, mockNotebookK8sResource, mockProjectK8sResource, mockStorageClasses, + mockStorageClassList, } from '~/__mocks__'; + import { mockClusterSettings } from '~/__mocks__/mockClusterSettings'; import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource'; import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource'; @@ -27,9 +30,10 @@ import { storageClassesTable } from '~/__tests__/cypress/cypress/pages/storageCl type HandlersProps = { isEmpty?: boolean; + storageClassName?: string; }; -const initInterceptors = ({ isEmpty = false }: HandlersProps) => { +const initInterceptors = ({ isEmpty = false, storageClassName }: HandlersProps) => { cy.interceptOdh('GET /api/cluster-settings', mockClusterSettings({})); cy.interceptK8sList(PodModel, mockK8sResourceList([mockPodK8sResource({})])); cy.interceptK8sList(ProjectModel, mockK8sResourceList([mockProjectK8sResource({})])); @@ -44,7 +48,7 @@ const initInterceptors = ({ isEmpty = false }: HandlersProps) => { isEmpty ? [] : [ - mockPVCK8sResource({ uid: 'test-id' }), + mockPVCK8sResource({ uid: 'test-id', storageClassName }), mockPVCK8sResource({ displayName: 'Another Cluster Storage' }), ], ), @@ -55,6 +59,43 @@ const initInterceptors = ({ isEmpty = false }: HandlersProps) => { const [openshiftDefaultStorageClass, otherStorageClass] = mockStorageClasses; describe('ClusterStorage', () => { + describe('when StorageClasses feature flag is enabled', () => { + beforeEach(() => { + cy.interceptOdh( + 'GET /api/config', + mockDashboardConfig({ + disableStorageClasses: false, + }), + ); + + cy.interceptOdh( + 'GET /api/k8s/apis/storage.k8s.io/v1/storageclasses', + {}, + mockStorageClassList(), + ); + }); + + it('Check whether the Storage class column is present', () => { + initInterceptors({ storageClassName: 'openshift-default-sc' }); + clusterStorage.visit('test-project'); + const clusterStorageRow = clusterStorage.getClusterStorageRow('Test Storage'); + clusterStorageRow.findStorageClassColumn().should('exist'); + }); + + it('Check whether the Storage class is deprecated', () => { + initInterceptors({ storageClassName: 'test-storage-class-1' }); + clusterStorage.visit('test-project'); + + const clusterStorageRow = clusterStorage.getClusterStorageRow('Test Storage'); + clusterStorageRow.findDeprecatedLabel().should('exist'); + + clusterStorageRow.findDeprecatedLabel().trigger('mouseenter'); + clusterStorageRow.shouldHaveDeprecatedTooltip(); + clusterStorage.shouldHaveDeprecatedAlertMessage(); + clusterStorage.closeDeprecatedAlert(); + }); + }); + it('Empty state', () => { initInterceptors({ isEmpty: true }); clusterStorage.visit('test-project'); @@ -121,10 +162,17 @@ describe('ClusterStorage', () => { }); }); - it('list accelerator profiles and Table sorting', () => { + it('list cluster storage and Table sorting', () => { + cy.interceptOdh( + 'GET /api/config', + mockDashboardConfig({ + disableStorageClasses: true, + }), + ); initInterceptors({}); clusterStorage.visit('test-project'); const clusterStorageRow = clusterStorage.getClusterStorageRow('Test Storage'); + clusterStorageRow.findStorageClassColumn().should('not.exist'); clusterStorageRow.shouldHaveStorageTypeValue('Persistent storage'); clusterStorageRow.findConnectedWorkbenches().should('have.text', 'No connections'); clusterStorageRow.toggleExpandableContent(); diff --git a/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx b/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx index a3b551d0b3..aeba5e9822 100644 --- a/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx +++ b/frontend/src/pages/projects/screens/detail/storage/StorageTable.tsx @@ -1,10 +1,15 @@ import * as React from 'react'; +import { Alert, AlertActionCloseButton } from '@patternfly/react-core'; import { Table } from '~/components/table'; import { PersistentVolumeClaimKind } from '~/k8sTypes'; import DeletePVCModal from '~/pages/projects/pvc/DeletePVCModal'; +import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas'; +import { getStorageClassConfig } from '~/pages/storageClasses/utils'; +import useStorageClasses from '~/concepts/k8s/useStorageClasses'; import StorageTableRow from './StorageTableRow'; import { columns } from './data'; import ManageStorageModal from './ManageStorageModal'; +import { StorageTableData } from './types'; type StorageTableProps = { pvcs: PersistentVolumeClaimKind[]; @@ -15,20 +20,65 @@ type StorageTableProps = { const StorageTable: React.FC = ({ pvcs, refresh, onAddPVC }) => { const [deleteStorage, setDeleteStorage] = React.useState(); const [editPVC, setEditPVC] = React.useState(); + const isStorageClassesAvailable = useIsAreaAvailable(SupportedArea.STORAGE_CLASSES).status; + const [storageClasses, storageClassesLoaded] = useStorageClasses(); + const [alertDismissed, setAlertDismissed] = React.useState(false); + const storageTableData: StorageTableData[] = pvcs.map((pvc) => ({ + pvc, + storageClass: storageClasses.find((sc) => sc.metadata.name === pvc.spec.storageClassName), + })); + const isDeprecatedAlert = React.useMemo( + () => + storageClassesLoaded && + storageTableData.some( + (data) => !data.storageClass || !getStorageClassConfig(data.storageClass)?.isEnabled, + ), + [storageClassesLoaded, storageTableData], + ); + const shouldShowAlert = isDeprecatedAlert && !alertDismissed && isStorageClassesAvailable; + + const getStorageColumns = () => { + let storageColumns = columns; + + if (!isStorageClassesAvailable) { + storageColumns = columns.filter((column) => column.field !== 'storage'); + } + return storageColumns; + }; return ( <> + {shouldShowAlert && ( + setAlertDismissed(true)} + /> + } + > + 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 + different storage class, contact your administrator. + + )} ( + rowRenderer={(data, i) => ( void; onEditPVC: (pvc: PersistentVolumeClaimKind) => void; onAddPVC: () => void; }; const StorageTableRow: React.FC = ({ - obj, + pvc, rowIndex, + storageClass, + storageClassesLoaded, onDeletePVC, onEditPVC, onAddPVC, }) => { const [isExpanded, setExpanded] = React.useState(false); - const isRootVolume = useIsRootVolume(obj); + const isRootVolume = useIsRootVolume(pvc); + + const isStorageClassesAvailable = useIsAreaAvailable(SupportedArea.STORAGE_CLASSES).status; + const storageClassConfig = storageClass && getStorageClassConfig(storageClass); const actions: IAction[] = [ { title: 'Edit storage', onClick: () => { - onEditPVC(obj); + onEditPVC(pvc); }, }, ]; @@ -49,7 +66,7 @@ const StorageTableRow: React.FC = ({ actions.push({ title: 'Delete storage', onClick: () => { - onDeletePVC(obj); + onDeletePVC(pvc); }, }); } @@ -71,14 +88,66 @@ const StorageTableRow: React.FC = ({ alignItems={{ default: 'alignItemsCenter' }} > - + - + - {getDescriptionFromK8sResource(obj)} + {getDescriptionFromK8sResource(pvc)} + + {isStorageClassesAvailable && ( + + )}
+ + + + {storageClassConfig?.displayName ?? + storageClass?.metadata.name ?? + pvc.spec.storageClassName} + + + {storageClassesLoaded && ( + + {!storageClass ? ( + + + + ) : ( + !storageClassConfig?.isEnabled && ( + + + + ) + )} + + )} + + + {storageClassesLoaded ? storageClassConfig?.description : } + + @@ -92,7 +161,7 @@ const StorageTableRow: React.FC = ({ @@ -104,7 +173,7 @@ const StorageTableRow: React.FC = ({ Size - + diff --git a/frontend/src/pages/projects/screens/detail/storage/data.ts b/frontend/src/pages/projects/screens/detail/storage/data.ts index 5faa755727..047fc40631 100644 --- a/frontend/src/pages/projects/screens/detail/storage/data.ts +++ b/frontend/src/pages/projects/screens/detail/storage/data.ts @@ -1,8 +1,9 @@ import { SortableData } from '~/components/table'; import { getDisplayNameFromK8sResource } from '~/concepts/k8s/utils'; -import { PersistentVolumeClaimKind } from '~/k8sTypes'; +import { getStorageClassConfig } from '~/pages/storageClasses/utils'; +import { StorageTableData } from './types'; -export const columns: SortableData[] = [ +export const columns: SortableData[] = [ { field: 'expand', label: '', @@ -13,18 +14,32 @@ export const columns: SortableData[] = [ label: 'Name', width: 30, sortable: (a, b) => - getDisplayNameFromK8sResource(a).localeCompare(getDisplayNameFromK8sResource(b)), + getDisplayNameFromK8sResource(a.pvc).localeCompare(getDisplayNameFromK8sResource(b.pvc)), + }, + { + field: 'storage', + label: 'Storage class', + width: 30, + sortable: (a, b) => + (a.storageClass + ? getStorageClassConfig(a.storageClass)?.displayName ?? a.storageClass.metadata.name + : a.pvc.spec.storageClassName ?? '' + ).localeCompare( + b.storageClass + ? getStorageClassConfig(b.storageClass)?.displayName ?? b.storageClass.metadata.name + : b.pvc.spec.storageClassName ?? '', + ), }, { field: 'type', label: 'Type', - width: 25, + width: 20, sortable: false, }, { field: 'connected', label: 'Connected workbenches', - width: 25, + width: 20, sortable: false, }, { diff --git a/frontend/src/pages/projects/screens/detail/storage/types.ts b/frontend/src/pages/projects/screens/detail/storage/types.ts new file mode 100644 index 0000000000..2d46661f86 --- /dev/null +++ b/frontend/src/pages/projects/screens/detail/storage/types.ts @@ -0,0 +1,6 @@ +import { PersistentVolumeClaimKind, StorageClassKind } from '~/k8sTypes'; + +export type StorageTableData = { + pvc: PersistentVolumeClaimKind; + storageClass: StorageClassKind | undefined; +};