Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add status popover when storage is nearly full #842

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 || 'NaN') || 1;
andrewballantyne marked this conversation as resolved.
Show resolved Hide resolved
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;