Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-12075-fixed-1745266693581.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

Revert Object Storage Size Conversions from Base10 to Base2 ([#12075](https://github.com/linode/manager/pull/12075))
3 changes: 1 addition & 2 deletions packages/manager/src/components/Uploaders/FileUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ export const FileUpload = React.memo((props: FileUploadProps) => {
})}
variant="body1"
>
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(props.sizeInBytes, { base10: true }).formatted}
{readableBytes(props.sizeInBytes).formatted}
</StyledFileSizeTypography>
{props.percentCompleted === 100 ? (
<FileUploadComplete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ describe('ObjectDetailsDrawer', () => {
screen.findByText(/^Last modified: 2019-12-31/)
).resolves.toBeInTheDocument();

expect(screen.getByText('12.3 KB')).toBeVisible();
expect(screen.getByText(/^https:\/\/my-bucket/)).toBeVisible();
});

Expand All @@ -62,4 +61,28 @@ describe('ObjectDetailsDrawer', () => {
expect(() => screen.getByTestId('lastModified')).toThrow();
});
});

describe('readableBytes edge cases in Object Storage', () => {
it('displays bytes correctly for sizes under 1 KB threshold', () => {
renderWithTheme(<ObjectDetailsDrawer {...props} size={1000} />);

// Values under 1024 bytes should remain as bytes
expect(screen.getByText('1000 bytes')).toBeVisible();
});

it('uses base2 calculations (1024-based) for KB display', () => {
renderWithTheme(<ObjectDetailsDrawer {...props} size={1024} />);

// 1024 bytes = 1 KB in base2 (not 1.02 KB as it would be in base10)
expect(screen.getByText('1 KB')).toBeVisible();
});

it('converts base10 MB values correctly to base2 display', () => {
renderWithTheme(<ObjectDetailsDrawer {...props} size={1000000} />);

// 1,000,000 bytes / 1024 = 976.56 KB (rounded to 977 KB)
// Since this is less than 1 MB in base2, it displays as KB
expect(screen.getByText('977 KB')).toBeVisible();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ export const ObjectDetailsDrawer = React.memo(
>
{size ? (
<Typography variant="subtitle2">
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(size, { base10: true }).formatted}
{readableBytes(size).formatted}
</Typography>
) : null}
{formattedLastModified && Boolean(profile) ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export const ObjectTableRow = (props: Props) => {
<Grid
container
spacing={2}
wrap="nowrap"
sx={{
alignItems: 'center',
}}
wrap="nowrap"
>
<Grid className="py0">
<ObjectIcon size={20} />
Expand All @@ -56,10 +56,7 @@ export const ObjectTableRow = (props: Props) => {
</Grid>
</Grid>
</TableCell>
<TableCell noWrap>
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(objectSize, { base10: true }).formatted}
</TableCell>
<TableCell noWrap>{readableBytes(objectSize).formatted}</TableCell>
<Hidden mdDown>
<TableCell noWrap>
<DateTimeDisplay value={objectLastModified} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,7 @@ export const BucketDetailsDrawer = React.memo(
)}
{typeof size === 'number' && (
<Typography variant="subtitle2">
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(size, { base10: true }).formatted}
{readableBytes(size).formatted}
</Typography>
)}
{/* @TODO OBJ Multicluster: use region instead of cluster if isObjMultiClusterEnabled. */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ describe('ObjectStorageLanding', () => {
await screen.findByText(buckets[1].label);
});

it('renders a "Total usage" section if there is more than one Bucket', async () => {
it('renders a "Total usage" section using base2 calculations if there is more than one Bucket', async () => {
const buckets = objectStorageBucketFactory.buildList(2, {
size: 1e9 * 5,
size: 1024 * 1024 * 1024 * 5, // 5 GB in base2 (5 GiB)
});

// Mock Clusters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,7 @@ export const BucketLanding = (props: Props) => {
style={{ marginTop: 18, textAlign: 'center', width: '100%' }}
variant="body1"
>
Total storage used:{' '}
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(totalUsage, { base10: true }).formatted}
Total storage used: {readableBytes(totalUsage).formatted}
</Typography>
) : null}
<TransferDisplay
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@
getByText('test-bucket-001.alpha.linodeobjects.com');
});

it('should render a size with the correct size abbreviation', () => {
it('should render size with base2 calculations (displaying GB but representing GiB)', () => {
const { getByText } = renderWithTheme(
wrapWithTableBody(<BucketTableRow {...props} />)
);
getByText('5.42 GB');
getByText('5.05 GB');

Check warning on line 44 in packages/manager/src/features/ObjectStorage/BucketLanding/BucketTableRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Avoid destructuring queries from `render` result, use `screen.getByText` instead Raw Output: {"ruleId":"testing-library/prefer-screen-queries","severity":1,"message":"Avoid destructuring queries from `render` result, use `screen.getByText` instead","line":44,"column":5,"nodeType":"Identifier","messageId":"preferScreenQueries","endLine":44,"endColumn":14}

Check warning on line 44 in packages/manager/src/features/ObjectStorage/BucketLanding/BucketTableRow.test.tsx

View workflow job for this annotation

GitHub Actions / ESLint Review (manager)

[eslint] reported by reviewdog 🐢 Wrap stand-alone `getBy*` query with `expect` function for better explicit assertion Raw Output: {"ruleId":"testing-library/prefer-explicit-assert","severity":1,"message":"Wrap stand-alone `getBy*` query with `expect` function for better explicit assertion","line":44,"column":5,"nodeType":"Identifier","messageId":"preferExplicitAssert","endLine":44,"endColumn":14}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const BucketTableRow = (props: BucketTableRowProps) => {
<Typography data-qa-region variant="body1">
{isObjMultiClusterEnabled && regionsLookup && region
? regionsLookup[region].label
: clusterRegion?.label ?? cluster}
: (clusterRegion?.label ?? cluster)}
</Typography>
</StyledBucketRegionCell>
</Hidden>
Expand All @@ -109,8 +109,7 @@ export const BucketTableRow = (props: BucketTableRowProps) => {
</Hidden>
<StyledBucketSizeCell noWrap>
<Typography data-qa-size variant="body1">
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(size, { base10: true }).formatted}
{readableBytes(size).formatted}
</Typography>
</StyledBucketSizeCell>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,7 @@ export const OMC_BucketLanding = (props: Props) => {
style={{ marginTop: 18, textAlign: 'center', width: '100%' }}
variant="body1"
>
Total storage used:{' '}
{/* to convert from binary units (GiB) to decimal units (GB) we need to pass the base10 flag */}
{readableBytes(totalUsage, { base10: true }).formatted}
Total storage used: {readableBytes(totalUsage).formatted}
</Typography>
) : null}
<TransferDisplay spacingTop={buckets.length > 1 ? 8 : 18} />
Expand Down
36 changes: 27 additions & 9 deletions packages/utilities/src/helpers/unitConversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,35 @@ export interface ReadableBytesOptions {
base10?: boolean;
handleNegatives?: boolean;
maxUnit?: StorageSymbol;
round?: Partial<Record<StorageSymbol, number>> | number;
round?: number | Partial<Record<StorageSymbol, number>>;
unit?: StorageSymbol;
unitLabels?: Partial<Record<StorageSymbol, string>>;
}

export type StorageSymbol = 'GB' | 'KB' | 'MB' | 'TB' | 'byte' | 'bytes';
export type StorageSymbol = 'byte' | 'bytes' | 'GB' | 'KB' | 'MB' | 'TB';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple questions here:

  1. are we not receiving/displaying actual base2 metric names (GiB, KiB, etc). I believe this was an important consideration
  2. could those types be moved to APIv4 package?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. That is correct - That's an intentional decision from Product which appears to be in alignment with other industry practices
  2. Potentially! Let me look

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure it belongs in APIv4 - open to be convinced
On a side note, I think I'm going to follow-up on this PR and try to combine some redundancy from the Cloud Pulse utils packages/manager/src/features/CloudPulse/Utils/unitConversion.ts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough, i suppose API usually sends everything in bytes so yeah, maybe not. It feels more right to import this type from there (which I need to do) but not a big deal.


// This code inspired by: https://ourcodeworld.com/articles/read/713/converting-bytes-to-human-readable-values-kb-mb-gb-tb-pb-eb-zb-yb-with-javascript

/**
* Converts raw bytes to human-readable format using base2 calculations (1024-based)
* while displaying using common units (KB/MB/GB/TB).
*
* IMPORTANT: We intentionally use base2 calculations (1024 bytes = 1 KB) even though
* we display using traditional storage units. This aligns with industry practice. Internally these represent GiB, TiB, PiB values
* despite the displayed labels.
*
* To use base10 calculations (1000-based), set the base10 option to true.
* By default, calculations use base2 (1024-based).
*
* See: https://techdocs.akamai.com/cloud-computing/docs/understanding-how-billing-works#storage-units
*
* @param num - The number of bytes to convert.
* @param options - Options for the conversion.
* @returns An object containing the formatted value, unit, and value.
*/
export const readableBytes = (
num: number,
options: ReadableBytesOptions = {}
options: ReadableBytesOptions = {},
) => {
// These are the units Classic uses. This can easily be extended –
// just keep adding to this array and the corresponding interface.
Expand Down Expand Up @@ -126,7 +144,7 @@ export const readableBytes = (
export const determinePower = (
num: number,
storageUnits: StorageSymbol[],
options: ReadableBytesOptions
options: ReadableBytesOptions,
) => {
// If maxUnit has been supplied, use that
if (options.unit) {
Expand All @@ -136,7 +154,7 @@ export const determinePower = (

// Otherwise, we need to do some magic, which I don't 100% understand
const magicallyCalculatedPower = Math.floor(
Math.log(num) / Math.log(multiplier)
Math.log(num) / Math.log(multiplier),
);

// If the magically calculated power/unit is higher than the
Expand All @@ -154,7 +172,7 @@ export const determinePower = (
const determineDecimalPlaces = (
num: number,
unit: StorageSymbol,
options: ReadableBytesOptions = {}
options: ReadableBytesOptions = {},
) => {
if (typeof options.round === 'number') {
return options.round;
Expand All @@ -175,12 +193,12 @@ const determineDecimalPlaces = (

export const convertBytesToTarget = (
unit: StorageSymbol | StorageUnitExponentKey,
value: number
value: number,
) => {
switch (unit) {
case 'B':
case 'byte':
case 'bytes':
case 'B':
return value;
default:
return convertStorageUnit('B', value, unit);
Expand All @@ -207,7 +225,7 @@ type StorageUnitExponentKey = keyof typeof StorageUnitExponents;
export const convertStorageUnit = (
sourceUnit: StorageUnitExponentKey,
sourceQuantity: number | undefined,
targetUnit: StorageUnitExponentKey
targetUnit: StorageUnitExponentKey,
) => {
if (sourceQuantity === undefined) {
return 0;
Expand Down