Skip to content
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"check-cycles": "CHECK_CYCLES=true yarn dev-once",
"coverage": "jest --coverage .",
"eslint": "node ./node_modules/.bin/eslint --max-warnings ${MAX_WARNINGS:-0} --color",
"lint": "NODE_OPTIONS=--max-old-space-size=4096 MAX_WARNINGS=339 yarn eslint --format ./scripts/eslint-exact-warnings.js .",
"lint": "NODE_OPTIONS=--max-old-space-size=4096 MAX_WARNINGS=338 yarn eslint --format ./scripts/eslint-exact-warnings.js .",
"gherkin-lint": "./node_modules/.bin/gherkin-lint -c ./packages/dev-console/integration-tests/.gherkin-lintrc ./packages/*/integration-tests/features",
"test": "LANG=en_US.UTF-8 jest",
"debug-test": "node --inspect-brk node_modules/.bin/jest --runInBand",
Expand Down
44 changes: 43 additions & 1 deletion frontend/public/components/__tests__/units.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import * as _ from 'lodash';
import { units, validate, convertToBaseValue, humanizePercentage } from '../utils/units';
import {
units,
validate,
convertToBaseValue,
humanizePercentage,
humanizeBinaryBytes,
} from '../utils/units';

describe('units', () => {
describe('round', () => {
Expand Down Expand Up @@ -372,3 +378,39 @@ describe('convert to base value', () => {
test_('1T', 1000000000000);
test_('1P', 1000000000000000);
});

describe('PVC capacity round-trip: convertToBaseValue → humanizeBinaryBytes', () => {
const test_ = (input, expectedString) => {
it(`${input} → ${expectedString}`, () => {
const base = convertToBaseValue(input);
expect(base).not.toBeNull();
expect(humanizeBinaryBytes(base).string).toEqual(expectedString);
});
};

// Binary units (K8s standard for storage)
test_('1Ki', '1 KiB');
test_('100Ki', '100 KiB');
test_('1Mi', '1 MiB');
test_('512Mi', '512 MiB');
test_('1Gi', '1 GiB');
test_('3Gi', '3 GiB');
test_('100Gi', '100 GiB');
test_('1Ti', '1 TiB');
test_('3Ti', '3 TiB');
test_('10Ti', '10 TiB');
test_('1Pi', '1 PiB');
test_('1Ei', '1 EiB');

// Multi-unit equivalences
test_('1024Ki', '1 MiB');
test_('1024Mi', '1 GiB');
test_('1024Gi', '1 TiB');
test_('3072Gi', '3 TiB');

// Decimal units (some CSI drivers may use these)
test_('1k', '1,000 B');
test_('1M', '976.6 KiB');
test_('1G', '953.7 MiB');
test_('1T', '931.3 GiB');
});
67 changes: 39 additions & 28 deletions frontend/public/components/persistent-volume-claim.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FC } from 'react';
import { useMemo, useCallback, Suspense, useState, useEffect } from 'react';
import { useMemo, useCallback, Suspense, useState } from 'react';
import { ChartDonut } from '@patternfly/react-charts/victory';
import {
Alert,
Expand Down Expand Up @@ -290,15 +290,12 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {
const volumeMode = pvc?.spec?.volumeMode;
const conditions = pvc?.status?.conditions;

// State to track dismissed alerts
const [isErrorAlertDismissed, setIsErrorAlertDismissed] = useState(false);
const [isInfoAlertDismissed, setIsInfoAlertDismissed] = useState(false);

// Reset alert dismiss states when PVC changes
useEffect(() => {
setIsErrorAlertDismissed(false);
setIsInfoAlertDismissed(false);
}, [pvc?.metadata?.uid]);
// Track which PVC uid the alert was dismissed for — automatically resets when PVC changes
const [errorAlertDismissedUid, setErrorAlertDismissedUid] = useState<string | undefined>();
const [infoAlertDismissedUid, setInfoAlertDismissedUid] = useState<string | undefined>();
const isErrorAlertDismissed =
!!pvc?.metadata?.uid && errorAlertDismissedUid === pvc?.metadata?.uid;
const isInfoAlertDismissed = !!pvc?.metadata?.uid && infoAlertDismissedUid === pvc?.metadata?.uid;

const query =
name && namespace
Expand All @@ -313,8 +310,10 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {

const totalCapacityMetric = convertToBaseValue(storage);
const totalRequestMetric = convertToBaseValue(requestedStorage);
const usedMetrics = response?.data?.result?.[0]?.value?.[1];
const availableMetrics = usedMetrics ? totalCapacityMetric - Number(usedMetrics) : null;
const usedMetricsRaw = response?.data?.result?.[0]?.value?.[1];
const usedMetrics = usedMetricsRaw != null ? Number(usedMetricsRaw) : undefined;
const availableMetrics =
usedMetrics != null && Number.isFinite(usedMetrics) ? totalCapacityMetric - usedMetrics : null;
const totalCapacity = humanizeBinaryBytes(totalCapacityMetric);
const availableCapacity = humanizeBinaryBytes(availableMetrics, undefined, totalCapacity.unit);
const usedCapacity = humanizeBinaryBytes(usedMetrics, undefined, totalCapacity.unit);
Expand All @@ -326,12 +325,13 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {

const totalCapacityString = `${Number(totalCapacity.value.toFixed(1))} ${totalCapacity.unit}`;

const donutData = usedMetrics
? [
{ x: i18next.t('public~Used'), y: usedCapacity.value },
{ x: i18next.t('public~Available'), y: availableCapacity.value },
]
: [{ x: i18next.t('public~Total'), y: totalCapacity.value }];
const donutData =
usedMetrics != null && Number.isFinite(usedMetrics)
? [
{ x: i18next.t('public~Used'), y: usedCapacity.value },
{ x: i18next.t('public~Available'), y: availableCapacity.value },
]
: [{ x: i18next.t('public~Total'), y: totalCapacity.value }];

const [pvcAlertExtensions] = useResolvedExtensions<PVCAlert>(isPVCAlert);

Expand All @@ -352,7 +352,11 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {
variant="danger"
title={t('VolumeAttributesClass modification failed')}
className="co-alert co-alert--margin-bottom-sm"
actionClose={<AlertActionCloseButton onClose={() => setIsErrorAlertDismissed(true)} />}
actionClose={
<AlertActionCloseButton
onClose={() => setErrorAlertDismissedUid(pvc?.metadata?.uid)}
/>
}
data-test="vac-error-alert"
data-test-id="vac-error-alert"
>
Expand All @@ -371,7 +375,11 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {
: t('VolumeAttributesClass modification in progress')
}
className="co-alert co-alert--margin-bottom-sm"
actionClose={<AlertActionCloseButton onClose={() => setIsInfoAlertDismissed(true)} />}
actionClose={
<AlertActionCloseButton
onClose={() => setInfoAlertDismissedUid(pvc?.metadata?.uid)}
/>
}
>
{!currentVolumeAttributesClassName
? t('VolumeAttributesClass "{{target}}" is pending application.', {
Expand Down Expand Up @@ -441,14 +449,17 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {
</DescriptionListDescription>
</DescriptionListGroup>
)}
{usedMetrics && _.isEmpty(loadError) && !loading && (
<DescriptionListGroup>
<DescriptionListTerm>{t('Used')}</DescriptionListTerm>
<DescriptionListDescription>
{humanizeBinaryBytes(usedMetrics).string}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{usedMetrics != null &&
Number.isFinite(usedMetrics) &&
_.isEmpty(loadError) &&
!loading && (
<DescriptionListGroup>
<DescriptionListTerm>{t('Used')}</DescriptionListTerm>
<DescriptionListDescription>
{humanizeBinaryBytes(usedMetrics).string}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{!_.isEmpty(accessModes) && (
<DescriptionListGroup>
<DescriptionListTerm>{t('Access modes')}</DescriptionListTerm>
Expand Down