Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move VPC edit form to VPC view page #2418

Merged
merged 15 commits into from
Sep 10, 2024
Merged
24 changes: 13 additions & 11 deletions app/forms/vpc-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,20 @@ export function EditVpcSideModalForm() {
query: { project },
})

const onDismiss = () => navigate(pb.vpcs({ project }))

const editVpc = useApiMutation('vpcUpdate', {
onSuccess(vpc) {
onSuccess(updatedVpc) {
queryClient.invalidateQueries('vpcList')
queryClient.setQueryData(
'vpcView',
{ path: { vpc: vpc.name }, query: { project } },
vpc
)
addToast({ content: 'Your VPC has been created' })
onDismiss()
navigate(pb.vpc({ project, vpc: updatedVpc.name }))
addToast({ content: 'Your VPC has been updated' })

// Only invalidate if we're staying on the same page. If the name
// _has_ changed, invalidating vpcView causes an error page to flash
// while the loader for the target page is running because the current
// page's VPC gets cleared out while we're still on the page. If we're
// navigating to a different page, its query will fetch anew regardless.
if (vpc.name === updatedVpc.name) {
queryClient.invalidateQueries('vpcView')
}
},
})

Expand All @@ -60,7 +62,7 @@ export function EditVpcSideModalForm() {
form={form}
formType="edit"
resourceName="VPC"
onDismiss={onDismiss}
onDismiss={() => navigate(pb.vpc({ project, vpc: vpcName }))}
onSubmit={({ name, description, dnsName }) => {
editVpc.mutate({
path: { vpc: vpcName },
Expand Down
53 changes: 48 additions & 5 deletions app/pages/project/vpcs/VpcPage/VpcPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
*
* Copyright Oxide Computer Company
*/
import type { LoaderFunctionArgs } from 'react-router-dom'
import { useMemo } from 'react'
import { useNavigate, type LoaderFunctionArgs } from 'react-router-dom'

import { apiQueryClient, usePrefetchedApiQuery } from '@oxide/api'
import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
} from '@oxide/api'
import { Networking24Icon } from '@oxide/design-system/icons/react'

import { MoreActionsMenu } from '~/components/MoreActionsMenu'
import { RouteTabs, Tab } from '~/components/RouteTabs'
import { getVpcSelector, useVpcSelector } from '~/hooks'
import { confirmDelete } from '~/stores/confirm-delete'
import { addToast } from '~/stores/toast'
import { DescriptionCell } from '~/table/cells/DescriptionCell'
import { DateTime } from '~/ui/lib/DateTime'
import { PageHeader, PageTitle } from '~/ui/lib/PageHeader'
Expand All @@ -27,17 +36,51 @@ VpcPage.loader = async ({ params }: LoaderFunctionArgs) => {
}

export function VpcPage() {
const queryClient = useApiQueryClient()
const navigate = useNavigate()
const vpcSelector = useVpcSelector()
const { project, vpc: vpcName } = vpcSelector
const { data: vpc } = usePrefetchedApiQuery('vpcView', {
path: { vpc: vpcSelector.vpc },
query: { project: vpcSelector.project },
path: { vpc: vpcName },
query: { project },
})

const { mutateAsync: deleteVpc } = useApiMutation('vpcDelete', {
onSuccess() {
queryClient.invalidateQueries('vpcList')
navigate(pb.vpcs({ project }))
addToast({ content: 'Your VPC has been deleted' })
},
})

const actions = useMemo(
() => [
{
label: 'Edit',
onActivate() {
navigate(pb.vpcEdit(vpcSelector))
},
},
{
label: 'Delete',
onActivate: confirmDelete({
doDelete: () => deleteVpc({ path: { vpc: vpcName }, query: { project } }),
label: vpcName,
}),
className: 'destructive',
},
],
[deleteVpc, navigate, project, vpcName, vpcSelector]
)

return (
<>
<PageHeader>
<PageTitle icon={<Networking24Icon />}>{vpc.name}</PageTitle>
<VpcDocsPopover />
<div className="inline-flex gap-2">
<VpcDocsPopover />
<MoreActionsMenu label="VPC actions" actions={actions} />
</div>
</PageHeader>
<PropertiesTable.Group className="mb-16">
<PropertiesTable>
Expand Down
12 changes: 6 additions & 6 deletions app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,6 @@ export const routes = createRoutesFromElements(
element={<CreateVpcSideModalForm />}
handle={{ crumb: 'New VPC' }}
/>
<Route
path="vpcs/:vpc/edit"
element={<EditVpcSideModalForm />}
loader={EditVpcSideModalForm.loader}
handle={{ crumb: 'Edit VPC' }}
/>
</Route>

<Route path="vpcs" handle={{ crumb: 'VPCs' }}>
Expand All @@ -365,6 +359,12 @@ export const routes = createRoutesFromElements(
loader={VpcFirewallRulesTab.loader}
/>
<Route element={<VpcFirewallRulesTab />} loader={VpcFirewallRulesTab.loader}>
<Route
path="edit"
element={<EditVpcSideModalForm />}
loader={EditVpcSideModalForm.loader}
handle={{ crumb: 'Edit VPC' }}
/>
<Route
path="firewall-rules"
handle={{ crumb: 'Firewall Rules' }}
Expand Down
29 changes: 29 additions & 0 deletions test/e2e/vpcs.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,35 @@ test('can nav to VpcPage from /', async ({ page }) => {
await expect(page.getByRole('cell', { name: 'allow-icmp' })).toBeVisible()
})

test('can edit VPC', async ({ page }) => {
// update the VPC name, starting from the VPCs list page
await page.goto('/projects/mock-project/vpcs')
await expectRowVisible(page.getByRole('table'), { name: 'mock-vpc' })
await clickRowAction(page, 'mock-vpc', 'Edit')
await expect(page).toHaveURL('/projects/mock-project/vpcs/mock-vpc/edit')
await page.getByRole('textbox', { name: 'Name' }).first().fill('mock-vpc-2')
await page.getByRole('button', { name: 'Update VPC' }).click()
await expect(page).toHaveURL('/projects/mock-project/vpcs/mock-vpc-2/firewall-rules')
await expect(page.getByRole('heading', { name: 'mock-vpc-2' })).toBeVisible()

// now update the VPC description, starting from the VPC view page
await page.getByRole('button', { name: 'VPC actions' }).click()
await page.getByRole('menuitem', { name: 'Edit' }).click()
await expect(page).toHaveURL('/projects/mock-project/vpcs/mock-vpc-2/edit')
await page.getByRole('textbox', { name: 'Description' }).fill('updated description')
await page.getByRole('button', { name: 'Update VPC' }).click()
await expect(page).toHaveURL('/projects/mock-project/vpcs/mock-vpc-2/firewall-rules')
charliepark marked this conversation as resolved.
Show resolved Hide resolved

// go to the VPCs list page and verify the name and description change
await page.getByRole('link', { name: 'VPCs' }).click()
await expect(page.getByRole('table').locator('tbody >> tr')).toHaveCount(1)
await expectRowVisible(page.getByRole('table'), {
name: 'mock-vpc-2',
'DNS name': 'mock-vpc',
description: 'updated description',
})
})

test('can create and delete subnet', async ({ page }) => {
await page.goto('/projects/mock-project/vpcs/mock-vpc')
await page.getByRole('tab', { name: 'Subnets' }).click()
Expand Down
Loading