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$/),
})