From 3a845face8f066e031e93762294c8d60781f12bc Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Sun, 6 Oct 2024 16:18:49 -0700 Subject: [PATCH 01/12] Use dynamic calculations to determine proper unit sizing on capacity bars --- app/components/CapacityBars.tsx | 24 ++++++++++---- app/util/units.spec.ts | 57 +++++++++++++++++++++++++++++++++ app/util/units.ts | 47 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 app/util/units.spec.ts diff --git a/app/components/CapacityBars.tsx b/app/components/CapacityBars.tsx index f5d415cf1c..4bec4cb0f9 100644 --- a/app/components/CapacityBars.tsx +++ b/app/components/CapacityBars.tsx @@ -9,7 +9,7 @@ import type { VirtualResourceCounts } from '@oxide/api' import { Cpu16Icon, Ram16Icon, Ssd16Icon } from '@oxide/design-system/icons/react' -import { bytesToGiB, bytesToTiB } from '~/util/units' +import { bytesToSpecificUnit, getUnits } from '~/util/units' import { CapacityBar } from './CapacityBar' @@ -22,6 +22,16 @@ export const CapacityBars = ({ provisioned: VirtualResourceCounts allocatedLabel: string }) => { + // These will most likely be GiB, but calculating dynamically to handle larger configurations in the future + const memoryUnits = getUnits(Math.max(provisioned.memory, allocated.memory)) + const provisionedMemory = bytesToSpecificUnit(provisioned.memory, memoryUnits) + const allocatedMemory = bytesToSpecificUnit(allocated.memory, memoryUnits) + + // These will most likely be TiB, but calculating dynamically for the same reason as above + const storageUnits = getUnits(Math.max(provisioned.storage, allocated.storage)) + const provisionedStorage = bytesToSpecificUnit(provisioned.storage, storageUnits) + const allocatedStorage = bytesToSpecificUnit(allocated.storage, storageUnits) + return (
} title="MEMORY" - unit="GiB" - provisioned={bytesToGiB(provisioned.memory)} - capacity={bytesToGiB(allocated.memory)} + unit={memoryUnits} + provisioned={provisionedMemory} + capacity={allocatedMemory} capacityLabel={allocatedLabel} /> } title="STORAGE" - unit="TiB" - provisioned={bytesToTiB(provisioned.storage)} - capacity={bytesToTiB(allocated.storage)} + unit={storageUnits} + provisioned={provisionedStorage} + capacity={allocatedStorage} capacityLabel={allocatedLabel} />
diff --git a/app/util/units.spec.ts b/app/util/units.spec.ts new file mode 100644 index 0000000000..e698e85f4d --- /dev/null +++ b/app/util/units.spec.ts @@ -0,0 +1,57 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright Oxide Computer Company + */ +import { expect, it } from 'vitest' + +import { bytesToRationalNumber } from './units' + +function bytesToRationalNumberTest() { + // the basics + expect(bytesToRationalNumber(1024)).toEqual({ number: 1, unit: 'KiB' }) + expect(bytesToRationalNumber(1048576)).toEqual({ number: 1, unit: 'MiB' }) + expect(bytesToRationalNumber(1073741824)).toEqual({ number: 1, unit: 'GiB' }) + expect(bytesToRationalNumber(1099511627776)).toEqual({ number: 1, unit: 'TiB' }) + + // double those + expect(bytesToRationalNumber(2048)).toEqual({ number: 2, unit: 'KiB' }) + expect(bytesToRationalNumber(2097152)).toEqual({ number: 2, unit: 'MiB' }) + expect(bytesToRationalNumber(2147483648)).toEqual({ number: 2, unit: 'GiB' }) + expect(bytesToRationalNumber(2199023255552)).toEqual({ number: 2, unit: 'TiB' }) + + // just 1.5 now + expect(bytesToRationalNumber(1536)).toEqual({ number: 1.5, unit: 'KiB' }) + expect(bytesToRationalNumber(1572864)).toEqual({ number: 1.5, unit: 'MiB' }) + expect(bytesToRationalNumber(1610612736)).toEqual({ number: 1.5, unit: 'GiB' }) + expect(bytesToRationalNumber(1649267441664)).toEqual({ number: 1.5, unit: 'TiB' }) + + // let's do two decimal places (1.75) + expect(bytesToRationalNumber(1792)).toEqual({ number: 1.75, unit: 'KiB' }) + expect(bytesToRationalNumber(1835008)).toEqual({ number: 1.75, unit: 'MiB' }) + expect(bytesToRationalNumber(1879048192)).toEqual({ number: 1.75, unit: 'GiB' }) + expect(bytesToRationalNumber(1924145348608)).toEqual({ number: 1.75, unit: 'TiB' }) + + // and three decimal places (1.755) + expect(bytesToRationalNumber(1797.12, 3)).toEqual({ number: 1.755, unit: 'KiB' }) + expect(bytesToRationalNumber(1840250.88, 3)).toEqual({ number: 1.755, unit: 'MiB' }) + expect(bytesToRationalNumber(1884416901.12, 3)).toEqual({ number: 1.755, unit: 'GiB' }) + expect(bytesToRationalNumber(1929642906746.88, 3)).toEqual({ + number: 1.755, + unit: 'TiB', + }) + + // but if we only want two decimal places, it should round appropriately + // note the missing second argument, so we'll used the default decimal value, 2 + expect(bytesToRationalNumber(1797.12)).toEqual({ number: 1.76, unit: 'KiB' }) + expect(bytesToRationalNumber(1840250.88)).toEqual({ number: 1.76, unit: 'MiB' }) + expect(bytesToRationalNumber(1884416901.12)).toEqual({ number: 1.76, unit: 'GiB' }) + expect(bytesToRationalNumber(1929642906746.88)).toEqual({ + number: 1.76, + unit: 'TiB', + }) +} + +it('rounds to a rational number', bytesToRationalNumberTest) diff --git a/app/util/units.ts b/app/util/units.ts index e88a5672a5..bc30e171b1 100644 --- a/app/util/units.ts +++ b/app/util/units.ts @@ -12,5 +12,52 @@ export const MiB = 1024 * KiB export const GiB = 1024 * MiB export const TiB = 1024 * GiB +export const bytesToKiB = (b: number, digits = 2) => round(b / KiB, digits) +export const bytesToMiB = (b: number, digits = 2) => round(b / MiB, digits) export const bytesToGiB = (b: number, digits = 2) => round(b / GiB, digits) export const bytesToTiB = (b: number, digits = 2) => round(b / TiB, digits) + +type Unit = 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB' + +export type BytesToRationalNumber = { + number: number + unit: Unit +} + +export const bytesToRationalNumber = (b: number, digits = 2) => { + if (b < 1024) { + return { number: round(b, digits), unit: 'B' } + } + // 1024^2 = 1,048,576 + if (b < 1048576) { + return { number: bytesToKiB(b, digits), unit: 'KiB' } + } + // 1024^3 = 1,073,741,824 + if (b < 1073741824) { + return { number: bytesToMiB(b, digits), unit: 'MiB' } + } + // 1024^4 = 1,099,511,627,776 + if (b < 1099511627776) { + return { number: bytesToGiB(b, digits), unit: 'GiB' } + } + return { number: bytesToTiB(b, digits), unit: 'TiB' } +} + +// Used when we have multiple related numbers that might normally round to different units. +// Once the proper "unified" unit base is established, all numbers can be converted to a specific unit. +export const bytesToSpecificUnit = (b: number, unit: Unit, digits = 2) => { + if (unit === 'KiB') { + return bytesToKiB(b, digits) + } + if (unit === 'MiB') { + return bytesToMiB(b, digits) + } + if (unit === 'GiB') { + return bytesToGiB(b, digits) + } + if (unit === 'TiB') { + return bytesToTiB(b, digits) + } +} + +export const getUnits = (number: number) => bytesToRationalNumber(number).unit From 43b9e5153b7991421eb790054e0c0f976ab38e62 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Mon, 7 Oct 2024 00:10:04 -0400 Subject: [PATCH 02/12] refactor utilization page --- app/components/CapacityBars.tsx | 18 +++--- app/pages/system/UtilizationPage.tsx | 82 ++++++++++++++++++---------- app/util/units.spec.ts | 54 +++++++++--------- app/util/units.ts | 38 +++++-------- 4 files changed, 104 insertions(+), 88 deletions(-) diff --git a/app/components/CapacityBars.tsx b/app/components/CapacityBars.tsx index 4bec4cb0f9..af185c13a2 100644 --- a/app/components/CapacityBars.tsx +++ b/app/components/CapacityBars.tsx @@ -9,7 +9,7 @@ import type { VirtualResourceCounts } from '@oxide/api' import { Cpu16Icon, Ram16Icon, Ssd16Icon } from '@oxide/design-system/icons/react' -import { bytesToSpecificUnit, getUnits } from '~/util/units' +import { bytesToSpecificUnit, getUnit } from '~/util/units' import { CapacityBar } from './CapacityBar' @@ -23,14 +23,14 @@ export const CapacityBars = ({ allocatedLabel: string }) => { // These will most likely be GiB, but calculating dynamically to handle larger configurations in the future - const memoryUnits = getUnits(Math.max(provisioned.memory, allocated.memory)) - const provisionedMemory = bytesToSpecificUnit(provisioned.memory, memoryUnits) - const allocatedMemory = bytesToSpecificUnit(allocated.memory, memoryUnits) + const memoryUnit = getUnit(Math.max(provisioned.memory, allocated.memory)) + const provisionedMemory = bytesToSpecificUnit(provisioned.memory, memoryUnit) + const allocatedMemory = bytesToSpecificUnit(allocated.memory, memoryUnit) // These will most likely be TiB, but calculating dynamically for the same reason as above - const storageUnits = getUnits(Math.max(provisioned.storage, allocated.storage)) - const provisionedStorage = bytesToSpecificUnit(provisioned.storage, storageUnits) - const allocatedStorage = bytesToSpecificUnit(allocated.storage, storageUnits) + const storageUnit = getUnit(Math.max(provisioned.storage, allocated.storage)) + const provisionedStorage = bytesToSpecificUnit(provisioned.storage, storageUnit) + const allocatedStorage = bytesToSpecificUnit(allocated.storage, storageUnit) return (
@@ -46,7 +46,7 @@ export const CapacityBars = ({ } title="MEMORY" - unit={memoryUnits} + unit={memoryUnit} provisioned={provisionedMemory} capacity={allocatedMemory} capacityLabel={allocatedLabel} @@ -54,7 +54,7 @@ export const CapacityBars = ({ } title="STORAGE" - unit={storageUnits} + unit={storageUnit} provisioned={provisionedStorage} capacity={allocatedStorage} capacityLabel={allocatedLabel} diff --git a/app/pages/system/UtilizationPage.tsx b/app/pages/system/UtilizationPage.tsx index 8a1e4a5675..2c7152b5da 100644 --- a/app/pages/system/UtilizationPage.tsx +++ b/app/pages/system/UtilizationPage.tsx @@ -14,6 +14,7 @@ import { FLEET_ID, totalUtilization, usePrefetchedApiQuery, + type SiloUtilization, } from '@oxide/api' import { Metrics16Icon, Metrics24Icon } from '@oxide/design-system/icons/react' @@ -33,7 +34,7 @@ import { Tabs } from '~/ui/lib/Tabs' import { docLinks } from '~/util/links' import { round } from '~/util/math' import { pb } from '~/util/path-builder' -import { bytesToGiB, bytesToTiB } from '~/util/units' +import { bytesToGiB, bytesToSpecificUnit, bytesToTiB, getUnit } from '~/util/units' SystemUtilizationPage.loader = async () => { await Promise.all([ @@ -189,44 +190,22 @@ function UsageTab() { {silo.siloName} - + {getUsageCellForSilo(silo, 'cpus')} - + {getUsageCellForSilo(silo, 'memory')} - + {getUsageCellForSilo(silo, 'storage')} - + {getAvailableCellForSilo(silo, 'cpus')} - + {getAvailableCellForSilo(silo, 'memory')} - + {getAvailableCellForSilo(silo, 'storage')} @@ -257,6 +236,28 @@ const UsageCell = ({
) +const getUsageCellForSilo = ( + silo: SiloUtilization, + resource: 'cpus' | 'memory' | 'storage' +) => { + if (resource === 'cpus') { + return ( + + ) + } + const unit = getUnit(Math.max(silo.provisioned[resource], silo.allocated[resource])) + return ( + + ) +} + const AvailableCell = ({ provisioned, allocated, @@ -282,3 +283,26 @@ const AvailableCell = ({ ) } + +const getAvailableCellForSilo = ( + silo: SiloUtilization, + resource: 'cpus' | 'memory' | 'storage' +) => { + if (resource === 'cpus') { + return ( + + ) + } + // These will most likely be GiB, but calculating dynamically to handle larger configurations in the future + const unit = getUnit(Math.max(silo.provisioned[resource], silo.allocated[resource])) + return ( + + ) +} diff --git a/app/util/units.spec.ts b/app/util/units.spec.ts index e698e85f4d..6320fa0906 100644 --- a/app/util/units.spec.ts +++ b/app/util/units.spec.ts @@ -7,51 +7,51 @@ */ import { expect, it } from 'vitest' -import { bytesToRationalNumber } from './units' +import { bytesToReadableNumber } from './units' -function bytesToRationalNumberTest() { +function bytesToReadableNumberTest() { // the basics - expect(bytesToRationalNumber(1024)).toEqual({ number: 1, unit: 'KiB' }) - expect(bytesToRationalNumber(1048576)).toEqual({ number: 1, unit: 'MiB' }) - expect(bytesToRationalNumber(1073741824)).toEqual({ number: 1, unit: 'GiB' }) - expect(bytesToRationalNumber(1099511627776)).toEqual({ number: 1, unit: 'TiB' }) + expect(bytesToReadableNumber(1024)).toEqual({ number: 1, unit: 'KiB' }) + expect(bytesToReadableNumber(1048576)).toEqual({ number: 1, unit: 'MiB' }) + expect(bytesToReadableNumber(1073741824)).toEqual({ number: 1, unit: 'GiB' }) + expect(bytesToReadableNumber(1099511627776)).toEqual({ number: 1, unit: 'TiB' }) // double those - expect(bytesToRationalNumber(2048)).toEqual({ number: 2, unit: 'KiB' }) - expect(bytesToRationalNumber(2097152)).toEqual({ number: 2, unit: 'MiB' }) - expect(bytesToRationalNumber(2147483648)).toEqual({ number: 2, unit: 'GiB' }) - expect(bytesToRationalNumber(2199023255552)).toEqual({ number: 2, unit: 'TiB' }) + expect(bytesToReadableNumber(2048)).toEqual({ number: 2, unit: 'KiB' }) + expect(bytesToReadableNumber(2097152)).toEqual({ number: 2, unit: 'MiB' }) + expect(bytesToReadableNumber(2147483648)).toEqual({ number: 2, unit: 'GiB' }) + expect(bytesToReadableNumber(2199023255552)).toEqual({ number: 2, unit: 'TiB' }) // just 1.5 now - expect(bytesToRationalNumber(1536)).toEqual({ number: 1.5, unit: 'KiB' }) - expect(bytesToRationalNumber(1572864)).toEqual({ number: 1.5, unit: 'MiB' }) - expect(bytesToRationalNumber(1610612736)).toEqual({ number: 1.5, unit: 'GiB' }) - expect(bytesToRationalNumber(1649267441664)).toEqual({ number: 1.5, unit: 'TiB' }) + expect(bytesToReadableNumber(1536)).toEqual({ number: 1.5, unit: 'KiB' }) + expect(bytesToReadableNumber(1572864)).toEqual({ number: 1.5, unit: 'MiB' }) + expect(bytesToReadableNumber(1610612736)).toEqual({ number: 1.5, unit: 'GiB' }) + expect(bytesToReadableNumber(1649267441664)).toEqual({ number: 1.5, unit: 'TiB' }) // let's do two decimal places (1.75) - expect(bytesToRationalNumber(1792)).toEqual({ number: 1.75, unit: 'KiB' }) - expect(bytesToRationalNumber(1835008)).toEqual({ number: 1.75, unit: 'MiB' }) - expect(bytesToRationalNumber(1879048192)).toEqual({ number: 1.75, unit: 'GiB' }) - expect(bytesToRationalNumber(1924145348608)).toEqual({ number: 1.75, unit: 'TiB' }) + expect(bytesToReadableNumber(1792)).toEqual({ number: 1.75, unit: 'KiB' }) + expect(bytesToReadableNumber(1835008)).toEqual({ number: 1.75, unit: 'MiB' }) + expect(bytesToReadableNumber(1879048192)).toEqual({ number: 1.75, unit: 'GiB' }) + expect(bytesToReadableNumber(1924145348608)).toEqual({ number: 1.75, unit: 'TiB' }) // and three decimal places (1.755) - expect(bytesToRationalNumber(1797.12, 3)).toEqual({ number: 1.755, unit: 'KiB' }) - expect(bytesToRationalNumber(1840250.88, 3)).toEqual({ number: 1.755, unit: 'MiB' }) - expect(bytesToRationalNumber(1884416901.12, 3)).toEqual({ number: 1.755, unit: 'GiB' }) - expect(bytesToRationalNumber(1929642906746.88, 3)).toEqual({ + expect(bytesToReadableNumber(1797.12, 3)).toEqual({ number: 1.755, unit: 'KiB' }) + expect(bytesToReadableNumber(1840250.88, 3)).toEqual({ number: 1.755, unit: 'MiB' }) + expect(bytesToReadableNumber(1884416901.12, 3)).toEqual({ number: 1.755, unit: 'GiB' }) + expect(bytesToReadableNumber(1929642906746.88, 3)).toEqual({ number: 1.755, unit: 'TiB', }) // but if we only want two decimal places, it should round appropriately // note the missing second argument, so we'll used the default decimal value, 2 - expect(bytesToRationalNumber(1797.12)).toEqual({ number: 1.76, unit: 'KiB' }) - expect(bytesToRationalNumber(1840250.88)).toEqual({ number: 1.76, unit: 'MiB' }) - expect(bytesToRationalNumber(1884416901.12)).toEqual({ number: 1.76, unit: 'GiB' }) - expect(bytesToRationalNumber(1929642906746.88)).toEqual({ + expect(bytesToReadableNumber(1797.12)).toEqual({ number: 1.76, unit: 'KiB' }) + expect(bytesToReadableNumber(1840250.88)).toEqual({ number: 1.76, unit: 'MiB' }) + expect(bytesToReadableNumber(1884416901.12)).toEqual({ number: 1.76, unit: 'GiB' }) + expect(bytesToReadableNumber(1929642906746.88)).toEqual({ number: 1.76, unit: 'TiB', }) } -it('rounds to a rational number', bytesToRationalNumberTest) +it('rounds to a rational number', bytesToReadableNumberTest) diff --git a/app/util/units.ts b/app/util/units.ts index bc30e171b1..d5a14fdfd8 100644 --- a/app/util/units.ts +++ b/app/util/units.ts @@ -7,6 +7,9 @@ */ import { round } from './math' +// We only need to support up to TiB for now, but we can add more if needed +type BinaryUnit = 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB' // | 'PiB' | 'EiB' | 'ZiB' | 'YiB' + export const KiB = 1024 export const MiB = 1024 * KiB export const GiB = 1024 * MiB @@ -17,14 +20,9 @@ export const bytesToMiB = (b: number, digits = 2) => round(b / MiB, digits) export const bytesToGiB = (b: number, digits = 2) => round(b / GiB, digits) export const bytesToTiB = (b: number, digits = 2) => round(b / TiB, digits) -type Unit = 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB' - -export type BytesToRationalNumber = { - number: number - unit: Unit -} - -export const bytesToRationalNumber = (b: number, digits = 2) => { +type BytesToReadableNumber = { number: number; unit: BinaryUnit } +/** Takes a raw byte count and determines the appropriate unit to use in formatting it */ +export const bytesToReadableNumber = (b: number, digits = 2): BytesToReadableNumber => { if (b < 1024) { return { number: round(b, digits), unit: 'B' } } @@ -45,19 +43,13 @@ export const bytesToRationalNumber = (b: number, digits = 2) => { // Used when we have multiple related numbers that might normally round to different units. // Once the proper "unified" unit base is established, all numbers can be converted to a specific unit. -export const bytesToSpecificUnit = (b: number, unit: Unit, digits = 2) => { - if (unit === 'KiB') { - return bytesToKiB(b, digits) - } - if (unit === 'MiB') { - return bytesToMiB(b, digits) - } - if (unit === 'GiB') { - return bytesToGiB(b, digits) - } - if (unit === 'TiB') { - return bytesToTiB(b, digits) - } -} +export const bytesToSpecificUnit = (b: number, unit: BinaryUnit, digits = 2): number => + ({ + B: round(b, digits), + KiB: bytesToKiB(b, digits), + MiB: bytesToMiB(b, digits), + GiB: bytesToGiB(b, digits), + TiB: bytesToTiB(b, digits), + })[unit] -export const getUnits = (number: number) => bytesToRationalNumber(number).unit +export const getUnit = (bytes: number): BinaryUnit => bytesToReadableNumber(bytes).unit From 3ce595124aca2130fdbb52285c9c41a98b89f946 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 00:11:39 -0400 Subject: [PATCH 03/12] refactoring --- app/components/CapacityBars.tsx | 14 ++-- app/pages/system/UtilizationPage.tsx | 110 +++++++++++---------------- app/util/units.ts | 31 +++++--- 3 files changed, 71 insertions(+), 84 deletions(-) diff --git a/app/components/CapacityBars.tsx b/app/components/CapacityBars.tsx index af185c13a2..fac10773a6 100644 --- a/app/components/CapacityBars.tsx +++ b/app/components/CapacityBars.tsx @@ -9,7 +9,7 @@ import type { VirtualResourceCounts } from '@oxide/api' import { Cpu16Icon, Ram16Icon, Ssd16Icon } from '@oxide/design-system/icons/react' -import { bytesToSpecificUnit, getUnit } from '~/util/units' +import { useConvertBytesToSpecificUnit, useGetUnit } from '~/util/units' import { CapacityBar } from './CapacityBar' @@ -23,14 +23,14 @@ export const CapacityBars = ({ allocatedLabel: string }) => { // These will most likely be GiB, but calculating dynamically to handle larger configurations in the future - const memoryUnit = getUnit(Math.max(provisioned.memory, allocated.memory)) - const provisionedMemory = bytesToSpecificUnit(provisioned.memory, memoryUnit) - const allocatedMemory = bytesToSpecificUnit(allocated.memory, memoryUnit) + const memoryUnit = useGetUnit(provisioned.memory, allocated.memory) + const provisionedMemory = useConvertBytesToSpecificUnit(provisioned.memory, memoryUnit) + const allocatedMemory = useConvertBytesToSpecificUnit(allocated.memory, memoryUnit) // These will most likely be TiB, but calculating dynamically for the same reason as above - const storageUnit = getUnit(Math.max(provisioned.storage, allocated.storage)) - const provisionedStorage = bytesToSpecificUnit(provisioned.storage, storageUnit) - const allocatedStorage = bytesToSpecificUnit(allocated.storage, storageUnit) + const storageUnit = useGetUnit(provisioned.storage, allocated.storage) + const provisionedStorage = useConvertBytesToSpecificUnit(provisioned.storage, storageUnit) + const allocatedStorage = useConvertBytesToSpecificUnit(allocated.storage, storageUnit) return (
diff --git a/app/pages/system/UtilizationPage.tsx b/app/pages/system/UtilizationPage.tsx index 2c7152b5da..355e2f1f56 100644 --- a/app/pages/system/UtilizationPage.tsx +++ b/app/pages/system/UtilizationPage.tsx @@ -34,7 +34,13 @@ import { Tabs } from '~/ui/lib/Tabs' import { docLinks } from '~/util/links' import { round } from '~/util/math' import { pb } from '~/util/path-builder' -import { bytesToGiB, bytesToSpecificUnit, bytesToTiB, getUnit } from '~/util/units' +import { + bytesToGiB, + bytesToTiB, + useConvertBytesToSpecificUnit, + useGetUnit, + type BinaryUnit, +} from '~/util/units' SystemUtilizationPage.loader = async () => { await Promise.all([ @@ -190,22 +196,28 @@ function UsageTab() { {silo.siloName} - {getUsageCellForSilo(silo, 'cpus')} + - {getUsageCellForSilo(silo, 'memory')} + - {getUsageCellForSilo(silo, 'storage')} + - {getAvailableCellForSilo(silo, 'cpus')} + - {getAvailableCellForSilo(silo, 'memory')} + - {getAvailableCellForSilo(silo, 'storage')} + @@ -217,15 +229,9 @@ function UsageTab() { ) } -const UsageCell = ({ - provisioned, - allocated, - unit, -}: { - provisioned: number - allocated: number - unit?: string -}) => ( +type CellProps = { provisioned: number; allocated: number; unit?: BinaryUnit } + +const UsageCell = ({ provisioned, allocated, unit }: CellProps) => (
{provisioned} / @@ -236,37 +242,7 @@ const UsageCell = ({
) -const getUsageCellForSilo = ( - silo: SiloUtilization, - resource: 'cpus' | 'memory' | 'storage' -) => { - if (resource === 'cpus') { - return ( - - ) - } - const unit = getUnit(Math.max(silo.provisioned[resource], silo.allocated[resource])) - return ( - - ) -} - -const AvailableCell = ({ - provisioned, - allocated, - unit, -}: { - provisioned: number - allocated: number - unit?: string -}) => { +const AvailableCell = ({ provisioned, allocated, unit }: CellProps) => { const usagePercent = (provisioned / allocated) * 100 return (
@@ -284,25 +260,25 @@ const AvailableCell = ({ ) } -const getAvailableCellForSilo = ( - silo: SiloUtilization, - resource: 'cpus' | 'memory' | 'storage' -) => { - if (resource === 'cpus') { - return ( - - ) - } - // These will most likely be GiB, but calculating dynamically to handle larger configurations in the future - const unit = getUnit(Math.max(silo.provisioned[resource], silo.allocated[resource])) - return ( - +type SiloCellProps = { + silo: SiloUtilization + resource: 'memory' | 'storage' + cellType: 'usage' | 'available' +} + +// Used as a wrapper around the UsageCell and AvailableCell components, +// for rendering the silo's memory and storage resources +const SiloCell = ({ silo, resource, cellType }: SiloCellProps) => { + // Get the raw values from the silo object + const provisionedRaw = silo.provisioned[resource] + const allocatedRaw = silo.allocated[resource] + // Use those to get the standardized unit + const unit = useGetUnit(provisionedRaw, allocatedRaw) + const provisioned = useConvertBytesToSpecificUnit(provisionedRaw, unit) + const allocated = useConvertBytesToSpecificUnit(allocatedRaw, unit) + return cellType === 'usage' ? ( + + ) : ( + ) } diff --git a/app/util/units.ts b/app/util/units.ts index d5a14fdfd8..1de4c696a7 100644 --- a/app/util/units.ts +++ b/app/util/units.ts @@ -5,10 +5,12 @@ * * Copyright Oxide Computer Company */ +import { useMemo } from 'react' + import { round } from './math' // We only need to support up to TiB for now, but we can add more if needed -type BinaryUnit = 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB' // | 'PiB' | 'EiB' | 'ZiB' | 'YiB' +export type BinaryUnit = 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB' // | 'PiB' | 'EiB' | 'ZiB' | 'YiB' export const KiB = 1024 export const MiB = 1024 * KiB @@ -43,13 +45,22 @@ export const bytesToReadableNumber = (b: number, digits = 2): BytesToReadableNum // Used when we have multiple related numbers that might normally round to different units. // Once the proper "unified" unit base is established, all numbers can be converted to a specific unit. -export const bytesToSpecificUnit = (b: number, unit: BinaryUnit, digits = 2): number => - ({ - B: round(b, digits), - KiB: bytesToKiB(b, digits), - MiB: bytesToMiB(b, digits), - GiB: bytesToGiB(b, digits), - TiB: bytesToTiB(b, digits), - })[unit] +export const useConvertBytesToSpecificUnit = ( + bytes: number, + unit: BinaryUnit, + digits = 2 +): number => + useMemo( + () => + ({ + B: round(bytes, digits), + KiB: bytesToKiB(bytes, digits), + MiB: bytesToMiB(bytes, digits), + GiB: bytesToGiB(bytes, digits), + TiB: bytesToTiB(bytes, digits), + })[unit], + [bytes, digits, unit] + ) -export const getUnit = (bytes: number): BinaryUnit => bytesToReadableNumber(bytes).unit +export const useGetUnit = (n1: number, n2: number): BinaryUnit => + useMemo(() => bytesToReadableNumber(Math.max(n1, n2)).unit, [n1, n2]) From b34c02eb3d2db55628ebe50752e6aadf2aaf1467 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 01:22:23 -0400 Subject: [PATCH 04/12] Add e2e test, update SiloQuotasTab --- app/pages/system/silos/SiloQuotasTab.tsx | 20 +++++++++++---- test/e2e/utilization.e2e.ts | 31 ++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/app/pages/system/silos/SiloQuotasTab.tsx b/app/pages/system/silos/SiloQuotasTab.tsx index 14df8fbb74..848e2aa840 100644 --- a/app/pages/system/silos/SiloQuotasTab.tsx +++ b/app/pages/system/silos/SiloQuotasTab.tsx @@ -23,7 +23,7 @@ import { Message } from '~/ui/lib/Message' import { Table } from '~/ui/lib/Table' import { classed } from '~/util/classed' import { links } from '~/util/links' -import { bytesToGiB, GiB } from '~/util/units' +import { bytesToGiB, GiB, useConvertBytesToSpecificUnit, useGetUnit } from '~/util/units' const Unit = classed.span`ml-1 text-tertiary` @@ -34,6 +34,16 @@ export function SiloQuotasTab() { }) const { allocated: quotas, provisioned } = utilization + const memoryUnits = useGetUnit(provisioned.memory, quotas.memory) + const provisionedMemory = useConvertBytesToSpecificUnit(provisioned.memory, memoryUnits) + const quotasMemory = useConvertBytesToSpecificUnit(quotas.memory, memoryUnits) + + const storageUnits = useGetUnit(provisioned.storage, quotas.storage) + const provisionedStorage = useConvertBytesToSpecificUnit( + provisioned.storage, + storageUnits + ) + const quotasStorage = useConvertBytesToSpecificUnit(quotas.storage, storageUnits) const [editing, setEditing] = useState(false) @@ -60,19 +70,19 @@ export function SiloQuotasTab() { Memory - {bytesToGiB(provisioned.memory)} GiB + {provisionedMemory} {memoryUnits} - {bytesToGiB(quotas.memory)} GiB + {quotasMemory} {memoryUnits} Storage - {bytesToGiB(provisioned.storage)} GiB + {provisionedStorage} {storageUnits} - {bytesToGiB(quotas.storage)} GiB + {quotasStorage} {storageUnits} diff --git a/test/e2e/utilization.e2e.ts b/test/e2e/utilization.e2e.ts index f37b1a17ed..eed6808682 100644 --- a/test/e2e/utilization.e2e.ts +++ b/test/e2e/utilization.e2e.ts @@ -116,6 +116,37 @@ test.describe('Silo utilization', () => { }) }) +test('Utilization shows correct units for CPU, memory, and storage', async ({ page }) => { + await page.goto('/system/utilization') + + // Verify the original values for the Memory tile + await expect(page.getByText('Memory(GIB)')).toBeVisible() + await expect(page.getByText('Provisioned384 GiB')).toBeVisible() + await expect(page.getByText('Quota (Total)800 GiB')).toBeVisible() + + // Navigate to the quotas tab + await page.goto('system/silos/maze-war?tab=quotas') + + // Verify that there's a row for memory with the correct units + await expect(page.getByText('Memory234 GiB300 GiB')).toBeVisible() + + // Edit the quota and verify the new value + await page.getByRole('button', { name: 'Edit quotas' }).click() + await page.getByRole('textbox', { name: 'Memory (GiB)' }).fill('3000') + await page.getByRole('button', { name: 'Update quotas' }).click() + // The table has been updated + await expect(page.getByText('Memory0.23 TiB2.93 TiB')).toBeVisible() + + // Navigate back to the utilization page without refreshing + await page.getByRole('link', { name: 'Utilization' }).click() + await expect(page.getByRole('heading', { name: 'Utilization' })).toBeVisible() + + // Verify the updated values for the Memory tile + await expect(page.getByText('Memory(TiB)')).toBeVisible() + await expect(page.getByText('Provisioned0.38 TiB')).toBeVisible() + await expect(page.getByText('Quota (Total)3.42 TiB')).toBeVisible() +}) + // TODO: it would be nice to test that actual data shows up in the graphs and // the date range picker works as expected, but it's hard to do asserts about // the graphs because they're big SVGs, the data coming from MSW is randomized, From 01d69314fa8a8e350e2a4844a5cfa161f382b6b1 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 01:44:38 -0400 Subject: [PATCH 05/12] Update tests --- test/e2e/silos.e2e.ts | 4 ++-- test/e2e/utilization.e2e.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index b37b2e862a..c1c567e02d 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -314,8 +314,8 @@ test('Quotas tab', async ({ page }) => { }) await expectRowVisible(table, { Resource: 'Storage', - Provisioned: '4403.2 GiB', - Quota: '7168 GiB', + Provisioned: '4.3 TiB', + Quota: '7 TiB', }) const sideModal = page.getByRole('dialog', { name: 'Edit quotas' }) diff --git a/test/e2e/utilization.e2e.ts b/test/e2e/utilization.e2e.ts index eed6808682..597d7de498 100644 --- a/test/e2e/utilization.e2e.ts +++ b/test/e2e/utilization.e2e.ts @@ -93,8 +93,8 @@ test.describe('System utilization', () => { await expectRowVisible(page.getByRole('table'), { Silo: 'all-zeros', CPU: '0', - Memory: '0 GiB', - Storage: '0 TiB', + Memory: '0 B', + Storage: '0 B', }) }) }) From d21e4e1e751520d29dfae9659db736acd936c6a4 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 08:32:22 -0400 Subject: [PATCH 06/12] Missed a test update --- test/e2e/silos.e2e.ts | 2 +- test/e2e/utilization.e2e.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index c1c567e02d..c37a49798b 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -344,5 +344,5 @@ test('Quotas tab', async ({ page }) => { // only one changes, the others stay the same await expectRowVisible(table, { Resource: 'CPU', Quota: '50 vCPUs' }) await expectRowVisible(table, { Resource: 'Memory', Quota: '50 GiB' }) - await expectRowVisible(table, { Resource: 'Storage', Quota: '7168 GiB' }) + await expectRowVisible(table, { Resource: 'Storage', Quota: '7 TiB' }) }) diff --git a/test/e2e/utilization.e2e.ts b/test/e2e/utilization.e2e.ts index 597d7de498..a314573625 100644 --- a/test/e2e/utilization.e2e.ts +++ b/test/e2e/utilization.e2e.ts @@ -128,14 +128,17 @@ test('Utilization shows correct units for CPU, memory, and storage', async ({ pa await page.goto('system/silos/maze-war?tab=quotas') // Verify that there's a row for memory with the correct units - await expect(page.getByText('Memory234 GiB300 GiB')).toBeVisible() + const table = page.getByRole('table') + await expectRowVisible(table, { Provisioned: '234 GiB', Quota: '300 GiB' }) + await expect(page.getByText('2.93 TiB')).toBeHidden() // Edit the quota and verify the new value await page.getByRole('button', { name: 'Edit quotas' }).click() await page.getByRole('textbox', { name: 'Memory (GiB)' }).fill('3000') await page.getByRole('button', { name: 'Update quotas' }).click() - // The table has been updated - await expect(page.getByText('Memory0.23 TiB2.93 TiB')).toBeVisible() + // Verify that the table has been updated + await expectRowVisible(table, { Provisioned: '0.23 TiB', Quota: '2.93 TiB' }) + await expect(page.getByText('300 GiB')).toBeHidden() // Navigate back to the utilization page without refreshing await page.getByRole('link', { name: 'Utilization' }).click() From f6219f180c92398fa9d64725eb313e3c0e18ecfb Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 15:30:36 -0400 Subject: [PATCH 07/12] refactor, based on PR feedback --- app/components/CapacityBars.tsx | 14 +++--- app/pages/system/UtilizationPage.tsx | 10 ++--- app/pages/system/silos/SiloQuotasTab.tsx | 17 +++---- app/util/units.spec.ts | 54 +++++++++++------------ app/util/units.ts | 56 +++++++----------------- 5 files changed, 63 insertions(+), 88 deletions(-) diff --git a/app/components/CapacityBars.tsx b/app/components/CapacityBars.tsx index fac10773a6..6ecb478462 100644 --- a/app/components/CapacityBars.tsx +++ b/app/components/CapacityBars.tsx @@ -9,7 +9,7 @@ import type { VirtualResourceCounts } from '@oxide/api' import { Cpu16Icon, Ram16Icon, Ssd16Icon } from '@oxide/design-system/icons/react' -import { useConvertBytesToSpecificUnit, useGetUnit } from '~/util/units' +import { formatBytesAs, getUnit } from '~/util/units' import { CapacityBar } from './CapacityBar' @@ -23,14 +23,14 @@ export const CapacityBars = ({ allocatedLabel: string }) => { // These will most likely be GiB, but calculating dynamically to handle larger configurations in the future - const memoryUnit = useGetUnit(provisioned.memory, allocated.memory) - const provisionedMemory = useConvertBytesToSpecificUnit(provisioned.memory, memoryUnit) - const allocatedMemory = useConvertBytesToSpecificUnit(allocated.memory, memoryUnit) + const memoryUnit = getUnit(Math.max(provisioned.memory, allocated.memory)) + const provisionedMemory = formatBytesAs(provisioned.memory, memoryUnit) + const allocatedMemory = formatBytesAs(allocated.memory, memoryUnit) // These will most likely be TiB, but calculating dynamically for the same reason as above - const storageUnit = useGetUnit(provisioned.storage, allocated.storage) - const provisionedStorage = useConvertBytesToSpecificUnit(provisioned.storage, storageUnit) - const allocatedStorage = useConvertBytesToSpecificUnit(allocated.storage, storageUnit) + const storageUnit = getUnit(Math.max(provisioned.storage, allocated.storage)) + const provisionedStorage = formatBytesAs(provisioned.storage, storageUnit) + const allocatedStorage = formatBytesAs(allocated.storage, storageUnit) return (
diff --git a/app/pages/system/UtilizationPage.tsx b/app/pages/system/UtilizationPage.tsx index 355e2f1f56..4fa263fc78 100644 --- a/app/pages/system/UtilizationPage.tsx +++ b/app/pages/system/UtilizationPage.tsx @@ -37,8 +37,8 @@ import { pb } from '~/util/path-builder' import { bytesToGiB, bytesToTiB, - useConvertBytesToSpecificUnit, - useGetUnit, + formatBytesAs, + getUnit, type BinaryUnit, } from '~/util/units' @@ -273,9 +273,9 @@ const SiloCell = ({ silo, resource, cellType }: SiloCellProps) => { const provisionedRaw = silo.provisioned[resource] const allocatedRaw = silo.allocated[resource] // Use those to get the standardized unit - const unit = useGetUnit(provisionedRaw, allocatedRaw) - const provisioned = useConvertBytesToSpecificUnit(provisionedRaw, unit) - const allocated = useConvertBytesToSpecificUnit(allocatedRaw, unit) + const unit = getUnit(Math.max(provisionedRaw, allocatedRaw)) + const provisioned = formatBytesAs(provisionedRaw, unit) + const allocated = formatBytesAs(allocatedRaw, unit) return cellType === 'usage' ? ( ) : ( diff --git a/app/pages/system/silos/SiloQuotasTab.tsx b/app/pages/system/silos/SiloQuotasTab.tsx index 848e2aa840..d24b8fa41a 100644 --- a/app/pages/system/silos/SiloQuotasTab.tsx +++ b/app/pages/system/silos/SiloQuotasTab.tsx @@ -23,7 +23,7 @@ import { Message } from '~/ui/lib/Message' import { Table } from '~/ui/lib/Table' import { classed } from '~/util/classed' import { links } from '~/util/links' -import { bytesToGiB, GiB, useConvertBytesToSpecificUnit, useGetUnit } from '~/util/units' +import { bytesToGiB, formatBytesAs, getUnit, GiB } from '~/util/units' const Unit = classed.span`ml-1 text-tertiary` @@ -34,16 +34,13 @@ export function SiloQuotasTab() { }) const { allocated: quotas, provisioned } = utilization - const memoryUnits = useGetUnit(provisioned.memory, quotas.memory) - const provisionedMemory = useConvertBytesToSpecificUnit(provisioned.memory, memoryUnits) - const quotasMemory = useConvertBytesToSpecificUnit(quotas.memory, memoryUnits) + const memoryUnits = getUnit(Math.max(provisioned.memory, quotas.memory)) + const provisionedMemory = formatBytesAs(provisioned.memory, memoryUnits) + const quotasMemory = formatBytesAs(quotas.memory, memoryUnits) - const storageUnits = useGetUnit(provisioned.storage, quotas.storage) - const provisionedStorage = useConvertBytesToSpecificUnit( - provisioned.storage, - storageUnits - ) - const quotasStorage = useConvertBytesToSpecificUnit(quotas.storage, storageUnits) + const storageUnits = getUnit(Math.max(provisioned.storage, quotas.storage)) + const provisionedStorage = formatBytesAs(provisioned.storage, storageUnits) + const quotasStorage = formatBytesAs(quotas.storage, storageUnits) const [editing, setEditing] = useState(false) diff --git a/app/util/units.spec.ts b/app/util/units.spec.ts index 6320fa0906..d324c982d4 100644 --- a/app/util/units.spec.ts +++ b/app/util/units.spec.ts @@ -7,51 +7,51 @@ */ import { expect, it } from 'vitest' -import { bytesToReadableNumber } from './units' +import { formatBytes } from './units' -function bytesToReadableNumberTest() { +function formatBytesTest() { // the basics - expect(bytesToReadableNumber(1024)).toEqual({ number: 1, unit: 'KiB' }) - expect(bytesToReadableNumber(1048576)).toEqual({ number: 1, unit: 'MiB' }) - expect(bytesToReadableNumber(1073741824)).toEqual({ number: 1, unit: 'GiB' }) - expect(bytesToReadableNumber(1099511627776)).toEqual({ number: 1, unit: 'TiB' }) + expect(formatBytes(1024)).toEqual({ number: 1, unit: 'KiB' }) + expect(formatBytes(1048576)).toEqual({ number: 1, unit: 'MiB' }) + expect(formatBytes(1073741824)).toEqual({ number: 1, unit: 'GiB' }) + expect(formatBytes(1099511627776)).toEqual({ number: 1, unit: 'TiB' }) // double those - expect(bytesToReadableNumber(2048)).toEqual({ number: 2, unit: 'KiB' }) - expect(bytesToReadableNumber(2097152)).toEqual({ number: 2, unit: 'MiB' }) - expect(bytesToReadableNumber(2147483648)).toEqual({ number: 2, unit: 'GiB' }) - expect(bytesToReadableNumber(2199023255552)).toEqual({ number: 2, unit: 'TiB' }) + expect(formatBytes(2048)).toEqual({ number: 2, unit: 'KiB' }) + expect(formatBytes(2097152)).toEqual({ number: 2, unit: 'MiB' }) + expect(formatBytes(2147483648)).toEqual({ number: 2, unit: 'GiB' }) + expect(formatBytes(2199023255552)).toEqual({ number: 2, unit: 'TiB' }) // just 1.5 now - expect(bytesToReadableNumber(1536)).toEqual({ number: 1.5, unit: 'KiB' }) - expect(bytesToReadableNumber(1572864)).toEqual({ number: 1.5, unit: 'MiB' }) - expect(bytesToReadableNumber(1610612736)).toEqual({ number: 1.5, unit: 'GiB' }) - expect(bytesToReadableNumber(1649267441664)).toEqual({ number: 1.5, unit: 'TiB' }) + expect(formatBytes(1536)).toEqual({ number: 1.5, unit: 'KiB' }) + expect(formatBytes(1572864)).toEqual({ number: 1.5, unit: 'MiB' }) + expect(formatBytes(1610612736)).toEqual({ number: 1.5, unit: 'GiB' }) + expect(formatBytes(1649267441664)).toEqual({ number: 1.5, unit: 'TiB' }) // let's do two decimal places (1.75) - expect(bytesToReadableNumber(1792)).toEqual({ number: 1.75, unit: 'KiB' }) - expect(bytesToReadableNumber(1835008)).toEqual({ number: 1.75, unit: 'MiB' }) - expect(bytesToReadableNumber(1879048192)).toEqual({ number: 1.75, unit: 'GiB' }) - expect(bytesToReadableNumber(1924145348608)).toEqual({ number: 1.75, unit: 'TiB' }) + expect(formatBytes(1792)).toEqual({ number: 1.75, unit: 'KiB' }) + expect(formatBytes(1835008)).toEqual({ number: 1.75, unit: 'MiB' }) + expect(formatBytes(1879048192)).toEqual({ number: 1.75, unit: 'GiB' }) + expect(formatBytes(1924145348608)).toEqual({ number: 1.75, unit: 'TiB' }) // and three decimal places (1.755) - expect(bytesToReadableNumber(1797.12, 3)).toEqual({ number: 1.755, unit: 'KiB' }) - expect(bytesToReadableNumber(1840250.88, 3)).toEqual({ number: 1.755, unit: 'MiB' }) - expect(bytesToReadableNumber(1884416901.12, 3)).toEqual({ number: 1.755, unit: 'GiB' }) - expect(bytesToReadableNumber(1929642906746.88, 3)).toEqual({ + expect(formatBytes(1797.12, 3)).toEqual({ number: 1.755, unit: 'KiB' }) + expect(formatBytes(1840250.88, 3)).toEqual({ number: 1.755, unit: 'MiB' }) + expect(formatBytes(1884416901.12, 3)).toEqual({ number: 1.755, unit: 'GiB' }) + expect(formatBytes(1929642906746.88, 3)).toEqual({ number: 1.755, unit: 'TiB', }) // but if we only want two decimal places, it should round appropriately // note the missing second argument, so we'll used the default decimal value, 2 - expect(bytesToReadableNumber(1797.12)).toEqual({ number: 1.76, unit: 'KiB' }) - expect(bytesToReadableNumber(1840250.88)).toEqual({ number: 1.76, unit: 'MiB' }) - expect(bytesToReadableNumber(1884416901.12)).toEqual({ number: 1.76, unit: 'GiB' }) - expect(bytesToReadableNumber(1929642906746.88)).toEqual({ + expect(formatBytes(1797.12)).toEqual({ number: 1.76, unit: 'KiB' }) + expect(formatBytes(1840250.88)).toEqual({ number: 1.76, unit: 'MiB' }) + expect(formatBytes(1884416901.12)).toEqual({ number: 1.76, unit: 'GiB' }) + expect(formatBytes(1929642906746.88)).toEqual({ number: 1.76, unit: 'TiB', }) } -it('rounds to a rational number', bytesToReadableNumberTest) +it('rounds to a rational number', formatBytesTest) diff --git a/app/util/units.ts b/app/util/units.ts index 1de4c696a7..2f0489018f 100644 --- a/app/util/units.ts +++ b/app/util/units.ts @@ -5,12 +5,11 @@ * * Copyright Oxide Computer Company */ -import { useMemo } from 'react' import { round } from './math' // We only need to support up to TiB for now, but we can add more if needed -export type BinaryUnit = 'B' | 'KiB' | 'MiB' | 'GiB' | 'TiB' // | 'PiB' | 'EiB' | 'ZiB' | 'YiB' +export type BinaryUnit = 'KiB' | 'MiB' | 'GiB' | 'TiB' // | 'PiB' | 'EiB' | 'ZiB' | 'YiB' export const KiB = 1024 export const MiB = 1024 * KiB @@ -22,45 +21,24 @@ export const bytesToMiB = (b: number, digits = 2) => round(b / MiB, digits) export const bytesToGiB = (b: number, digits = 2) => round(b / GiB, digits) export const bytesToTiB = (b: number, digits = 2) => round(b / TiB, digits) -type BytesToReadableNumber = { number: number; unit: BinaryUnit } +type FormattedBytes = { number: number; unit: BinaryUnit } + +const unitSize: Record = { KiB, MiB, GiB, TiB } + +export function getUnit(b: number): BinaryUnit { + if (b < MiB) return 'KiB' + if (b < GiB) return 'MiB' + if (b < TiB) return 'GiB' + return 'TiB' +} + /** Takes a raw byte count and determines the appropriate unit to use in formatting it */ -export const bytesToReadableNumber = (b: number, digits = 2): BytesToReadableNumber => { - if (b < 1024) { - return { number: round(b, digits), unit: 'B' } - } - // 1024^2 = 1,048,576 - if (b < 1048576) { - return { number: bytesToKiB(b, digits), unit: 'KiB' } - } - // 1024^3 = 1,073,741,824 - if (b < 1073741824) { - return { number: bytesToMiB(b, digits), unit: 'MiB' } - } - // 1024^4 = 1,099,511,627,776 - if (b < 1099511627776) { - return { number: bytesToGiB(b, digits), unit: 'GiB' } - } - return { number: bytesToTiB(b, digits), unit: 'TiB' } +export const formatBytes = (b: number, digits = 2): FormattedBytes => { + const unit = getUnit(b) + return { number: formatBytesAs(b, unit, digits), unit } } // Used when we have multiple related numbers that might normally round to different units. // Once the proper "unified" unit base is established, all numbers can be converted to a specific unit. -export const useConvertBytesToSpecificUnit = ( - bytes: number, - unit: BinaryUnit, - digits = 2 -): number => - useMemo( - () => - ({ - B: round(bytes, digits), - KiB: bytesToKiB(bytes, digits), - MiB: bytesToMiB(bytes, digits), - GiB: bytesToGiB(bytes, digits), - TiB: bytesToTiB(bytes, digits), - })[unit], - [bytes, digits, unit] - ) - -export const useGetUnit = (n1: number, n2: number): BinaryUnit => - useMemo(() => bytesToReadableNumber(Math.max(n1, n2)).unit, [n1, n2]) +export const formatBytesAs = (bytes: number, unit: BinaryUnit, digits = 2): number => + round(bytes / unitSize[unit], digits) From 4603aa499a5baecc11a2e81d64734bc7d1c4f302 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Wed, 9 Oct 2024 17:39:13 -0400 Subject: [PATCH 08/12] Fix test --- test/e2e/utilization.e2e.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/utilization.e2e.ts b/test/e2e/utilization.e2e.ts index a314573625..d535cb3f52 100644 --- a/test/e2e/utilization.e2e.ts +++ b/test/e2e/utilization.e2e.ts @@ -93,8 +93,8 @@ test.describe('System utilization', () => { await expectRowVisible(page.getByRole('table'), { Silo: 'all-zeros', CPU: '0', - Memory: '0 B', - Storage: '0 B', + Memory: '0 KiB', + Storage: '0 KiB', }) }) }) From 7f1d534e240d862f71e6c06e053f727a92277626 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 10 Oct 2024 11:03:25 -0400 Subject: [PATCH 09/12] Ensure percentage calculation happens with original values --- app/pages/system/UtilizationPage.tsx | 30 ++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/app/pages/system/UtilizationPage.tsx b/app/pages/system/UtilizationPage.tsx index 4fa263fc78..24e907a2c9 100644 --- a/app/pages/system/UtilizationPage.tsx +++ b/app/pages/system/UtilizationPage.tsx @@ -211,6 +211,7 @@ function UsageTab() { @@ -242,8 +243,12 @@ const UsageCell = ({ provisioned, allocated, unit }: CellProps) => (
) -const AvailableCell = ({ provisioned, allocated, unit }: CellProps) => { - const usagePercent = (provisioned / allocated) * 100 +const AvailableCell = ({ + provisioned, + allocated, + unit, + usagePercent, +}: CellProps & { usagePercent: number }) => { return (
@@ -262,12 +267,13 @@ const AvailableCell = ({ provisioned, allocated, unit }: CellProps) => { type SiloCellProps = { silo: SiloUtilization + // CPUs have simpler data representations, so we don't use SiloCell for them resource: 'memory' | 'storage' cellType: 'usage' | 'available' } -// Used as a wrapper around the UsageCell and AvailableCell components, -// for rendering the silo's memory and storage resources +/** A wrapper around the UsageCell and AvailableCell components, + for rendering the silo's memory and storage resources */ const SiloCell = ({ silo, resource, cellType }: SiloCellProps) => { // Get the raw values from the silo object const provisionedRaw = silo.provisioned[resource] @@ -276,9 +282,17 @@ const SiloCell = ({ silo, resource, cellType }: SiloCellProps) => { const unit = getUnit(Math.max(provisionedRaw, allocatedRaw)) const provisioned = formatBytesAs(provisionedRaw, unit) const allocated = formatBytesAs(allocatedRaw, unit) - return cellType === 'usage' ? ( - - ) : ( - + if (cellType === 'usage') + return + + // Use the original values to calculate the usage percentage + const usagePercent = (provisionedRaw / allocatedRaw) * 100 + return ( + ) } From e91151fa635c809c14716f93b591665acbad67ef Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 10 Oct 2024 11:38:12 -0400 Subject: [PATCH 10/12] More interesting mock data --- mock-api/silo.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/mock-api/silo.ts b/mock-api/silo.ts index fb14ccd4c3..8e01ba3f46 100644 --- a/mock-api/silo.ts +++ b/mock-api/silo.ts @@ -44,14 +44,14 @@ export const siloQuotas: Json = [ { silo_id: silos[0].id, cpus: 50, - memory: 300 * GiB, - storage: 7 * TiB, + memory: 306.55 * GiB, + storage: 7.91 * TiB, }, { silo_id: silos[1].id, cpus: 34, - memory: 500 * GiB, - storage: 9 * TiB, + memory: 500.02 * GiB, + storage: 9.36 * TiB, }, ] @@ -62,14 +62,14 @@ export const siloProvisioned: Json = [ { silo_id: silos[0].id, cpus: 30, - memory: 234 * GiB, - storage: 4.3 * TiB, + memory: 234.31 * GiB, + storage: 4.339 * TiB, }, { silo_id: silos[1].id, cpus: 8, - memory: 150 * GiB, - storage: 2 * TiB, + memory: 150.529 * GiB, + storage: 2.75 * TiB, }, ] From 942ddc4fcdc69d51dbad77e21a3ab6e7cf2585b8 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 10 Oct 2024 11:46:00 -0400 Subject: [PATCH 11/12] Update utilization test --- test/e2e/utilization.e2e.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/test/e2e/utilization.e2e.ts b/test/e2e/utilization.e2e.ts index d535cb3f52..46677aa53d 100644 --- a/test/e2e/utilization.e2e.ts +++ b/test/e2e/utilization.e2e.ts @@ -23,21 +23,21 @@ test.describe('System utilization', () => { await page.goto('/system/utilization') await expect(page.getByRole('heading', { name: 'Utilization' })).toBeVisible() - await expect(page.getByText('Provisioned384 GiB')).toBeVisible() + await expect(page.getByText('Provisioned384.84 GiB')).toBeVisible() await expect(page.getByText('Provisioned / Quota')).toBeVisible() const table = page.getByRole('table') await expectRowVisible(table, { CPU: '20', - Storage: '2.7 TiB', - Memory: '66 GiB', + Storage: '3.57 TiB', + Memory: '72.24 GiB', Silo: 'maze-war', }) await expectRowVisible(table, { CPU: '26', - Storage: '7 TiB', - Memory: '350 GiB', + Storage: '6.61 TiB', + Memory: '349.49 GiB', Silo: 'myriad', }) @@ -104,7 +104,7 @@ test.describe('Silo utilization', () => { await page.goto('/utilization') await expect(page.getByRole('heading', { name: 'Utilization' })).toBeVisible() // Capacity bars are showing up - await expect(page.getByText('Provisioned234 GiB')).toBeVisible() + await expect(page.getByText('Provisioned234.31 GiB')).toBeVisible() }) test('works for dev user', async ({ browser }) => { @@ -112,7 +112,7 @@ test.describe('Silo utilization', () => { await page.goto('/utilization') await expect(page.getByRole('heading', { name: 'Utilization' })).toBeVisible() // Capacity bars are showing up - await expect(page.getByText('Provisioned234 GiB')).toBeVisible() + await expect(page.getByText('Provisioned234.31 GiB')).toBeVisible() }) }) @@ -121,15 +121,15 @@ test('Utilization shows correct units for CPU, memory, and storage', async ({ pa // Verify the original values for the Memory tile await expect(page.getByText('Memory(GIB)')).toBeVisible() - await expect(page.getByText('Provisioned384 GiB')).toBeVisible() - await expect(page.getByText('Quota (Total)800 GiB')).toBeVisible() + await expect(page.getByText('Provisioned384.84 GiB')).toBeVisible() + await expect(page.getByText('Quota (Total)806.57 GiB')).toBeVisible() // Navigate to the quotas tab await page.goto('system/silos/maze-war?tab=quotas') // Verify that there's a row for memory with the correct units const table = page.getByRole('table') - await expectRowVisible(table, { Provisioned: '234 GiB', Quota: '300 GiB' }) + await expectRowVisible(table, { Provisioned: '234.31 GiB', Quota: '306.55 GiB' }) await expect(page.getByText('2.93 TiB')).toBeHidden() // Edit the quota and verify the new value @@ -138,7 +138,7 @@ test('Utilization shows correct units for CPU, memory, and storage', async ({ pa await page.getByRole('button', { name: 'Update quotas' }).click() // Verify that the table has been updated await expectRowVisible(table, { Provisioned: '0.23 TiB', Quota: '2.93 TiB' }) - await expect(page.getByText('300 GiB')).toBeHidden() + await expect(page.getByText('306.55 GiB')).toBeHidden() // Navigate back to the utilization page without refreshing await page.getByRole('link', { name: 'Utilization' }).click() From 5c788744a3d554c3d729f079f5fa12dade9d8041 Mon Sep 17 00:00:00 2001 From: Charlie Park Date: Thu, 10 Oct 2024 11:56:34 -0400 Subject: [PATCH 12/12] A few more test updates --- test/e2e/silos.e2e.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/e2e/silos.e2e.ts b/test/e2e/silos.e2e.ts index c37a49798b..4eca426a79 100644 --- a/test/e2e/silos.e2e.ts +++ b/test/e2e/silos.e2e.ts @@ -309,13 +309,13 @@ test('Quotas tab', async ({ page }) => { }) await expectRowVisible(table, { Resource: 'Memory', - Provisioned: '234 GiB', - Quota: '300 GiB', + Provisioned: '234.31 GiB', + Quota: '306.55 GiB', }) await expectRowVisible(table, { Resource: 'Storage', - Provisioned: '4.3 TiB', - Quota: '7 TiB', + Provisioned: '4.34 TiB', + Quota: '7.91 TiB', }) const sideModal = page.getByRole('dialog', { name: 'Edit quotas' }) @@ -344,5 +344,5 @@ test('Quotas tab', async ({ page }) => { // only one changes, the others stay the same await expectRowVisible(table, { Resource: 'CPU', Quota: '50 vCPUs' }) await expectRowVisible(table, { Resource: 'Memory', Quota: '50 GiB' }) - await expectRowVisible(table, { Resource: 'Storage', Quota: '7 TiB' }) + await expectRowVisible(table, { Resource: 'Storage', Quota: '7.91 TiB' }) })