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
10 changes: 10 additions & 0 deletions app/components/CapacityBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ function TitleCell({ icon, title, unit }: TitleCellProps) {
}

function PctCell({ pct }: { pct: number }) {
// NaN happens when both top and bottom are 0
if (Number.isNaN(pct)) {
return (
<div className="flex -translate-y-0.5 items-baseline text-quaternary">
<div className="font-light text-sans-2xl">—</div>
<div className="text-sans-xl">%</div>
</div>
)
}

const [wholeNumber, decimal] = splitDecimal(pct)
return (
<div className="flex -translate-y-0.5 items-baseline">
Expand Down
7 changes: 7 additions & 0 deletions app/forms/silo-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useToast } from '~/hooks'
import { FormDivider } from '~/ui/lib/Divider'
import { pb } from '~/util/path-builder'
import { GiB } from '~/util/units'

export type SiloCreateFormValues = Omit<SiloCreate, 'mappedFleetRoles'> & {
siloAdminGetsFleetAdmin: boolean
Expand Down Expand Up @@ -74,6 +75,7 @@ export function CreateSiloSideModalForm() {
adminGroupName,
siloAdminGetsFleetAdmin,
siloViewerGetsFleetViewer,
quotas,
...rest
}) => {
const mappedFleetRoles: SiloCreate['mappedFleetRoles'] = {}
Expand All @@ -88,6 +90,11 @@ export function CreateSiloSideModalForm() {
// no point setting it to empty string or whitespace
adminGroupName: adminGroupName?.trim() || undefined,
mappedFleetRoles,
quotas: {
cpus: quotas.cpus,
memory: quotas.memory * GiB,
storage: quotas.storage * GiB,
},
...rest,
},
})
Expand Down
8 changes: 6 additions & 2 deletions mock-api/msw/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,14 @@ export const lookup = {

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

const provisioned = db.siloProvisioned.find((p) => p.silo_id === silo.id)
if (!provisioned) throw internalError()
if (!provisioned) {
throw internalError(`no entry in db.siloProvisioned for silo ${silo.name}`)
}

return {
allocated: pick(quotas, 'cpus', 'storage', 'memory'),
Expand Down
4 changes: 3 additions & 1 deletion mock-api/msw/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,7 +1130,7 @@ export const handlers = makeHandlers({
requireFleetViewer(cookies)
return paginated(query, db.silos)
},
siloCreate({ body, cookies }) {
siloCreate({ body: { quotas, ...body }, cookies }) {
requireFleetViewer(cookies)
errIfExists(db.silos, { name: body.name })
const newSilo: Json<Api.Silo> = {
Expand All @@ -1140,6 +1140,8 @@ export const handlers = makeHandlers({
mapped_fleet_roles: body.mapped_fleet_roles || {},
}
db.silos.push(newSilo)
db.siloQuotas.push({ silo_id: newSilo.id, ...quotas })
db.siloProvisioned.push({ silo_id: newSilo.id, cpus: 0, memory: 0, storage: 0 })
return json(newSilo, { status: 201 })
},
siloView({ path, cookies }) {
Expand Down
3 changes: 2 additions & 1 deletion mock-api/msw/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export const NotImplemented = () => {
throw json({ error_code: 'NotImplemented' }, { status: 501 })
}

export const internalError = () => json({ error_code: 'InternalError' }, { status: 500 })
export const internalError = (message: string) =>
json({ error_code: 'InternalError', message }, { status: 500 })

export const errIfExists = <T extends Record<string, unknown>>(
collection: T[],
Expand Down
4 changes: 2 additions & 2 deletions test/e2e/networking.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { expect, test } from '@playwright/test'

import { expectNotVisible, expectVisible } from './utils'
import { closeToast, expectNotVisible, expectVisible } from './utils'

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

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

await expect(page.getByRole('link', { name: 'new-vpc' })).toBeVisible()
})
Expand Down
32 changes: 22 additions & 10 deletions test/e2e/silos.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ test('Create silo', async ({ page }) => {
await page.click('role=link[name="New silo"]')

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

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

await page.goBack()

// now go check the quotas in its entry in the utilization table
await page.getByRole('link', { name: 'Utilization' }).click()
await expectRowVisible(page.getByRole('table'), {
Silo: 'other-silo',
CPU: '30',
Memory: '58 GiB',
Storage: '0.72 TiB',
})

await page.goBack()

// now delete it
await page.locator('role=button[name="Row actions"]').nth(2).click()
await page.click('role=menuitem[name="Delete"]')
Expand Down
46 changes: 45 additions & 1 deletion test/e2e/utilization.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
*
* Copyright Oxide Computer Company
*/
import { expect, expectRowVisible, getPageAsUser, test } from './utils'
import {
clickRowAction,
closeToast,
expect,
expectRowVisible,
getPageAsUser,
test,
} from './utils'

// not trying to get elaborate here. just make sure the pages load, which
// exercises the loader prefetches and invariant checks
Expand Down Expand Up @@ -41,6 +48,43 @@ test.describe('System utilization', () => {
await page.goto('/system/utilization')
await expect(page.getByText('Page not found')).toBeVisible()
})

test('zero over zero', async ({ page }) => {
// easiest way to test this is to create a silo with zero quotas and delete
// the other two silos so it's the only one shown on system utilization.
// Otherwise we'd have to create a user in the silo to see the utilization
// inside the silo

await page.goto('/system/silos-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('all-zeros')
// don't need to set silo values, they're zero by default
await page.getByRole('button', { name: 'Create silo' }).click()

await closeToast(page)

const confirm = page.getByRole('button', { name: 'Confirm' })

await clickRowAction(page, 'maze-war', 'Delete')
await confirm.click()
await expect(page.getByRole('cell', { name: 'maze-war' })).toBeHidden()

await clickRowAction(page, 'myriad', 'Delete')
await confirm.click()
await expect(page.getByRole('cell', { name: 'myriad' })).toBeHidden()

await page.getByRole('link', { name: 'Utilization' }).click()

// all three capacity bars are zeroed out
await expect(page.getByText('—%')).toHaveCount(3)
await expect(page.getByText('NaN')).toBeHidden()

await expectRowVisible(page.getByRole('table'), {
Silo: 'all-zeros',
CPU: '0',
Memory: '0 GiB',
Storage: '0 TiB',
})
})
})

test.describe('Silo utilization', () => {
Expand Down
14 changes: 10 additions & 4 deletions test/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,16 @@ export async function expectRowVisible(
export async function stopInstance(page: Page) {
await page.click('role=button[name="Instance actions"]')
await page.click('role=menuitem[name="Stop"]')
// close toast and wait for it to fade out. for some reason it prevents things
// from working, but only in tests as far as we can tell
await page.click('role=button[name="Dismiss notification"]')
await sleep(2000)
await closeToast(page)
}

/**
* Close toast and wait for it to fade out. For some reason it prevents things
* from working, but only in tests as far as we can tell.
*/
export async function closeToast(page: Page) {
await page.getByRole('button', { name: 'Dismiss notification' }).click()
await sleep(1000)
}

/**
Expand Down