Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
351f659
WIP instance form work
zephraph Apr 1, 2022
914efff
Fix button size
zephraph Apr 1, 2022
e8d1647
Minor accessibilty updates to the table
zephraph Apr 1, 2022
fa4b299
Wire up dismiss states
zephraph Apr 1, 2022
6399c8d
Improve tests, name validation
zephraph Apr 1, 2022
73fe1a0
Merge branch 'main' into update-instance-page
zephraph Apr 2, 2022
cb8e0a1
Merge branch 'main' into update-instance-page
zephraph Apr 4, 2022
0c5c1d3
Update extract types
zephraph Apr 4, 2022
65f0eae
Add disk attach
zephraph Apr 4, 2022
5f20f32
Fix instance-create page not rendering
zephraph Apr 4, 2022
a739175
Merge branch 'main' into update-instance-page
zephraph Apr 5, 2022
879dffd
Add ability to get form context from user
zephraph Apr 5, 2022
2cc26a6
WIP instance create
zephraph Apr 5, 2022
54dcc28
Merge branch 'main' into update-instance-page
zephraph Apr 5, 2022
9a51f08
Fix issue where form params weren't being set
zephraph Apr 5, 2022
914bb3d
fixup disk attach form
zephraph Apr 5, 2022
a68dfb0
automatically update packer-id
github-actions[bot] Apr 5, 2022
24b0daa
Move pre-built form types out of form lib
zephraph Apr 6, 2022
e91aa2d
Delete old instance create page
zephraph Apr 7, 2022
1c7660c
Revert page param changes
zephraph Apr 7, 2022
2478899
Remove old comment
zephraph Apr 7, 2022
fffc858
Remove some left over form param references
zephraph Apr 7, 2022
51f1d87
Remove more form param references, cleanup type failures
zephraph Apr 7, 2022
a5a03d0
Fix lint issues
zephraph Apr 7, 2022
bbba80f
Fix e2e tests
zephraph Apr 7, 2022
4595486
Add e2e test for instance create
zephraph Apr 8, 2022
f2d8057
Update app/forms/instance-create.tsx
zephraph Apr 8, 2022
2a11866
export use-forms from app/forms/index
zephraph Apr 8, 2022
4b77762
Merge branch 'update-instance-page' of github.com:oxidecomputer/conso…
zephraph Apr 8, 2022
e4c0c75
Wire up disk creates
zephraph Apr 8, 2022
880ba88
Remove disk size, capitalize source type
zephraph Apr 8, 2022
1e0ee98
Fix disk attach api
zephraph Apr 8, 2022
92a59b6
revert disk attach api change
zephraph Apr 8, 2022
45bcd88
Merge main into update-instance-page
david-crespo Apr 8, 2022
89a86d8
update disk name thing
david-crespo Apr 8, 2022
a21e9b5
Invert disks table logic flow (#777)
david-crespo Apr 9, 2022
f64f84f
allow any param, but it might be undefined (#779)
david-crespo Apr 9, 2022
89ced0d
Merge branch 'main' into update-instance-page
zephraph Apr 11, 2022
18e222f
Address PR feedback, validateName -> getNameValidator
zephraph Apr 11, 2022
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
108 changes: 108 additions & 0 deletions app/components/DisksTableField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import React, { useState } from 'react'
import { useField } from 'formik'
import { CreateDiskForm } from 'app/forms/disk-create'
import { AttachDiskForm } from 'app/forms/disk-attach'
import {
Button,
Error16Icon,
FieldLabel,
MiniTable,
SideModal,
} from '@oxide/ui'
import type { FormValues } from '../forms'

type DiskTableItem =
| (FormValues<'disk-create'> & { type: 'create' })
| (FormValues<'disk-attach'> & { type: 'attach' })

export function DisksTableField() {
const [showDiskCreate, setShowDiskCreate] = useState(false)
const [showDiskAttach, setShowDiskAttach] = useState(false)

const [, { value: items = [] }, { setValue: setItems }] = useField<
DiskTableItem[]
>({ name: 'disks' })

return (
<>
<div className="max-w-lg">
<FieldLabel id="new-disks-label">{/* this was empty */}</FieldLabel>
{!!items.length && (
<MiniTable className="mb-4">
<MiniTable.Header>
<MiniTable.HeadCell>Name</MiniTable.HeadCell>
<MiniTable.HeadCell>Type</MiniTable.HeadCell>
{/* For remove button */}
<MiniTable.HeadCell className="w-12" />
</MiniTable.Header>
<MiniTable.Body>
{items.map((item, index) => (
<MiniTable.Row
tabindex="0"
aria-rowindex={index + 1}
aria-label={`Name: ${item.name}, Type: ${item.type}`}
key={item.name}
>
<MiniTable.Cell>{item.name}</MiniTable.Cell>
<MiniTable.Cell>{item.type}</MiniTable.Cell>
<MiniTable.Cell>
<Button
variant="link"
onClick={() =>
setItems(items.filter((i) => i.name !== item.name))
}
>
<Error16Icon title={`remove ${item.name}`} />
</Button>
</MiniTable.Cell>
</MiniTable.Row>
))}
</MiniTable.Body>
</MiniTable>
)}

<div className="space-x-3">
<Button
variant="secondary"
size="sm"
onClick={() => setShowDiskCreate(true)}
>
Create new disk
</Button>
<Button
variant="secondary"
size="sm"
onClick={() => setShowDiskAttach(true)}
>
Attach existing disk
</Button>
</div>
</div>

<SideModal
id="create-disk-modal"
isOpen={showDiskCreate}
onDismiss={() => setShowDiskCreate(false)}
>
<CreateDiskForm
onSubmit={(values) => {
setItems([...items, { type: 'create', ...values }])
setShowDiskCreate(false)
}}
/>
</SideModal>
<SideModal
id="attach-disk-modal"
isOpen={showDiskAttach}
onDismiss={() => setShowDiskAttach(false)}
>
<AttachDiskForm
onSubmit={(values) => {
setItems([...items, { type: 'attach', ...values }])
setShowDiskAttach(false)
}}
/>
</SideModal>
</>
)
}
26 changes: 26 additions & 0 deletions app/forms/__tests__/instance-create.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from '@playwright/test'

test.describe('Instance Create Form', () => {
test('can invoke instance create form from instances page', async ({
page,
}) => {
await page.goto('/orgs/maze-war/projects/mock-project/instances')
await page.locator('text="New Instance"').click()
await expect(page.locator('h1:has-text("Create instance")')).toBeVisible()

await page.fill('input[name=name]', 'mock-instance')
await page.locator('.ox-radio-card').nth(0).click()

await page.locator('input[value=ubuntu] ~ .ox-radio-card').click()

await page.locator('button:has-text("Create instance")').click()

await page.waitForNavigation()

expect(page.url()).toContain(
'/orgs/maze-war/projects/mock-project/instances/mock-instance'
)

await expect(page.locator('h1:has-text("mock-instance")')).toBeVisible()
})
})
67 changes: 67 additions & 0 deletions app/forms/disk-attach.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Form, NameField } from '@oxide/form'
import React from 'react'
import type { Disk } from '@oxide/api'
import { useApiMutation, useApiQueryClient } from '@oxide/api'
import { invariant } from '@oxide/util'
import { useParams } from 'app/hooks'
import type { PrebuiltFormProps } from 'app/forms'

const values = {
name: '',
}

export function AttachDiskForm({
id = 'form-disk-attach',
title = 'Attach Disk',
initialValues = values,
onSubmit,
onSuccess,
onError,
...props
}: PrebuiltFormProps<typeof values, Disk>) {
const queryClient = useApiQueryClient()
const pathParams = useParams('orgName', 'projectName')

const attachDisk = useApiMutation('instanceDisksAttach', {
onSuccess(data) {
const { instanceName, ...others } = pathParams
invariant(instanceName, 'instanceName is required')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it mean to require instanceName at submit time but not at render time?

queryClient.invalidateQueries('instanceDisksGet', {
instanceName,
...others,
})
onSuccess?.(data)
},
onError,
})

return (
<Form
id={id}
title={title}
initialValues={initialValues}
onSubmit={
onSubmit ||
(({ name }) => {
const { instanceName, ...others } = pathParams
invariant(instanceName, 'instanceName is required')
attachDisk.mutate({
instanceName,
...others,
body: { name },
})
})
}
mutation={attachDisk}
{...props}
>
<NameField id="form-disk-attach-name" label="Disk name" />
<Form.Actions>
<Form.Submit>{title}</Form.Submit>
<Form.Cancel />
</Form.Actions>
</Form>
)
}

export default AttachDiskForm
15 changes: 7 additions & 8 deletions app/forms/disk-create.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from 'react'
import {
DescriptionField,
Form,
Expand All @@ -7,17 +8,16 @@ import {
Radio,
} from '@oxide/form'
import { Divider } from '@oxide/ui'
import React from 'react'
import type { PrebuiltFormProps } from '@oxide/form'
import { useParams } from 'app/hooks'
import type { Disk } from '@oxide/api'
import { useApiMutation, useApiQueryClient } from '@oxide/api'

import type { PrebuiltFormProps } from 'app/forms'
import { useParams } from 'app/hooks'

const values = {
name: '',
description: '',
size: 0,
type: '',
sourceType: '',
deletionRule: '',
}
Expand All @@ -31,12 +31,12 @@ export function CreateDiskForm({
onError,
...props
}: PrebuiltFormProps<typeof values, Disk>) {
const parentNames = useParams('orgName', 'projectName')
const queryClient = useApiQueryClient()
const pathParams = useParams('orgName', 'projectName')

const createDisk = useApiMutation('projectDisksPost', {
onSuccess(data) {
queryClient.invalidateQueries('projectDisksGet', parentNames)
queryClient.invalidateQueries('projectDisksGet', pathParams)
onSuccess?.(data)
},
onError,
Expand All @@ -50,7 +50,7 @@ export function CreateDiskForm({
onSubmit={
onSubmit ||
((body) => {
createDisk.mutate({ ...parentNames, body })
createDisk.mutate({ ...pathParams, body })
})
}
mutation={createDisk}
Expand All @@ -59,7 +59,6 @@ export function CreateDiskForm({
<NameField id="disk-name" />
<DescriptionField id="disk-description" />
<Divider />
<TextField id="disk-type" name="type" />
<RadioField column id="disk-source-type" name="sourceType">
<Radio value="blank">Blank disk</Radio>
<Radio value="image">Image</Radio>
Expand Down
1 change: 1 addition & 0 deletions app/forms/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import babel from '@babel/core'
import { traverse } from '@babel/core'
import fs from 'fs/promises'
import path from 'path'
import './index'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without explicitly importing index here vitest doesn't correctly when the file changes.


test('FormTypes must contain references to all forms', async () => {
let formIds: string[] = []
Expand Down
49 changes: 49 additions & 0 deletions app/forms/index.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,67 @@
// TODO: Make these just be default exports

import type { CreateSubnetForm } from './subnet-create'
import type { EditSubnetForm } from './subnet-edit'
import type { CreateOrgForm } from './org-create'
import type { CreateDiskForm } from './disk-create'
import type { CreateProjectForm } from './project-create'
import type CreateInstanceForm from './instance-create'
import type AttachDiskForm from './disk-attach'

import type { FormProps } from '@oxide/form'
import type { ErrorResponse } from '@oxide/api'
import type { ComponentType } from 'react'

/**
* A map of all existing forms. When a new form is created in the forms directory, a
* new entry should be added here with the key of the string name of the form's filename
* and a value of the form's type. There's a test to validate that this happens.
*/
export interface FormTypes {
'instance-create': typeof CreateInstanceForm
'org-create': typeof CreateOrgForm
'project-create': typeof CreateProjectForm
'disk-attach': typeof AttachDiskForm
'disk-create': typeof CreateDiskForm
'subnet-create': typeof CreateSubnetForm
'subnet-edit': typeof EditSubnetForm
}

export type FormValues<K extends keyof FormTypes> = ExtractFormValues<
FormTypes[K]
>

/**
* A form that's built out ahead of time and intended to be re-used dynamically. Fields
* that are expected to be provided by default are set to optional.
*/
export type PrebuiltFormProps<Values, Data> = Omit<
Optional<
FormProps<Values>,
'id' | 'title' | 'initialValues' | 'onSubmit' | 'mutation'
>,
'children'
> & {
children?: never
onSuccess?: (data: Data) => void
onError?: (err: ErrorResponse) => void
}

/**
* A utility type for a prebuilt form that extends another form
*/
export type ExtendedPrebuiltFormProps<C, D = void> = C extends ComponentType<
infer B
>
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
B extends PrebuiltFormProps<infer V, any>
? PrebuiltFormProps<V, D>
: never
: never

export type ExtractFormValues<C> = C extends ComponentType<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
PrebuiltFormProps<infer V, any>
>
? V
: never
Copy link
Collaborator

@david-crespo david-crespo Apr 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types are complicated enough that it suggests to me we might be doing something we don't need to be doing, but I'm fine leaving it and mulling that over as we use it

Loading