Skip to content

Commit

Permalink
Add status popover when storage is nearly full (#842)
Browse files Browse the repository at this point in the history
* Add status popover when storage is nearly full

* address comments

* small update
  • Loading branch information
DaoDaoNoCode committed Dec 8, 2022
1 parent f830891 commit 0fbbb3d
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 31 deletions.
20 changes: 20 additions & 0 deletions frontend/src/api/network/pvcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,23 @@ export const deletePvc = (pvcName: string, namespace: string): Promise<K8sStatus
queryOptions: { name: pvcName, ns: namespace },
});
};

export const updatePvcSize = (
pvcName: string,
namespace: string,
size: string,
): Promise<PersistentVolumeClaimKind> => {
return k8sPatchResource({
model: PVCModel,
queryOptions: { name: pvcName, ns: namespace },
patches: [
{
op: 'replace',
path: '/spec/resources/requests',
value: {
storage: size,
},
},
],
});
};
23 changes: 12 additions & 11 deletions frontend/src/pages/projects/components/PVSizeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,42 @@ type PVSizeFieldProps = {
fieldID: string;
size: number;
setSize: (size: number) => void;
disable?: boolean;
currentSize?: string;
};

const PVSizeField: React.FC<PVSizeFieldProps> = ({ fieldID, size, setSize, disable }) => {
const MIN_SIZE = 1;
const PVSizeField: React.FC<PVSizeFieldProps> = ({ fieldID, size, setSize, currentSize }) => {
const minSize = parseInt(currentSize || '') || 1;
const defaultSize = useDefaultPvcSize();
const availableSize = defaultSize * 2;

const onStep = (step: number) => {
setSize(normalizeBetween(size + step, MIN_SIZE, availableSize));
setSize(normalizeBetween(size + step, minSize, availableSize));
};
return (
<FormGroup
label="Persistent storage size"
helperText={disable ? 'Size cannot be changed after creation.' : ''}
helperText={
currentSize
? "Increase the capacity of storage data. Note that capacity can't be less than the current storage size. This can be a time-consuming process."
: ''
}
helperTextIcon={<ExclamationTriangleIcon />}
validated={disable ? 'warning' : 'default'}
validated={currentSize ? 'warning' : 'default'}
fieldId={fieldID}
>
<InputGroup>
<NumberInput
id={fieldID}
isDisabled={disable}
name={fieldID}
value={size}
max={availableSize}
min={MIN_SIZE}
min={minSize}
onPlus={() => onStep(1)}
onMinus={() => onStep(-1)}
onChange={(event) => {
if (isHTMLInputElement(event.target)) {
const newSize = Number(event.target.value);
setSize(
isNaN(newSize) ? availableSize : normalizeBetween(newSize, MIN_SIZE, availableSize),
);
setSize(isNaN(newSize) ? size : normalizeBetween(newSize, minSize, availableSize));
}
}}
/>
Expand Down
18 changes: 10 additions & 8 deletions frontend/src/pages/projects/components/StorageSizeBars.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,19 @@ const StorageSizeBar: React.FC<StorageSizeBarProps> = ({ pvc }) => {

if (!error && isNaN(inUseInBytes)) {
return (
<Tooltip
removeFindDomNode
content="No active storage information at this time, check back later"
>
<Text component="small">Max {maxValue}</Text>
</Tooltip>
<div>
<Tooltip
removeFindDomNode
content="No active storage information at this time, check back later"
>
<Text component="small">Max {maxValue}</Text>
</Tooltip>
</div>
);
}

const inUseValue = `${bytesAsGB(inUseInBytes)}Gi`;
const percentage = (parseFloat(inUseValue) / parseFloat(maxValue)) * 100;
const percentage = ((parseFloat(inUseValue) / parseFloat(maxValue)) * 100).toFixed(2);
const percentageLabel = error ? '' : `Storage is ${percentage}% full`;

let inUseRender: React.ReactNode;
Expand All @@ -55,7 +57,7 @@ const StorageSizeBar: React.FC<StorageSizeBarProps> = ({ pvc }) => {
<Progress
aria-label={percentageLabel || 'Storage progress bar'}
measureLocation={ProgressMeasureLocation.none}
value={percentage}
value={Number(percentage)}
style={{ gridGap: 0 }} // PF issue with split & measureLocation
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
removeNotebookPVC,
updatePvcDescription,
updatePvcDisplayName,
updatePvcSize,
} from '../../../../../api';
import { NotebookKind, PersistentVolumeClaimKind } from '../../../../../k8sTypes';
import { ProjectDetailsContext } from '../../../ProjectDetailsContext';
Expand All @@ -17,7 +18,7 @@ import ExistingConnectedNotebooks from './ExistingConnectedNotebooks';
import useRelatedNotebooks, {
ConnectedNotebookContext,
} from '../../../notebook/useRelatedNotebooks';
import { getPvcDescription, getPvcDisplayName } from '../../../utils';
import { getPvcDescription, getPvcDisplayName, getPvcTotalSize } from '../../../utils';

import './ManageStorageModal.scss';

Expand Down Expand Up @@ -105,6 +106,9 @@ const ManageStorageModal: React.FC<AddStorageModalProps> = ({ existingData, isOp
return;
}
handleNotebookNameConnection(pvcName);
if (parseInt(getPvcTotalSize(existingData)) !== createData.size) {
await updatePvcSize(pvcName, namespace, `${createData.size}Gi`);
}
} else {
createPvc(pvc)
.then((createdPvc) => handleNotebookNameConnection(createdPvc.metadata.name))
Expand Down Expand Up @@ -144,7 +148,7 @@ const ManageStorageModal: React.FC<AddStorageModalProps> = ({ existingData, isOp
<CreateNewStorageSection
data={createData}
setData={(key, value) => setCreateData(key, value)}
disableSize={!!existingData}
currentSize={existingData?.status?.capacity?.storage}
autoFocusName
/>
{createData.hasExistingNotebookConnections && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const StorageList: React.FC = () => {
/>
}
>
<StorageTable pvcs={pvcs} refresh={refresh} />
<StorageTable pvcs={pvcs} refresh={refresh} onAddPVC={() => setOpen(true)} />
</DetailsSection>
<ManageStorageModal
isOpen={isOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import ManageStorageModal from './ManageStorageModal';
type StorageTableProps = {
pvcs: PersistentVolumeClaimKind[];
refresh: () => void;
onAddPVC: () => void;
};

const StorageTable: React.FC<StorageTableProps> = ({ pvcs: unsortedPvcs, refresh }) => {
const StorageTable: React.FC<StorageTableProps> = ({ pvcs: unsortedPvcs, refresh, onAddPVC }) => {
const [deleteStorage, setDeleteStorage] = React.useState<PersistentVolumeClaimKind | undefined>();
const [editPVC, setEditPVC] = React.useState<PersistentVolumeClaimKind | undefined>();
const sort = useTableColumnSort<PersistentVolumeClaimKind>(columns, 1);
Expand All @@ -36,6 +37,7 @@ const StorageTable: React.FC<StorageTableProps> = ({ pvcs: unsortedPvcs, refresh
obj={pvc}
onEditPVC={setEditPVC}
onDeletePVC={setDeleteStorage}
onAddPVC={onAddPVC}
/>
))}
</TableComposable>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Td,
Tr,
} from '@patternfly/react-table';
import { Text, Title } from '@patternfly/react-core';
import { Flex, FlexItem, Text, Title } from '@patternfly/react-core';
import { getPvcDescription, getPvcDisplayName } from '../../../utils';
import { PersistentVolumeClaimKind } from '../../../../../k8sTypes';
import { HddIcon } from '@patternfly/react-icons';
Expand All @@ -16,14 +16,21 @@ import ConnectedNotebookNames from '../../../notebook/ConnectedNotebookNames';
import { ConnectedNotebookContext } from '../../../notebook/useRelatedNotebooks';
import ResourceNameTooltip from '../../../components/ResourceNameTooltip';
import useIsRootVolume from './useIsRootVolume';
import StorageWarningStatus from './StorageWarningStatus';

type StorageTableRowProps = {
obj: PersistentVolumeClaimKind;
onDeletePVC: (pvc: PersistentVolumeClaimKind) => void;
onEditPVC: (pvc: PersistentVolumeClaimKind) => void;
onAddPVC: () => void;
};

const StorageTableRow: React.FC<StorageTableRowProps> = ({ obj, onDeletePVC, onEditPVC }) => {
const StorageTableRow: React.FC<StorageTableRowProps> = ({
obj,
onDeletePVC,
onEditPVC,
onAddPVC,
}) => {
const [isExpanded, setExpanded] = React.useState(false);
const isRootVolume = useIsRootVolume(obj);

Expand All @@ -50,9 +57,19 @@ const StorageTableRow: React.FC<StorageTableRowProps> = ({ obj, onDeletePVC, onE
<Tr>
<Td expand={{ rowIndex: 0, isExpanded, onToggle: () => setExpanded(!isExpanded) }} />
<Td dataLabel="Name">
<Title headingLevel="h4">
<ResourceNameTooltip resource={obj}>{getPvcDisplayName(obj)}</ResourceNameTooltip>
</Title>
<Flex
spaceItems={{ default: 'spaceItemsSm' }}
alignItems={{ default: 'alignItemsCenter' }}
>
<FlexItem>
<Title headingLevel="h4">
<ResourceNameTooltip resource={obj}>{getPvcDisplayName(obj)}</ResourceNameTooltip>
</Title>
</FlexItem>
<FlexItem>
<StorageWarningStatus obj={obj} onEditPVC={onEditPVC} onAddPVC={onAddPVC} />
</FlexItem>
</Flex>
<Text>{getPvcDescription(obj)}</Text>
</Td>
<Td dataLabel="Type">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import * as React from 'react';
import { Bullseye, Button, Icon, Popover } from '@patternfly/react-core';
import {
ExclamationCircleIcon,
ExclamationTriangleIcon,
InfoCircleIcon,
} from '@patternfly/react-icons';
import { PersistentVolumeClaimKind } from '../../../../../k8sTypes';
import { usePVCFreeAmount } from '../../../../../api';
import { getPvcTotalSize } from '../../../utils';
import { bytesAsGB } from '../../../../../utilities/number';
import { getFullStatusFromPercentage } from './utils';
import useStorageStatusAlert from './useStorageStatusAlert';

type StorageWarningStatusProps = {
obj: PersistentVolumeClaimKind;
onAddPVC: () => void;
onEditPVC: (pvc: PersistentVolumeClaimKind) => void;
};

const StorageWarningStatus: React.FC<StorageWarningStatusProps> = ({
obj,
onEditPVC,
onAddPVC,
}) => {
const [inUseInBytes, loaded] = usePVCFreeAmount(obj);
const percentage = loaded
? Number(((bytesAsGB(inUseInBytes) / parseFloat(getPvcTotalSize(obj))) * 100).toFixed(2))
: NaN;
useStorageStatusAlert(obj, percentage);

const percentageStatus = getFullStatusFromPercentage(percentage);

if (!percentageStatus) {
return null;
}

let renderIcon: React.ReactNode;
let popoverHeader: React.ReactNode;
let popoverBody: React.ReactNode;
if (percentageStatus === 'error') {
renderIcon = (
<Icon status="danger">
<ExclamationCircleIcon />
</Icon>
);
popoverHeader = `Storage full`;
popoverBody = 'Save actions will fail and you can no longer write new data.';
} else {
popoverHeader = `Storage ${percentage}% full`;
popoverBody =
'Once storage is 100% full, you will no longer be able to write new data and save actions will fail.';
if (percentageStatus === 'warning') {
renderIcon = (
<Icon status="warning">
<ExclamationTriangleIcon />
</Icon>
);
} else {
renderIcon = (
<Icon status="info">
<InfoCircleIcon />
</Icon>
);
}
}

return (
<Popover
position="right"
headerIcon={<Bullseye>{renderIcon}</Bullseye>}
headerContent={popoverHeader}
bodyContent={popoverBody}
footerContent={(hide) => (
<>
To increase available storage, delete files,{' '}
<Button
isInline
variant="link"
onClick={() => {
hide();
onEditPVC(obj);
}}
>
edit storage size
</Button>
, or{' '}
<Button
isInline
variant="link"
onClick={() => {
hide();
onAddPVC();
}}
>
add new cluster storage
</Button>
.
</>
)}
removeFindDomNode
>
<Bullseye>{renderIcon}</Bullseye>
</Popover>
);
};

export default StorageWarningStatus;

0 comments on commit 0fbbb3d

Please sign in to comment.