Skip to content

Commit

Permalink
Display storage class selecetd in workbenches
Browse files Browse the repository at this point in the history
  • Loading branch information
pnaik1 committed Sep 17, 2024
1 parent 7964daa commit 1183dfb
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 12 deletions.
4 changes: 3 additions & 1 deletion frontend/src/__mocks__/mockPVCK8sResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ type MockResourceConfigType = {
name?: string;
namespace?: string;
storage?: string;
storageClassName?: string;
displayName?: string;
uid?: string;
};
Expand All @@ -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 => ({
Expand All @@ -38,7 +40,7 @@ export const mockPVCK8sResource = ({
},
},
volumeName: 'pvc-8644e33b-a710-45a3-9d54-7f987494643a',
storageClassName: 'gp3',
storageClassName,
volumeMode: 'Filesystem',
},
status: {
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -110,6 +123,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'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { mockK8sResourceList, mockNotebookK8sResource, mockProjectK8sResource } from '~/__mocks__';
import {
mockDashboardConfig,
mockK8sResourceList,
mockNotebookK8sResource,
mockProjectK8sResource,
mockStorageClassList,
} from '~/__mocks__';
import { mockClusterSettings } from '~/__mocks__/mockClusterSettings';
import { mockPVCK8sResource } from '~/__mocks__/mockPVCK8sResource';
import { mockPodK8sResource } from '~/__mocks__/mockPodK8sResource';
Expand All @@ -20,9 +26,10 @@ import { mockPrometheusQueryResponse } from '~/__mocks__/mockPrometheusQueryResp

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({})]));
Expand All @@ -37,7 +44,7 @@ const initInterceptors = ({ isEmpty = false }: HandlersProps) => {
isEmpty
? []
: [
mockPVCK8sResource({ uid: 'test-id' }),
mockPVCK8sResource({ uid: 'test-id', storageClassName }),
mockPVCK8sResource({ displayName: 'Another Cluster Storage' }),
],
),
Expand All @@ -46,6 +53,43 @@ const initInterceptors = ({ isEmpty = false }: HandlersProps) => {
};

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');
Expand Down Expand Up @@ -101,10 +145,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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as React from 'react';
import { Alert, AlertActionCloseButton } from '@patternfly/react-core';
import { Table } from '~/components/table';
import { PersistentVolumeClaimKind } from '~/k8sTypes';
import { PersistentVolumeClaimKind, StorageClassConfig } 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';
Expand All @@ -15,20 +19,66 @@ type StorageTableProps = {
const StorageTable: React.FC<StorageTableProps> = ({ pvcs, refresh, onAddPVC }) => {
const [deleteStorage, setDeleteStorage] = React.useState<PersistentVolumeClaimKind | undefined>();
const [editPVC, setEditPVC] = React.useState<PersistentVolumeClaimKind | undefined>();
const isStorageClassesAvailable = useIsAreaAvailable(SupportedArea.STORAGE_CLASSES).status;
const [alertStatuses, setAlertStatuses] = React.useState<Record<string, boolean>>({});

const isDeprecatedAlert = React.useMemo(
() => Object.values(alertStatuses).includes(true),
[alertStatuses],
);
const [storageClasses, storageClassesLoaded] = useStorageClasses();
const storageClassConfigMap = React.useMemo(
() =>
storageClasses.reduce((acc: Record<string, StorageClassConfig | undefined>, sc) => {
acc[sc.metadata.name] = getStorageClassConfig(sc);

return acc;
}, {}),
[storageClasses],
);
const getStorageColumns = () => {
let storageColumns = columns;

if (!isStorageClassesAvailable) {
storageColumns = columns.filter((column) => column.field !== 'storage');
}
return storageColumns;
};

return (
<>
{isDeprecatedAlert && isStorageClassesAvailable && (
<Alert
data-testid="storage-class-deprecated-alert"
variant="warning"
isInline
title="Deprecated storage class"
actionClose={
<AlertActionCloseButton
data-testid="storage-class-deprecated-alert-close-button"
onClose={() => setAlertStatuses({})}
/>
}
>
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.
</Alert>
)}
<Table
data={pvcs}
columns={columns}
columns={getStorageColumns()}
disableRowRenderSupport
data-testid="storage-table"
variant="compact"
rowRenderer={(pvc, i) => (
<StorageTableRow
key={pvc.metadata.uid}
rowIndex={i}
storageClassConfigMap={storageClassConfigMap}
storageClassesLoaded={storageClassesLoaded}
obj={pvc}
setAlertStatuses={setAlertStatuses}
onEditPVC={setEditPVC}
onDeletePVC={setDeleteStorage}
onAddPVC={onAddPVC}
Expand All @@ -49,6 +99,13 @@ const StorageTable: React.FC<StorageTableProps> = ({ pvcs, refresh, onAddPVC })
pvcToDelete={deleteStorage}
onClose={(deleted) => {
if (deleted) {
if (deleteStorage) {
setAlertStatuses((prev) => ({
...prev,
[deleteStorage.metadata.name]: false,
}));
}

refresh();
}
setDeleteStorage(undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,33 @@ import {
Td,
Tr,
} from '@patternfly/react-table';
import { Flex, FlexItem, Text } from '@patternfly/react-core';
import { HddIcon } from '@patternfly/react-icons';
import { PersistentVolumeClaimKind } from '~/k8sTypes';
import {
Flex,
FlexItem,
Label,
Skeleton,
Text,
TextContent,
TextVariants,
Tooltip,
} from '@patternfly/react-core';
import { ExclamationTriangleIcon, HddIcon } from '@patternfly/react-icons';
import { PersistentVolumeClaimKind, StorageClassConfig } from '~/k8sTypes';
import StorageSizeBar from '~/pages/projects/components/StorageSizeBars';
import ConnectedNotebookNames from '~/pages/projects/notebook/ConnectedNotebookNames';
import { ConnectedNotebookContext } from '~/pages/projects/notebook/useRelatedNotebooks';
import { TableRowTitleDescription } from '~/components/table';
import { getDescriptionFromK8sResource, getDisplayNameFromK8sResource } from '~/concepts/k8s/utils';
import { SupportedArea, useIsAreaAvailable } from '~/concepts/areas';
import useIsRootVolume from './useIsRootVolume';
import StorageWarningStatus from './StorageWarningStatus';

type StorageTableRowProps = {
obj: PersistentVolumeClaimKind;
rowIndex: number;
storageClassConfigMap: Record<string, StorageClassConfig | undefined>;
storageClassesLoaded: boolean;
setAlertStatuses: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
onDeletePVC: (pvc: PersistentVolumeClaimKind) => void;
onEditPVC: (pvc: PersistentVolumeClaimKind) => void;
onAddPVC: () => void;
Expand All @@ -29,13 +42,28 @@ type StorageTableRowProps = {
const StorageTableRow: React.FC<StorageTableRowProps> = ({
obj,
rowIndex,
storageClassConfigMap,
storageClassesLoaded,
setAlertStatuses,
onDeletePVC,
onEditPVC,
onAddPVC,
}) => {
const [isExpanded, setExpanded] = React.useState(false);
const isRootVolume = useIsRootVolume(obj);

const isStorageClassesAvailable = useIsAreaAvailable(SupportedArea.STORAGE_CLASSES).status;

const storageClass = storageClassConfigMap[obj.spec.storageClassName ?? ''];
const isStorageClassDisabled = storageClass && !storageClass.isEnabled;
const isStorageClassDeleted = storageClassesLoaded && !storageClass;
React.useEffect(() => {
setAlertStatuses((prevState) => ({
...prevState,
[obj.metadata.name]: !!isStorageClassDisabled || isStorageClassDeleted,
}));
}, [isStorageClassDeleted, isStorageClassDisabled, obj.metadata.name, setAlertStatuses]);

const actions: IAction[] = [
{
title: 'Edit storage',
Expand Down Expand Up @@ -79,6 +107,47 @@ const StorageTableRow: React.FC<StorageTableRowProps> = ({
</Flex>
<Text>{getDescriptionFromK8sResource(obj)}</Text>
</Td>

{isStorageClassesAvailable && (
<Td dataLabel="Storage class">
<TextContent>
<Text>
{obj.spec.storageClassName}{' '}
{isStorageClassDeleted && (
<Tooltip content="This storage class is deleted.">
<Label
data-testid="storage-class-deleted"
isCompact
icon={<ExclamationTriangleIcon />}
color="gold"
>
Deleted
</Label>
</Tooltip>
)}
{isStorageClassDisabled && (
<Tooltip
data-testid="storage-class-deprecated-tooltip"
content="This storage class is deprecated, but the cluster storage is still active."
>
<Label
data-testid="storage-class-deprecated"
isCompact
icon={<ExclamationTriangleIcon />}
color="gold"
>
Deprecated
</Label>
</Tooltip>
)}
</Text>

<Text component={TextVariants.small}>
{storageClassesLoaded ? storageClass?.description : <Skeleton />}
</Text>
</TextContent>
</Td>
)}
<Td dataLabel="Type">
<Text>
<Flex>
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/pages/projects/screens/detail/storage/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@ export const columns: SortableData<PersistentVolumeClaimKind>[] = [
sortable: (a, b) =>
getDisplayNameFromK8sResource(a).localeCompare(getDisplayNameFromK8sResource(b)),
},
{
field: 'storage',
label: 'Storage class',
width: 30,
sortable: (a, b) =>

Check warning on line 22 in frontend/src/pages/projects/screens/detail/storage/data.ts

View check run for this annotation

Codecov / codecov/patch

frontend/src/pages/projects/screens/detail/storage/data.ts#L22

Added line #L22 was not covered by tests
(a.spec.storageClassName ?? '').localeCompare(b.spec.storageClassName ?? ''),
},
{
field: 'type',
label: 'Type',
width: 25,
width: 20,
sortable: false,
},
{
field: 'connected',
label: 'Connected workbenches',
width: 25,
width: 20,
sortable: false,
},
{
Expand Down

0 comments on commit 1183dfb

Please sign in to comment.