Skip to content

Commit 83ace42

Browse files
authored
Convert quotas to GiB before posting (#2141)
* convert quotas to GiB before posting! * e2e test it * handle NaN pct utilization in CapacityBar
1 parent 9270a93 commit 83ace42

File tree

9 files changed

+107
-21
lines changed

9 files changed

+107
-21
lines changed

app/components/CapacityBar.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ function TitleCell({ icon, title, unit }: TitleCellProps) {
6262
}
6363

6464
function PctCell({ pct }: { pct: number }) {
65+
// NaN happens when both top and bottom are 0
66+
if (Number.isNaN(pct)) {
67+
return (
68+
<div className="flex -translate-y-0.5 items-baseline text-quaternary">
69+
<div className="font-light text-sans-2xl"></div>
70+
<div className="text-sans-xl">%</div>
71+
</div>
72+
)
73+
}
74+
6575
const [wholeNumber, decimal] = splitDecimal(pct)
6676
return (
6777
<div className="flex -translate-y-0.5 items-baseline">

app/forms/silo-create.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { SideModalForm } from '~/components/form/SideModalForm'
2020
import { useForm, useToast } from '~/hooks'
2121
import { FormDivider } from '~/ui/lib/Divider'
2222
import { pb } from '~/util/path-builder'
23+
import { GiB } from '~/util/units'
2324

2425
export type SiloCreateFormValues = Omit<SiloCreate, 'mappedFleetRoles'> & {
2526
siloAdminGetsFleetAdmin: boolean
@@ -74,6 +75,7 @@ export function CreateSiloSideModalForm() {
7475
adminGroupName,
7576
siloAdminGetsFleetAdmin,
7677
siloViewerGetsFleetViewer,
78+
quotas,
7779
...rest
7880
}) => {
7981
const mappedFleetRoles: SiloCreate['mappedFleetRoles'] = {}
@@ -88,6 +90,11 @@ export function CreateSiloSideModalForm() {
8890
// no point setting it to empty string or whitespace
8991
adminGroupName: adminGroupName?.trim() || undefined,
9092
mappedFleetRoles,
93+
quotas: {
94+
cpus: quotas.cpus,
95+
memory: quotas.memory * GiB,
96+
storage: quotas.storage * GiB,
97+
},
9198
...rest,
9299
},
93100
})

mock-api/msw/db.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,14 @@ export const lookup = {
236236

237237
export function utilizationForSilo(silo: Json<Api.Silo>) {
238238
const quotas = db.siloQuotas.find((q) => q.silo_id === silo.id)
239-
if (!quotas) throw internalError()
239+
if (!quotas) {
240+
throw internalError(`no entry in db.siloQuotas for silo ${silo.name}`)
241+
}
240242

241243
const provisioned = db.siloProvisioned.find((p) => p.silo_id === silo.id)
242-
if (!provisioned) throw internalError()
244+
if (!provisioned) {
245+
throw internalError(`no entry in db.siloProvisioned for silo ${silo.name}`)
246+
}
243247

244248
return {
245249
allocated: pick(quotas, 'cpus', 'storage', 'memory'),

mock-api/msw/handlers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1130,7 +1130,7 @@ export const handlers = makeHandlers({
11301130
requireFleetViewer(cookies)
11311131
return paginated(query, db.silos)
11321132
},
1133-
siloCreate({ body, cookies }) {
1133+
siloCreate({ body: { quotas, ...body }, cookies }) {
11341134
requireFleetViewer(cookies)
11351135
errIfExists(db.silos, { name: body.name })
11361136
const newSilo: Json<Api.Silo> = {
@@ -1140,6 +1140,8 @@ export const handlers = makeHandlers({
11401140
mapped_fleet_roles: body.mapped_fleet_roles || {},
11411141
}
11421142
db.silos.push(newSilo)
1143+
db.siloQuotas.push({ silo_id: newSilo.id, ...quotas })
1144+
db.siloProvisioned.push({ silo_id: newSilo.id, cpus: 0, memory: 0, storage: 0 })
11431145
return json(newSilo, { status: 201 })
11441146
},
11451147
siloView({ path, cookies }) {

mock-api/msw/util.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ export const NotImplemented = () => {
101101
throw json({ error_code: 'NotImplemented' }, { status: 501 })
102102
}
103103

104-
export const internalError = () => json({ error_code: 'InternalError' }, { status: 500 })
104+
export const internalError = (message: string) =>
105+
json({ error_code: 'InternalError', message }, { status: 500 })
105106

106107
export const errIfExists = <T extends Record<string, unknown>>(
107108
collection: T[],

test/e2e/networking.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88
import { expect, test } from '@playwright/test'
99

10-
import { expectNotVisible, expectVisible } from './utils'
10+
import { closeToast, expectNotVisible, expectVisible } from './utils'
1111

1212
test('Create and edit VPC', async ({ page }) => {
1313
await page.goto('/projects/mock-project')
@@ -46,7 +46,7 @@ test('Create and edit VPC', async ({ page }) => {
4646
await page.click('role=button[name="Update VPC"]')
4747

4848
// Close toast, it holds up the test for some reason
49-
await page.click('role=button[name="Dismiss notification"]')
49+
await closeToast(page)
5050

5151
await expect(page.getByRole('link', { name: 'new-vpc' })).toBeVisible()
5252
})

test/e2e/silos.e2e.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,17 @@ test('Create silo', async ({ page }) => {
3232
await page.click('role=link[name="New silo"]')
3333

3434
// fill out form
35-
await page.fill('role=textbox[name="Name"]', 'other-silo')
36-
await page.fill('role=textbox[name="Description"]', 'definitely a silo')
37-
await expect(page.locator('role=checkbox[name="Discoverable"]')).toBeChecked()
38-
await page.click('role=checkbox[name="Discoverable"]')
39-
await page.click('role=radio[name="Local only"]')
40-
await page.fill('role=textbox[name="Admin group name"]', 'admins')
41-
await page.click('role=checkbox[name="Grant fleet admin role to silo admins"]')
42-
await page.getByRole('textbox', { name: 'CPU quota (nCPUs)' }).fill('3')
43-
await page.getByRole('textbox', { name: 'Memory quota (GiB)' }).fill('5')
44-
await page.getByRole('textbox', { name: 'Storage quota (GiB)' }).fill('7')
35+
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('other-silo')
36+
await page.getByRole('textbox', { name: 'Description' }).fill('definitely a silo')
37+
const discoverable = page.getByRole('checkbox', { name: 'Discoverable' })
38+
await expect(discoverable).toBeChecked()
39+
await discoverable.click()
40+
await page.getByRole('radio', { name: 'Local only' }).click()
41+
await page.getByRole('textbox', { name: 'Admin group name' }).fill('admins')
42+
await page.getByRole('checkbox', { name: 'Grant fleet admin' }).click()
43+
await page.getByRole('textbox', { name: 'CPU quota' }).fill('30')
44+
await page.getByRole('textbox', { name: 'Memory quota' }).fill('58')
45+
await page.getByRole('textbox', { name: 'Storage quota' }).fill('735')
4546

4647
// Add a TLS cert
4748
const openCertModalButton = page.getByRole('button', { name: 'Add TLS certificate' })
@@ -114,6 +115,17 @@ test('Create silo', async ({ page }) => {
114115

115116
await page.goBack()
116117

118+
// now go check the quotas in its entry in the utilization table
119+
await page.getByRole('link', { name: 'Utilization' }).click()
120+
await expectRowVisible(page.getByRole('table'), {
121+
Silo: 'other-silo',
122+
CPU: '30',
123+
Memory: '58 GiB',
124+
Storage: '0.72 TiB',
125+
})
126+
127+
await page.goBack()
128+
117129
// now delete it
118130
await page.locator('role=button[name="Row actions"]').nth(2).click()
119131
await page.click('role=menuitem[name="Delete"]')

test/e2e/utilization.e2e.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
*
66
* Copyright Oxide Computer Company
77
*/
8-
import { expect, expectRowVisible, getPageAsUser, test } from './utils'
8+
import {
9+
clickRowAction,
10+
closeToast,
11+
expect,
12+
expectRowVisible,
13+
getPageAsUser,
14+
test,
15+
} from './utils'
916

1017
// not trying to get elaborate here. just make sure the pages load, which
1118
// exercises the loader prefetches and invariant checks
@@ -41,6 +48,43 @@ test.describe('System utilization', () => {
4148
await page.goto('/system/utilization')
4249
await expect(page.getByText('Page not found')).toBeVisible()
4350
})
51+
52+
test('zero over zero', async ({ page }) => {
53+
// easiest way to test this is to create a silo with zero quotas and delete
54+
// the other two silos so it's the only one shown on system utilization.
55+
// Otherwise we'd have to create a user in the silo to see the utilization
56+
// inside the silo
57+
58+
await page.goto('/system/silos-new')
59+
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('all-zeros')
60+
// don't need to set silo values, they're zero by default
61+
await page.getByRole('button', { name: 'Create silo' }).click()
62+
63+
await closeToast(page)
64+
65+
const confirm = page.getByRole('button', { name: 'Confirm' })
66+
67+
await clickRowAction(page, 'maze-war', 'Delete')
68+
await confirm.click()
69+
await expect(page.getByRole('cell', { name: 'maze-war' })).toBeHidden()
70+
71+
await clickRowAction(page, 'myriad', 'Delete')
72+
await confirm.click()
73+
await expect(page.getByRole('cell', { name: 'myriad' })).toBeHidden()
74+
75+
await page.getByRole('link', { name: 'Utilization' }).click()
76+
77+
// all three capacity bars are zeroed out
78+
await expect(page.getByText('—%')).toHaveCount(3)
79+
await expect(page.getByText('NaN')).toBeHidden()
80+
81+
await expectRowVisible(page.getByRole('table'), {
82+
Silo: 'all-zeros',
83+
CPU: '0',
84+
Memory: '0 GiB',
85+
Storage: '0 TiB',
86+
})
87+
})
4488
})
4589

4690
test.describe('Silo utilization', () => {

test/e2e/utils.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,16 @@ export async function expectRowVisible(
106106
export async function stopInstance(page: Page) {
107107
await page.click('role=button[name="Instance actions"]')
108108
await page.click('role=menuitem[name="Stop"]')
109-
// close toast and wait for it to fade out. for some reason it prevents things
110-
// from working, but only in tests as far as we can tell
111-
await page.click('role=button[name="Dismiss notification"]')
112-
await sleep(2000)
109+
await closeToast(page)
110+
}
111+
112+
/**
113+
* Close toast and wait for it to fade out. For some reason it prevents things
114+
* from working, but only in tests as far as we can tell.
115+
*/
116+
export async function closeToast(page: Page) {
117+
await page.getByRole('button', { name: 'Dismiss notification' }).click()
118+
await sleep(1000)
113119
}
114120

115121
/**

0 commit comments

Comments
 (0)