Skip to content

Commit

Permalink
Merge branch 'main' into gateway-route-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
charliepark committed Oct 23, 2024
2 parents a6678e9 + 4e96756 commit 018956d
Show file tree
Hide file tree
Showing 10 changed files with 56 additions and 24 deletions.
21 changes: 20 additions & 1 deletion app/api/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
import { describe, expect, it, test } from 'vitest'

import { genName, parsePortRange, synthesizeData } from './util'
import { diskCan, genName, instanceCan, parsePortRange, synthesizeData } from './util'

describe('parsePortRange', () => {
describe('parses', () => {
Expand Down Expand Up @@ -136,3 +136,22 @@ describe('synthesizeData', () => {
])
})
})

test('instanceCan', () => {
expect(instanceCan.start({ runState: 'running' })).toBe(false)
expect(instanceCan.start({ runState: 'stopped' })).toBe(true)

// @ts-expect-error typechecker rejects actions that don't exist
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
instanceCan.abc
})

test('diskCan', () => {
expect(diskCan.delete({ state: { state: 'creating' } })).toBe(false)
expect(diskCan.delete({ state: { state: 'attached', instance: 'xyz' } })).toBe(false)
expect(diskCan.delete({ state: { state: 'detached' } })).toBe(true)

// @ts-expect-error typechecker rejects actions that don't exist
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
diskCan.abc
})
12 changes: 6 additions & 6 deletions app/api/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export const genName = (...parts: [string, ...string[]]) => {
)
}

