Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 additions & 2 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@
"no-restricted-imports": [
"error",
{
// prevent confusion due to auto-imports and barrel files
"paths": ["."],
"paths": [
{
"name": ".",
"message": "Import from the file directly to avoid confusion due to auto-imports and barrel files."
},
{
"name": "filesize",
"message": "Import formatBytes from ~/util/units instead."
}
],
"patterns": [
// import all CSS except index.css at top level through CSS @import statements
// to avoid bad ordering situations. See https://github.com/oxidecomputer/console/pull/2035
Expand Down
5 changes: 2 additions & 3 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { filesize } from 'filesize'
import { useMemo } from 'react'
import { useController, useForm, type Control } from 'react-hook-form'
import { match } from 'ts-pattern'
Expand Down Expand Up @@ -43,7 +42,7 @@ import { TipIcon } from '~/ui/lib/TipIcon'
import { toLocaleDateString } from '~/util/date'
import { docLinks } from '~/util/links'
import { diskSizeNearest10 } from '~/util/math'
import { bytesToGiB, GiB } from '~/util/units'
import { bytesToGiB, formatBytes, GiB } from '~/util/units'

/**
* Same as DiskSource but with image and snapshot ID optional, reflecting The
Expand Down Expand Up @@ -416,7 +415,7 @@ const SnapshotSelectField = ({ control }: { control: Control<DiskCreateForm> })
label="Source snapshot"
placeholder="Select a snapshot"
items={snapshots.map((i) => {
const formattedSize = filesize(i.size, { base: 2, output: 'object' })
const formattedSize = formatBytes(i.size)
return {
value: i.id,
selectedLabel: i.name,
Expand Down
4 changes: 2 additions & 2 deletions app/forms/image-from-snapshot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*
* Copyright Oxide Computer Company
*/
import { filesize } from 'filesize'
import { useForm } from 'react-hook-form'
import { useNavigate, type LoaderFunctionArgs } from 'react-router'

Expand All @@ -31,6 +30,7 @@ import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { docLinks } from '~/util/links'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
import { formatBytes } from '~/util/units'

const defaultValues: Omit<ImageCreate, 'source'> = {
name: '',
Expand Down Expand Up @@ -94,7 +94,7 @@ export default function CreateImageFromSnapshotSideModalForm() {
<PropertiesTable.Row label="Snapshot">{data.name}</PropertiesTable.Row>
<PropertiesTable.Row label="Project">{project}</PropertiesTable.Row>
<PropertiesTable.Row label="Size">
{filesize(data.size, { base: 2 })}
{formatBytes(data.size).label}
</PropertiesTable.Row>
</PropertiesTable>

Expand Down
8 changes: 4 additions & 4 deletions app/forms/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/
import { skipToken, useQuery } from '@tanstack/react-query'
import cn from 'classnames'
import { filesize } from 'filesize'
import pMap from 'p-map'
import pRetry from 'p-retry'
import { useRef, useState } from 'react'
Expand Down Expand Up @@ -50,10 +49,11 @@ import { invariant } from '~/util/invariant'
import { docLinks, links } from '~/util/links'
import { pb } from '~/util/path-builder'
import { isAllZeros } from '~/util/str'
import { GiB, KiB } from '~/util/units'
import { formatBytes, GiB, KiB } from '~/util/units'

/** Format file size with two decimal points */
const fsize = (bytes: number) => filesize(bytes, { base: 2, pad: true })
// Padded because otherwise the numbers jump around a bit, e.g., when it goes
// from 10.55 to 14.7 to 19.23
const fsize = (bytes: number) => formatBytes(bytes, { pad: true }).label

type FormValues = {
imageName: string
Expand Down
5 changes: 2 additions & 3 deletions app/pages/project/instances/InstancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* Copyright Oxide Computer Company
*/
import { useQuery } from '@tanstack/react-query'
import { filesize } from 'filesize'
import { useId, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Link, useNavigate, type LoaderFunctionArgs } from 'react-router'
Expand Down Expand Up @@ -56,7 +55,7 @@ import { truncate } from '~/ui/lib/Truncate'
import { instanceMetricsBase, pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
import { pluralize } from '~/util/str'
import { GiB } from '~/util/units'
import { formatBytes, GiB } from '~/util/units'

import { useMakeInstanceActions } from './actions'

Expand Down Expand Up @@ -168,7 +167,7 @@ export default function InstancePage() {
enabled: !!primaryVpcId,
})

const memory = filesize(instance.memory, { output: 'object', base: 2 })
const memory = formatBytes(instance.memory)

return (
<>
Expand Down
4 changes: 2 additions & 2 deletions app/pages/project/instances/InstancesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/
import { useQuery, type UseQueryOptions } from '@tanstack/react-query'
import { createColumnHelper } from '@tanstack/react-table'
import { filesize } from 'filesize'
import { useMemo, useRef, useState } from 'react'
import { type LoaderFunctionArgs } from 'react-router'

Expand Down Expand Up @@ -42,6 +41,7 @@ import { ALL_ISH } from '~/util/consts'
import { toLocaleTimeString } from '~/util/date'
import { pb } from '~/util/path-builder'
import { pluralize } from '~/util/str'
import { formatBytes } from '~/util/units'

import { useMakeInstanceActions } from './actions'
import { ResizeInstanceModal } from './InstancePage'
Expand Down Expand Up @@ -113,7 +113,7 @@ export default function InstancesPage() {
colHelper.accessor('memory', {
header: 'Memory',
cell: (info) => {
const memory = filesize(info.getValue(), { output: 'object', base: 2 })
const memory = formatBytes(info.getValue())
return (
<>
{memory.value} <span className="text-tertiary ml-1">{memory.unit}</span>
Expand Down
4 changes: 2 additions & 2 deletions app/pages/system/inventory/sled/SledPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*
* Copyright Oxide Computer Company
*/
import { filesize } from 'filesize'
import type { LoaderFunctionArgs } from 'react-router'

import { api, q, queryClient, usePrefetchedQuery } from '@oxide/api'
Expand All @@ -19,6 +18,7 @@ import { PropertiesTable } from '~/ui/lib/PropertiesTable'
import { truncate } from '~/ui/lib/Truncate'
import { pb } from '~/util/path-builder'
import type * as PP from '~/util/path-params'
import { formatBytes } from '~/util/units'

import { ProvisionPolicyBadge, SledKindBadge, SledStateBadge } from './SledBadges'

Expand All @@ -38,7 +38,7 @@ export default function SledPage() {
const { sledId } = useSledParams()
const { data: sled } = usePrefetchedQuery(sledView({ sledId }))

const ram = filesize(sled.usablePhysicalRam, { output: 'object', base: 2 })
const ram = formatBytes(sled.usablePhysicalRam)

return (
<>
Expand Down
6 changes: 3 additions & 3 deletions app/table/cells/InstanceResourceCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
*
* Copyright Oxide Computer Company
*/
import { filesize } from 'filesize'

import type { Instance } from '@oxide/api'

import { formatBytes } from '~/util/units'

type Props = { value: Pick<Instance, 'ncpus' | 'memory'> }

export const InstanceResourceCell = ({ value }: Props) => {
const memory = filesize(value.memory, { output: 'object', base: 2 })
const memory = formatBytes(value.memory)
return (
<div className="space-y-0.5">
<div>
Expand Down
5 changes: 2 additions & 3 deletions app/table/columns/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@
* Copyright Oxide Computer Company
*/

import { filesize } from 'filesize'

import type { InstanceState } from '~/api'
import { InstanceStateBadge } from '~/components/StateBadge'
import { DescriptionCell } from '~/table/cells/DescriptionCell'
import { CopyToClipboard } from '~/ui/lib/CopyToClipboard'
import { DateTime } from '~/ui/lib/DateTime'
import { formatBytes } from '~/util/units'

// the full type of the info arg is CellContext<Row, Item> from RT, but in these
// cells we only care about the return value of getValue
Expand Down Expand Up @@ -40,7 +39,7 @@ function instanceStateCell(info: Info<InstanceState>) {

// not using Info<number> so this can also be used for minitables
export function sizeCellInner(value: number) {
const size = filesize(value, { base: 2, output: 'object' })
const size = formatBytes(value)
return (
<span className="text-default">
{size.value} <span className="text-tertiary">{size.unit}</span>
Expand Down
6 changes: 2 additions & 4 deletions app/ui/lib/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import { filesize } from 'filesize'
import {
useRef,
useState,
Expand All @@ -20,6 +19,7 @@ import { mergeRefs } from 'react-merge-refs'
import { Document16Icon, Error16Icon } from '@oxide/design-system/icons/react'

import { Truncate } from '~/ui/lib/Truncate'
import { formatBytes } from '~/util/units'

export type FileInputProps = Omit<ComponentProps<'input'>, 'type' | 'onChange'> & {
onChange: (f: File | null) => void
Expand Down Expand Up @@ -99,9 +99,7 @@ export function FileInput({
{file && !dragOver ? (
<div className="text-raise flex items-center">
<Truncate text={file.name} maxLength={32} position="middle" />
<span className="text-tertiary ml-1">
({filesize(file.size, { base: 2, pad: true })})
</span>
<span className="text-tertiary ml-1">({formatBytes(file.size).label})</span>
<button
type="button"
onClick={handleResetInput}
Expand Down
35 changes: 35 additions & 0 deletions app/util/units.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 { formatBytes, GiB, KiB, MiB } from './units'

it.each([
// bytes: never padded because fractional bytes don't make sense
[0, undefined, '0', 'B', '0 B'],
[0, { pad: true }, '0', 'B', '0 B'],
[5, undefined, '5', 'B', '5 B'],
[5, { pad: true }, '5', 'B', '5 B'],
[500, { pad: true }, '500', 'B', '500 B'],
// fractional inputs are rounded to whole bytes
[2.5, { pad: true }, '3', 'B', '3 B'],
[0.5, { pad: true }, '1', 'B', '1 B'],
// KiB and larger: scaled but not padded
[1023.5, undefined, '1', 'KiB', '1 KiB'],
[KiB, undefined, '1', 'KiB', '1 KiB'],
[1500, undefined, '1.46', 'KiB', '1.46 KiB'],
[MiB, undefined, '1', 'MiB', '1 MiB'],
[GiB, undefined, '1', 'GiB', '1 GiB'],
// padding is opt-in for scaled units
[KiB, { pad: true }, '1.00', 'KiB', '1.00 KiB'],
[1500, { pad: true }, '1.46', 'KiB', '1.46 KiB'],
[MiB, { pad: true }, '1.00', 'MiB', '1.00 MiB'],
[GiB, { pad: true }, '1.00', 'GiB', '1.00 GiB'],
])('formatBytes(%d, %o)', (bytes, opts, value, unit, label) => {
expect(formatBytes(bytes, opts)).toEqual({ value, unit, label })
})
37 changes: 37 additions & 0 deletions app/util/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
*
* Copyright Oxide Computer Company
*/
// formatBytes wraps filesize so the rest of the app gets a consistent API
// eslint-disable-next-line no-restricted-imports
import { filesize } from 'filesize'

import { round } from './math'

export const KiB = 1024
Expand All @@ -14,3 +18,36 @@ export const TiB = 1024 * GiB

export const bytesToGiB = (b: number, digits = 2) => round(b / GiB, digits)
export const bytesToTiB = (b: number, digits = 2) => round(b / TiB, digits)

export type FormattedBytes = {
/** Numeric portion of the formatted byte count, e.g. `1.5`. */
value: string
/** Binary unit label, e.g. `KiB`. */
unit: string
/** Full display string combining `value` and `unit`, e.g. `1.5 KiB`. */
label: string
}

type FormatBytesOptions = {
/** Pad scaled units to two decimal places, e.g. `1.00 KiB`. Bytes are never padded. */
pad?: boolean
}

/**
* Format a byte count for display, e.g. `1.5 KiB`. Always uses base 2 (binary
* units like KiB, MiB). Bytes are never padded because fractional bytes don't
* make sense, even when `pad` is true.
*/
export function formatBytes(
bytes: number,
{ pad = false }: FormatBytesOptions = {}
): FormattedBytes {
const { value, unit } = filesize(bytes, { base: 2, output: 'object' })
const formattedValue = pad && unit !== 'B' ? Number(value).toFixed(2) : String(value)

return {
value: formattedValue,
unit,
label: `${formattedValue} ${unit}`,
}
}
Loading