Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create and edit VPC subnets and routers (#593)
* first draft at create subnet SideModal * stub test for create subnet modal * working edit modal. wild! * extremely basic server errors * move classed into @oxide/util * router edit and create modals * extract form shared between edit and create, save 60 lines * got full page create subnet integration test to work * cut 50 lines updating everything new with orgName
- Loading branch information
1 parent
6d1adf6
commit 03d2693
Showing
23 changed files
with
657 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { | ||
fireEvent, | ||
lastPostBody, | ||
renderAppAt, | ||
screen, | ||
userEvent, | ||
waitForElementToBeRemoved, | ||
} from '../../../../test-utils' | ||
import fetchMock from 'fetch-mock' | ||
import { | ||
org, | ||
project, | ||
vpc, | ||
vpcSubnet, | ||
vpcSubnet2, | ||
vpcSubnets, | ||
} from '@oxide/api-mocks' | ||
|
||
const vpcUrl = `/api/organizations/${org.name}/projects/${project.name}/vpcs/default` | ||
const subnetsUrl = `${vpcUrl}/subnets` | ||
const getSubnetsUrl = `${subnetsUrl}?limit=10` | ||
|
||
describe('VpcPage', () => { | ||
describe('subnets tab', () => { | ||
it('creating a subnet works', async () => { | ||
fetchMock.get(vpcUrl, { status: 200, body: vpc }) | ||
fetchMock.getOnce(getSubnetsUrl, { status: 200, body: vpcSubnets }) | ||
const postMock = fetchMock.postOnce(subnetsUrl, { | ||
status: 201, | ||
body: vpcSubnet2, | ||
}) | ||
|
||
renderAppAt('/orgs/mock-org/projects/mock-project/vpcs/default') | ||
screen.getByText('Subnets') | ||
|
||
// wait for subnet to show up in the table | ||
await screen.findByRole('cell', { name: vpcSubnet.identity.name }) | ||
|
||
// modal is not already open | ||
expect(screen.queryByRole('dialog', { name: 'Create subnet' })).toBeNull() | ||
|
||
// click button to open modal | ||
fireEvent.click(screen.getByRole('button', { name: 'New subnet' })) | ||
|
||
// modal is open | ||
screen.getByRole('dialog', { name: 'Create subnet' }) | ||
|
||
const ipv4 = screen.getByRole('textbox', { name: 'IPv4 block' }) | ||
userEvent.type(ipv4, '1.1.1.2/24') | ||
|
||
const name = screen.getByRole('textbox', { name: 'Name' }) | ||
userEvent.type(name, 'mock-subnet-2') | ||
|
||
// override the subnets GET to include both subnets | ||
fetchMock.getOnce( | ||
getSubnetsUrl, | ||
{ | ||
status: 200, | ||
body: { items: [vpcSubnet, vpcSubnet2] }, | ||
}, | ||
{ overwriteRoutes: true } | ||
) | ||
|
||
// submit the form | ||
fireEvent.click(screen.getByRole('button', { name: 'Create subnet' })) | ||
|
||
// wait for modal to close | ||
await waitForElementToBeRemoved(() => | ||
screen.queryByRole('dialog', { name: 'Create subnet' }) | ||
) | ||
|
||
// it posted the form | ||
expect(lastPostBody(postMock)).toEqual({ | ||
ipv4Block: '1.1.1.2/24', | ||
ipv6Block: null, | ||
name: 'mock-subnet-2', | ||
description: '', | ||
}) | ||
|
||
// table should refetch and now include second subnet | ||
screen.getByRole('cell', { name: vpcSubnet.identity.name }) | ||
screen.getByRole('cell', { name: vpcSubnet2.identity.name }) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
app/pages/project/networking/VpcPage/modals/vpc-routers.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import React from 'react' | ||
import { Formik, Form } from 'formik' | ||
|
||
import { Button, FieldTitle, SideModal, TextField } from '@oxide/ui' | ||
import type { VpcRouter, ErrorResponse } from '@oxide/api' | ||
import { useApiMutation, useApiQueryClient } from '@oxide/api' | ||
import { getServerError } from '../../../../../util/errors' | ||
|
||
// this will get a lot more interesting once the API is updated to allow us to | ||
// put rules in the router, which is the whole point of a router | ||
|
||
type FormProps = { | ||
error: ErrorResponse | null | ||
id: string | ||
} | ||
|
||
// the moment the two forms diverge, inline them rather than introducing BS | ||
// props here | ||
const CommonForm = ({ error, id }: FormProps) => ( | ||
<Form id={id}> | ||
<SideModal.Section className="border-t"> | ||
<div className="space-y-0.5"> | ||
<FieldTitle htmlFor="router-name" tip="The name of the router"> | ||
Name | ||
</FieldTitle> | ||
<TextField id="router-name" name="name" /> | ||
</div> | ||
<div className="space-y-0.5"> | ||
<FieldTitle | ||
htmlFor="router-description" | ||
tip="A description for the router" | ||
> | ||
Description {/* TODO: indicate optional */} | ||
</FieldTitle> | ||
<TextField id="router-description" name="description" /> | ||
</div> | ||
</SideModal.Section> | ||
<SideModal.Section> | ||
<div className="text-red-500">{getServerError(error)}</div> | ||
</SideModal.Section> | ||
</Form> | ||
) | ||
|
||
type CreateProps = { | ||
isOpen: boolean | ||
onDismiss: () => void | ||
orgName: string | ||
projectName: string | ||
vpcName: string | ||
} | ||
|
||
export function CreateVpcRouterModal({ | ||
isOpen, | ||
onDismiss, | ||
orgName, | ||
projectName, | ||
vpcName, | ||
}: CreateProps) { | ||
const parentIds = { orgName, projectName, vpcName } | ||
const queryClient = useApiQueryClient() | ||
|
||
function dismiss() { | ||
createRouter.reset() | ||
onDismiss() | ||
} | ||
|
||
const createRouter = useApiMutation('vpcRoutersPost', { | ||
onSuccess() { | ||
queryClient.invalidateQueries('vpcRoutersGet', parentIds) | ||
dismiss() | ||
}, | ||
}) | ||
|
||
const formId = 'create-vpc-router-form' | ||
|
||
return ( | ||
<SideModal | ||
id="create-vpc-router-modal" | ||
title="Create router" | ||
isOpen={isOpen} | ||
onDismiss={dismiss} | ||
> | ||
<Formik | ||
initialValues={{ name: '', description: '' }} | ||
onSubmit={({ name, description }) => { | ||
createRouter.mutate({ | ||
...parentIds, | ||
body: { name, description }, | ||
}) | ||
}} | ||
> | ||
<CommonForm id={formId} error={createRouter.error} /> | ||
</Formik> | ||
<SideModal.Footer> | ||
<Button variant="dim" className="mr-2.5" onClick={dismiss}> | ||
Cancel | ||
</Button> | ||
<Button form={formId} type="submit"> | ||
Create router | ||
</Button> | ||
</SideModal.Footer> | ||
</SideModal> | ||
) | ||
} | ||
|
||
type EditProps = { | ||
onDismiss: () => void | ||
orgName: string | ||
projectName: string | ||
vpcName: string | ||
originalRouter: VpcRouter | null | ||
} | ||
|
||
export function EditVpcRouterModal({ | ||
onDismiss, | ||
orgName, | ||
projectName, | ||
vpcName, | ||
originalRouter, | ||
}: EditProps) { | ||
const parentIds = { orgName, projectName, vpcName } | ||
const queryClient = useApiQueryClient() | ||
|
||
function dismiss() { | ||
updateRouter.reset() | ||
onDismiss() | ||
} | ||
|
||
const updateRouter = useApiMutation('vpcRoutersPutRouter', { | ||
onSuccess() { | ||
queryClient.invalidateQueries('vpcRoutersGet', parentIds) | ||
dismiss() | ||
}, | ||
}) | ||
|
||
if (!originalRouter) return null | ||
|
||
const formId = 'edit-vpc-router-form' | ||
return ( | ||
<SideModal | ||
id="edit-vpc-router-modal" | ||
title="Edit router" | ||
onDismiss={dismiss} | ||
> | ||
<Formik | ||
initialValues={{ | ||
name: originalRouter.identity.name, | ||
description: originalRouter.identity.description, | ||
}} | ||
onSubmit={({ name, description }) => { | ||
updateRouter.mutate({ | ||
...parentIds, | ||
routerName: originalRouter.identity.name, | ||
body: { name, description }, | ||
}) | ||
}} | ||
> | ||
<CommonForm id={formId} error={updateRouter.error} /> | ||
</Formik> | ||
<SideModal.Footer> | ||
<Button variant="dim" className="mr-2.5" onClick={dismiss}> | ||
Cancel | ||
</Button> | ||
<Button form={formId} type="submit"> | ||
Update router | ||
</Button> | ||
</SideModal.Footer> | ||
</SideModal> | ||
) | ||
} |
Oops, something went wrong.
03d2693
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs: