diff --git a/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts b/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts index d87255a01e..6700f07c61 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/clusterStorage.ts @@ -48,6 +48,19 @@ class ClusterStorageRow extends TableRow { findStorageClassResourceKindText() { return cy.findByTestId('resource-kind-text'); } + + findStorageSizeWarning() { + return cy.findByTestId('size-warning-popover').click(); + } + + findStorageSizeWarningText() { + return cy + .findByTestId('size-warning-popover-text') + .should( + 'have.text', + 'To complete the storage size update, you must connect and run a workbench.', + ); + } } class ClusterStorageModal extends Modal { 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 33dddf5e95..4c1b46fbbb 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 @@ -56,6 +56,28 @@ const initInterceptors = ({ isEmpty = false, storageClassName }: HandlersProps) storageClassName, status: { phase: 'Pending' }, }), + mockPVCK8sResource({ + displayName: 'Updated storage with no workbench', + storageClassName, + storage: '13Gi', + status: { + phase: 'Bound', + accessModes: ['ReadWriteOnce'], + capacity: { + storage: '12Gi', + }, + conditions: [ + { + type: 'FileSystemResizePending', + status: 'True', + lastProbeTime: null, + lastTransitionTime: '2024-11-15T14:04:04Z', + message: + 'Waiting for user to (re-)start a pod to finish file system resize of volume on node.', + }, + ], + }, + }), ], ), ); @@ -334,6 +356,18 @@ describe('ClusterStorage', () => { clusterStorage.findClusterStorageTableHeaderButton('Name').should(be.sortDescending); }); + it('should show warning when cluster storage size is updated but no workbench is connected', () => { + initInterceptors({}); + clusterStorage.visit('test-project'); + const clusterStorageRow = clusterStorage.getClusterStorageRow( + 'Updated storage with no workbench', + ); + clusterStorageRow.toggleExpandableContent(); + clusterStorageRow.shouldHaveStorageSize('Max 13Gi'); + clusterStorageRow.findStorageSizeWarning(); + clusterStorageRow.findStorageSizeWarning().should('exist'); + }); + it('Edit cluster storage', () => { initInterceptors({}); clusterStorage.visit('test-project'); diff --git a/frontend/src/concepts/dashboard/DashboardPopupIconButton.tsx b/frontend/src/concepts/dashboard/DashboardPopupIconButton.tsx index 9ce1fbd02b..03338f5b0d 100644 --- a/frontend/src/concepts/dashboard/DashboardPopupIconButton.tsx +++ b/frontend/src/concepts/dashboard/DashboardPopupIconButton.tsx @@ -3,6 +3,7 @@ import { Button, ButtonProps, Icon } from '@patternfly/react-core'; type DashboardPopupIconButtonProps = Omit & { icon: React.ReactNode; + iconStatus?: 'custom' | 'info' | 'success' | 'warning' | 'danger'; }; /** @@ -10,10 +11,11 @@ type DashboardPopupIconButtonProps = Omit ( diff --git a/frontend/src/k8sTypes.ts b/frontend/src/k8sTypes.ts index b50b48058e..9a7818b168 100644 --- a/frontend/src/k8sTypes.ts +++ b/frontend/src/k8sTypes.ts @@ -311,6 +311,7 @@ export type PersistentVolumeClaimKind = K8sResourceCommon & { capacity?: { storage: string; }; + conditions?: K8sCondition[]; } & Record; }; diff --git a/frontend/src/pages/projects/components/StorageSizeBars.tsx b/frontend/src/pages/projects/components/StorageSizeBars.tsx index 4a4e6dd199..f9e0e83bdc 100644 --- a/frontend/src/pages/projects/components/StorageSizeBars.tsx +++ b/frontend/src/pages/projects/components/StorageSizeBars.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { Bullseye, + Popover, Progress, ProgressMeasureLocation, Spinner, @@ -9,11 +10,12 @@ import { Text, Tooltip, } from '@patternfly/react-core'; -import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import { ExclamationCircleIcon, ExclamationTriangleIcon } from '@patternfly/react-icons'; import { PersistentVolumeClaimKind } from '~/k8sTypes'; -import { getPvcTotalSize } from '~/pages/projects/utils'; +import { getPvcRequestSize, getPvcTotalSize } from '~/pages/projects/utils'; import { usePVCFreeAmount } from '~/api'; import { bytesAsRoundedGiB } from '~/utilities/number'; +import DashboardPopupIconButton from '~/concepts/dashboard/DashboardPopupIconButton'; type StorageSizeBarProps = { pvc: PersistentVolumeClaimKind; @@ -22,6 +24,7 @@ type StorageSizeBarProps = { const StorageSizeBar: React.FC = ({ pvc }) => { const [inUseInBytes, loaded, error] = usePVCFreeAmount(pvc); const maxValue = getPvcTotalSize(pvc); + const requestedValue = getPvcRequestSize(pvc); if (!error && Number.isNaN(inUseInBytes)) { return ( @@ -33,6 +36,25 @@ const StorageSizeBar: React.FC = ({ pvc }) => { ); } + if (pvc.status?.conditions?.find((c) => c.type === 'FileSystemResizePending')) { + return ( +
+ Max {requestedValue} + + } + aria-label="Size warning" + iconStatus="warning" + data-testid="size-warning-popover" + /> + +
+ ); + } + const inUseValue = `${bytesAsRoundedGiB(inUseInBytes)}GiB`; const percentage = ((parseFloat(inUseValue) / parseFloat(maxValue)) * 100).toFixed(2); const percentageLabel = error ? '' : `Storage is ${percentage}% full`; diff --git a/frontend/src/pages/projects/utils.ts b/frontend/src/pages/projects/utils.ts index b6b175adf5..05fe6108b1 100644 --- a/frontend/src/pages/projects/utils.ts +++ b/frontend/src/pages/projects/utils.ts @@ -9,6 +9,9 @@ export const getNotebookStatusPriority = (notebookState: NotebookState): number export const getPvcTotalSize = (pvc: PersistentVolumeClaimKind): string => formatMemory(pvc.status?.capacity?.storage || pvc.spec.resources.requests.storage); +export const getPvcRequestSize = (pvc: PersistentVolumeClaimKind): string => + formatMemory(pvc.spec.resources.requests.storage); + export const getCustomNotebookSize = ( existingNotebook: NotebookKind | undefined, ): NotebookSize => ({