diff --git a/app/pages/project/instances/InstancesPage.tsx b/app/pages/project/instances/InstancesPage.tsx index f3558491f..17a18d62c 100644 --- a/app/pages/project/instances/InstancesPage.tsx +++ b/app/pages/project/instances/InstancesPage.tsx @@ -39,6 +39,7 @@ import { Tooltip } from '~/ui/lib/Tooltip' import { setDiff } from '~/util/array' import { toLocaleTimeString } from '~/util/date' import { pb } from '~/util/path-builder' +import { pluralize } from '~/util/str' import { useMakeInstanceActions } from './actions' import { ResizeInstanceModal } from './instance/InstancePage' @@ -99,7 +100,8 @@ export function Component() { header: 'CPU', cell: (info) => ( <> - {info.getValue()} vCPU + {info.getValue()}{' '} + {pluralize('vCPU', info.getValue())} ), }), diff --git a/app/pages/project/instances/instance/InstancePage.tsx b/app/pages/project/instances/instance/InstancePage.tsx index 60d0a50a1..2975c7900 100644 --- a/app/pages/project/instances/instance/InstancePage.tsx +++ b/app/pages/project/instances/instance/InstancePage.tsx @@ -52,6 +52,7 @@ import { Spinner } from '~/ui/lib/Spinner' import { Tooltip } from '~/ui/lib/Tooltip' import { truncate } from '~/ui/lib/Truncate' import { pb } from '~/util/path-builder' +import { pluralize } from '~/util/str' import { GiB } from '~/util/units' import { useMakeInstanceActions } from '../actions' @@ -221,7 +222,7 @@ export function InstancePage() { {instance.ncpus} - vCPUs + {pluralize(' vCPU', instance.ncpus)} {memory.value} diff --git a/app/util/str.spec.tsx b/app/util/str.spec.tsx index 19fc93d56..8b774bc9a 100644 --- a/app/util/str.spec.tsx +++ b/app/util/str.spec.tsx @@ -14,6 +14,7 @@ import { extractText, kebabCase, normalizeName, + pluralize, titleCase, } from './str' @@ -23,6 +24,14 @@ describe('capitalize', () => { }) }) +describe('pluralize', () => { + it('pluralizes correctly', () => { + expect(pluralize('item', 0)).toBe('items') + expect(pluralize('item', 1)).toBe('item') + expect(pluralize('item', 2)).toBe('items') + }) +}) + describe('camelCase', () => { it('basic formats to camel case', () => { expect(camelCase('camelCase')).toBe('camelCase') @@ -51,8 +60,9 @@ it('commaSeries', () => { expect(commaSeries([], 'or')).toBe('') expect(commaSeries(['a'], 'or')).toBe('a') expect(commaSeries(['a', 'b'], 'or')).toBe('a or b') - expect(commaSeries(['a', 'b'], 'or')).toBe('a or b') + expect(commaSeries(['a', 'b'], 'and')).toBe('a and b') expect(commaSeries(['a', 'b', 'c'], 'or')).toBe('a, b, or c') + expect(commaSeries(['a', 'b', 'c'], 'and')).toBe('a, b, and c') }) describe('titleCase', () => { diff --git a/app/util/str.ts b/app/util/str.ts index a7614fc89..1e5b5f684 100644 --- a/app/util/str.ts +++ b/app/util/str.ts @@ -10,7 +10,7 @@ import React, { type ReactElement, type ReactNode } from 'react' export const capitalize = (s: string) => s && s.charAt(0).toUpperCase() + s.slice(1) -export const pluralize = (s: string, n: number) => `${n} ${s}${n === 1 ? '' : 's'}` +export const pluralize = (s: string, n: number) => `${s}${n === 1 ? '' : 's'}` export const camelCase = (s: string) => s diff --git a/test/e2e/instance.e2e.ts b/test/e2e/instance.e2e.ts index 2e6bab3b7..c1e61bdbd 100644 --- a/test/e2e/instance.e2e.ts +++ b/test/e2e/instance.e2e.ts @@ -161,7 +161,7 @@ test('can resize a failed or stopped instance', async ({ page }) => { // resize 'you-fail', currently in a failed state await expectRowVisible(table, { name: 'you-fail', - CPU: '4 vCPU', + CPU: '4 vCPUs', Memory: '6 GiB', state: expect.stringMatching(/^failed\d+s$/), }) @@ -173,7 +173,7 @@ test('can resize a failed or stopped instance', async ({ page }) => { await resizeModal.getByRole('button', { name: 'Resize' }).click() await expectRowVisible(table, { name: 'you-fail', - CPU: '10 vCPU', + CPU: '10 vCPUs', Memory: '20 GiB', state: expect.stringMatching(/^failed\d+s$/), }) @@ -181,7 +181,7 @@ test('can resize a failed or stopped instance', async ({ page }) => { // resize 'db1', which needs to be stopped first await expectRowVisible(table, { name: 'db1', - CPU: '2 vCPU', + CPU: '2 vCPUs', Memory: '4 GiB', state: expect.stringMatching(/^running\d+s$/), }) @@ -200,7 +200,7 @@ test('can resize a failed or stopped instance', async ({ page }) => { await resizeModal.getByRole('button', { name: 'Resize' }).click() await expectRowVisible(table, { name: 'db1', - CPU: '8 vCPU', + CPU: '8 vCPUs', Memory: '16 GiB', state: expect.stringMatching(/^stopped\d+s$/), }) @@ -224,19 +224,19 @@ test('instance table', async ({ page }) => { const table = page.getByRole('table') await expectRowVisible(table, { name: 'db1', - CPU: '2 vCPU', + CPU: '2 vCPUs', Memory: '4 GiB', state: expect.stringMatching(/^running\d+s$/), }) await expectRowVisible(table, { name: 'you-fail', - CPU: '4 vCPU', + CPU: '4 vCPUs', Memory: '6 GiB', state: expect.stringMatching(/^failed\d+s$/), }) await expectRowVisible(table, { name: 'not-there-yet', - CPU: '2 vCPU', + CPU: '2 vCPUs', Memory: '8 GiB', state: expect.stringMatching(/^starting\d+s$/), })