const instanceActions: Record<string, InstanceState[]> = {
const instanceActions = {
// NoVmm maps to to Stopped:
// https://github.com/oxidecomputer/omicron/blob/6dd9802/nexus/db-model/src/instance_state.rs#L55

Expand Down Expand Up @@ -120,12 +120,12 @@ const instanceActions: Record<string, InstanceState[]> = {
updateNic: ['stopped'],
// https://github.com/oxidecomputer/omicron/blob/6dd9802/nexus/src/app/instance.rs#L1520-L1522
serialConsole: ['running', 'rebooting', 'migrating', 'repairing'],
}
} satisfies Record<string, InstanceState[]>

// setting .states is a cute way to make it ergonomic to call the test function
// while also making the states available directly

export const instanceCan = R.mapValues(instanceActions, (states) => {
export const instanceCan = R.mapValues(instanceActions, (states: InstanceState[]) => {
const test = (i: { runState: InstanceState }) => states.includes(i.runState)
test.states = states
return test
Expand All @@ -140,7 +140,7 @@ export function instanceTransitioning({ runState }: Instance) {
)
}

const diskActions: Record<string, DiskState['state'][]> = {
const diskActions = {
// this is a weird one because the list of states is dynamic and it includes
// 'creating' in the unwind of the disk create saga, but does not include
// 'creating' in the disk delete saga, which is what we care about
Expand All @@ -154,9 +154,9 @@ const diskActions: Record<string, DiskState['state'][]> = {
detach: ['attached'],
// https://github.com/oxidecomputer/omicron/blob/3093818/nexus/db-queries/src/db/datastore/instance.rs#L1077-L1081
setAsBootDisk: ['attached'],
}
} satisfies Record<string, DiskState['state'][]>

export const diskCan = R.mapValues(diskActions, (states) => {
export const diskCan = R.mapValues(diskActions, (states: DiskState['state'][]) => {
// only have to Pick because we want this to work for both Disk and
// Json<Disk>, which we pass to it in the MSW handlers
const test = (d: Pick<Disk, 'state'>) => states.includes(d.state.state)
Expand Down
2 changes: 1 addition & 1 deletion app/components/DocsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type DocsPopoverProps = {
export const DocsPopover = ({ heading, icon, summary, links }: DocsPopoverProps) => {
return (
<Popover>
<PopoverButton className={cn(buttonStyle({ size: 'sm', variant: 'ghost' }), 'w-9')}>
<PopoverButton className={cn(buttonStyle({ size: 'sm', variant: 'ghost' }), 'w-8')}>
<Question12Icon aria-label="Links to docs" className="shrink-0" />
</PopoverButton>
<PopoverPanel
Expand Down
5 changes: 4 additions & 1 deletion app/components/form/fields/DisksTableField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { InstanceCreateInput } from '~/forms/instance-create'
import { Badge } from '~/ui/lib/Badge'
import { Button } from '~/ui/lib/Button'
import * as MiniTable from '~/ui/lib/MiniTable'
import { Truncate } from '~/ui/lib/Truncate'
import { bytesToGiB } from '~/util/units'

export type DiskTableItem =
Expand Down Expand Up @@ -60,7 +61,9 @@ export function DisksTableField({
aria-label={`Name: ${item.name}, Type: ${item.type}`}
key={item.name}
>
<MiniTable.Cell>{item.name}</MiniTable.Cell>
<MiniTable.Cell>
<Truncate text={item.name} maxLength={35} />
</MiniTable.Cell>
<MiniTable.Cell>
<Badge variant="solid">{item.type}</Badge>
</MiniTable.Cell>
Expand Down
6 changes: 6 additions & 0 deletions app/components/form/fields/NameField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export function NameField<
required={required}
label={label}
name={name}
transform={(value) =>
value
.toLowerCase()
.replace(/[\s_]+/g, '-')
.replace(/[^a-z0-9-]/g, '')
}
{...textFieldProps}
/>
)
Expand Down
2 changes: 1 addition & 1 deletion app/components/form/fields/TextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface TextFieldProps<
validate?: Validate<FieldPathValue<TFieldValues, TName>, TFieldValues>
control: Control<TFieldValues>
/** Alters the value of the input during the field's onChange event. */
transform?: (value: string) => FieldPathValue<TFieldValues, TName>
transform?: (value: string) => string
}

export function TextField<
Expand Down
8 changes: 7 additions & 1 deletion app/forms/image-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,13 @@ export function CreateImageSideModalForm() {

const createDisk = useApiMutation('diskCreate')
const startImport = useApiMutation('diskBulkWriteImportStart')
const uploadChunk = useApiMutation('diskBulkWriteImport')

// gcTime: 0 prevents the mutation cache from holding onto all the chunks for
// 5 minutes. It can be a ton of memory. To be honest, I don't even understand
// why the mutation cache exists. It's not like the query cache, which dedupes
// identical queries made around the same time.
// https://tanstack.com/query/v5/docs/reference/MutationCache
const uploadChunk = useApiMutation('diskBulkWriteImport', { gcTime: 0 })

// synthetic state for upload step because it consists of multiple requests
const [syntheticUploadState, setSyntheticUploadState] =
Expand Down
14 changes: 5 additions & 9 deletions app/forms/network-interface-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/
import { useMemo } from 'react'
import { useForm } from 'react-hook-form'
import type { SetRequired } from 'type-fest'

import { useApiQuery, type ApiError, type InstanceNetworkInterfaceCreate } from '@oxide/api'

Expand All @@ -19,10 +20,10 @@ import { SideModalForm } from '~/components/form/SideModalForm'
import { useProjectSelector } from '~/hooks/use-params'
import { FormDivider } from '~/ui/lib/Divider'

const defaultValues: InstanceNetworkInterfaceCreate = {
const defaultValues: SetRequired<InstanceNetworkInterfaceCreate, 'ip'> = {
name: '',
description: '',
ip: undefined,
ip: '',
subnetName: '',
vpcName: '',
}
Expand Down Expand Up @@ -58,7 +59,7 @@ export function CreateNetworkInterfaceForm({
resourceName="network interface"
title="Add network interface"
onDismiss={onDismiss}
onSubmit={onSubmit}
onSubmit={({ ip, ...rest }) => onSubmit({ ip: ip.trim() || undefined, ...rest })}
loading={loading}
submitError={submitError}
>
Expand All @@ -81,12 +82,7 @@ export function CreateNetworkInterfaceForm({
required
control={form.control}
/>
<TextField
name="ip"
label="IP Address"
control={form.control}
transform={(ip) => (ip.trim() === '' ? undefined : ip)}
/>
<TextField name="ip" label="IP Address" control={form.control} />
</SideModalForm>
)
}
2 changes: 1 addition & 1 deletion app/ui/styles/components/mini-table.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
}

& td > div {
@apply flex h-11 items-center border-y py-3 pl-3 text-accent bg-accent-secondary border-accent-tertiary;
@apply flex h-11 items-center border-y py-3 pl-3 pr-6 text-accent bg-accent-secondary border-accent-tertiary;
}

& td:last-child > div {
Expand Down
8 changes: 5 additions & 3 deletions test/e2e/project-create.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ test.describe('Project create', () => {
})

test('shows field-level validation error and does not POST', async ({ page }) => {
await page.fill('role=textbox[name="Name"]', 'Invalid name')

const input = page.getByRole('textbox', { name: 'Name' })
await input.pressSequentially('no sPoNgEbOb_CaSe or spaces')
await expect(input).toHaveValue('no-spongebob-case-or-spaces')
await input.fill('no-ending-dash-')
// submit to trigger validation
await page.getByRole('button', { name: 'Create project' }).click()

await expect(
page.getByText('Can only contain lower-case letters, numbers, and dashes').nth(0)
page.getByText('Must end with a letter or number', { exact: true }).nth(0)
).toBeVisible()
})

Expand Down

0 comments on commit 018956d

Please sign in to comment.