Skip to content

Commit

Permalink
Add router tab and routes pages (#2359)
Browse files Browse the repository at this point in the history
* first stub of Routers and RouterRoutes pages

* continuing stub of router routes pages

* Add more info to Router Route page

* adjustments; TypeValueCell

* deleting a route works

* Updating strings in target / destination type-value rendering

* Update based on feedback in oxide-product-eng

* remove underscores in badge

* add limit to route list prefetch to match QueryTable fetch

* Progress on side modals to create router and route

* Router create works

* Route create working

* Router and route creation / editing working, mostly

* Move Router edit side modal to Routers overview page

* Add TopBar pickers for VPC and Router

* vpcRouterDelete implemented

* Updating a router route now works

* Update tests

* Update to routes, path-builder spec

* alphabetize routes

* add in a missing route to the spec

* Update test to match new error text in console

* Update app/forms/vpc-router-create.tsx

Co-authored-by: David Crespo <david-crespo@users.noreply.github.com>

* Slight refactor on onDismiss

* Clean up some duplicated code

* Removed old stubbed code

* Simpler table construction; add link to VPC in TopBar

* refactoring

* Optimize route fields

* Adjust mock db values

* Refactor

* Paginate routes

* Adding some error handling for issues  unearthed when using this branch with dogfood

* VPCs can not be used as destinations or targets in cusom routes

* additional validations in forms; better comments

* small test improvement

* Additional restrictions on routes / routers

* Update tests

* switch to expectRowVisible

* use clickRowAction

* Refactoring post-review

* More refactoring

* Rename to RouterPage

* git add womp womp

* badge and IP Net updates

* Add disabled link for 'New route' on system router page

* Add DescriptionCell file and update imports

* update msw handler for name/description for VPC Route

* Update how form handles drop target

* Move from useEffect to onChange for TargetType

* refactor; pull onChange inline

* Update handlers to error if route or router name already exists

* Use VPC ID for testing Router uniqueness on update

---------

Co-authored-by: David Crespo <david.crespo@oxidecomputer.com>
Co-authored-by: David Crespo <david-crespo@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 14, 2024
1 parent 11e29ed commit 68e2dc8
Show file tree
Hide file tree
Showing 27 changed files with 1,269 additions and 36 deletions.
9 changes: 8 additions & 1 deletion app/api/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,14 @@ export const getUsePrefetchedApiQuery =
})
invariant(
result.data,
`Expected query to be prefetched. Key: ${JSON.stringify(queryKey)}`
`Expected query to be prefetched.
Key: ${JSON.stringify(queryKey)}
Ensure the following:
• loader is running
• query matches in both the loader and the component
• request isn't erroring-out server-side (check the Networking tab)
• mock API endpoint is implemented in handlers.ts
`
)
// TS infers non-nullable on a freestanding variable, but doesn't like to do
// it on a property. So we give it a hint
Expand Down
2 changes: 2 additions & 0 deletions app/api/path-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type SiloImage = { image?: string }
export type NetworkInterface = Merge<Instance, { interface?: string }>
export type Snapshot = Merge<Project, { snapshot?: string }>
export type Vpc = Merge<Project, { vpc?: string }>
export type VpcRouter = Merge<Vpc, { router?: string }>
export type VpcRouterRoute = Merge<VpcRouter, { route?: string }>
export type VpcSubnet = Merge<Vpc, { subnet?: string }>
export type FirewallRule = Merge<Vpc, { rule?: string }>
export type Silo = { silo?: string }
Expand Down
55 changes: 53 additions & 2 deletions app/components/TopBarPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import {
Success12Icon,
} from '@oxide/design-system/icons/react'

import { useInstanceSelector, useIpPoolSelector, useSiloSelector } from '~/hooks'
import {
useInstanceSelector,
useIpPoolSelector,
useSiloSelector,
useVpcRouterSelector,
useVpcSelector,
} from '~/hooks'
import { useCurrentUser } from '~/layouts/AuthenticatedLayout'
import { PAGE_SIZE } from '~/table/QueryTable'
import { Button } from '~/ui/lib/Button'
Expand Down Expand Up @@ -229,7 +235,7 @@ export function SiloPicker() {
export function IpPoolPicker() {
// picker only shows up when a pool is in scope
const { pool: poolName } = useIpPoolSelector()
const { data } = useApiQuery('ipPoolList', { query: { limit: 10 } })
const { data } = useApiQuery('ipPoolList', { query: { limit: PAGE_SIZE } })
const items = (data?.items || []).map((pool) => ({
label: pool.name,
to: pb.ipPool({ pool: pool.name }),
Expand All @@ -246,6 +252,51 @@ export function IpPoolPicker() {
)
}

/** Used when drilling down into a VPC from the Silo view. */
export function VpcPicker() {
// picker only shows up when a VPC is in scope
const { project, vpc } = useVpcSelector()
const { data } = useApiQuery('vpcList', { query: { project, limit: PAGE_SIZE } })
const items = (data?.items || []).map((v) => ({
label: v.name,
to: pb.vpc({ project, vpc: v.name }),
}))

return (
<TopBarPicker
aria-label="Switch VPC"
category="VPC"
current={vpc}
items={items}
noItemsText="No VPCs found"
to={pb.vpc({ project, vpc })}
/>
)
}

/** Used when drilling down into a VPC Router from the Silo view. */
export function VpcRouterPicker() {
// picker only shows up when a router is in scope
const { project, vpc, router } = useVpcRouterSelector()
const { data } = useApiQuery('vpcRouterList', {
query: { project, vpc, limit: PAGE_SIZE },
})
const items = (data?.items || []).map((r) => ({
label: r.name,
to: pb.vpcRouter({ vpc, project, router: r.name }),
}))

return (
<TopBarPicker
aria-label="Switch router"
category="router"
current={router}
items={items}
noItemsText="No routers found"
/>
)
}

const NoProjectLogo = () => (
<div className="flex h-[34px] w-[34px] items-center justify-center rounded text-secondary bg-secondary">
<Folder16Icon />
Expand Down
2 changes: 1 addition & 1 deletion app/forms/subnet-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function EditSubnetForm() {
},
})

const defaultValues = R.pick(subnet, ['name', 'description']) satisfies VpcSubnetUpdate
const defaultValues: VpcSubnetUpdate = R.pick(subnet, ['name', 'description'])

const form = useForm({ defaultValues })

Expand Down
55 changes: 55 additions & 0 deletions app/forms/vpc-router-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { useNavigate } from 'react-router-dom'

import { useApiMutation, useApiQueryClient, type VpcRouterCreate } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { useForm, useVpcSelector } from '~/hooks'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'

const defaultValues: VpcRouterCreate = {
name: '',
description: '',
}

export function CreateRouterSideModalForm() {
const queryClient = useApiQueryClient()
const vpcSelector = useVpcSelector()
const navigate = useNavigate()

const onDismiss = () => navigate(pb.vpcRouters(vpcSelector))

const createRouter = useApiMutation('vpcRouterCreate', {
onSuccess() {
queryClient.invalidateQueries('vpcRouterList')
addToast({ content: 'Your router has been created' })
onDismiss()
},
})

const form = useForm({ defaultValues })

return (
<SideModalForm
form={form}
formType="create"
resourceName="router"
onDismiss={onDismiss}
onSubmit={(body) => createRouter.mutate({ query: vpcSelector, body })}
loading={createRouter.isPending}
submitError={createRouter.error}
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
</SideModalForm>
)
}
87 changes: 87 additions & 0 deletions app/forms/vpc-router-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import {
useNavigate,
type LoaderFunctionArgs,
type NavigateFunction,
} from 'react-router-dom'

import {
apiQueryClient,
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
type VpcRouterUpdate,
} from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { NameField } from '~/components/form/fields/NameField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { getVpcRouterSelector, useForm, useVpcRouterSelector } from '~/hooks'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'

EditRouterSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { router, project, vpc } = getVpcRouterSelector(params)
await apiQueryClient.prefetchQuery('vpcRouterView', {
path: { router },
query: { project, vpc },
})
return null
}

export function EditRouterSideModalForm() {
const queryClient = useApiQueryClient()
const routerSelector = useVpcRouterSelector()
const { project, vpc, router } = routerSelector
const { data: routerData } = usePrefetchedApiQuery('vpcRouterView', {
path: { router },
query: { project, vpc },
})
const navigate = useNavigate()

const onDismiss = (navigate: NavigateFunction) => {
navigate(pb.vpcRouters({ project, vpc }))
}

const editRouter = useApiMutation('vpcRouterUpdate', {
onSuccess() {
queryClient.invalidateQueries('vpcRouterList')
addToast({ content: 'Your router has been updated' })
navigate(pb.vpcRouters({ project, vpc }))
},
})

const defaultValues: VpcRouterUpdate = {
name: router,
description: routerData.description,
}

const form = useForm({ defaultValues })

return (
<SideModalForm
form={form}
formType="edit"
resourceName="router"
onDismiss={() => onDismiss(navigate)}
onSubmit={(body) =>
editRouter.mutate({
path: { router },
query: { project, vpc },
body,
})
}
loading={editRouter.isPending}
submitError={editRouter.error}
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
</SideModalForm>
)
}
98 changes: 98 additions & 0 deletions app/forms/vpc-router-route-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { useNavigate } from 'react-router-dom'

import { useApiMutation, useApiQueryClient, type RouterRouteCreate } from '@oxide/api'

import { DescriptionField } from '~/components/form/fields/DescriptionField'
import { ListboxField } from '~/components/form/fields/ListboxField'
import { NameField } from '~/components/form/fields/NameField'
import { TextField } from '~/components/form/fields/TextField'
import { SideModalForm } from '~/components/form/SideModalForm'
import { fields, targetValueDescription } from '~/forms/vpc-router-route/shared'
import { useForm, useVpcRouterSelector } from '~/hooks'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'

const defaultValues: RouterRouteCreate = {
name: '',
description: '',
destination: { type: 'ip', value: '' },
target: { type: 'ip', value: '' },
}

export function CreateRouterRouteSideModalForm() {
const queryClient = useApiQueryClient()
const routerSelector = useVpcRouterSelector()
const navigate = useNavigate()

const onDismiss = () => {
navigate(pb.vpcRouter(routerSelector))
}

const createRouterRoute = useApiMutation('vpcRouterRouteCreate', {
onSuccess() {
queryClient.invalidateQueries('vpcRouterRouteList')
addToast({ content: 'Your route has been created' })
onDismiss()
},
})

const form = useForm({ defaultValues })
const targetType = form.watch('target.type')

return (
<SideModalForm
form={form}
formType="create"
resourceName="route"
onDismiss={onDismiss}
onSubmit={({ name, description, destination, target }) =>
createRouterRoute.mutate({
query: routerSelector,
body: {
name,
description,
destination,
// drop has no value
target: target.type === 'drop' ? { type: target.type } : target,
},
})
}
loading={createRouterRoute.isPending}
submitError={createRouterRoute.error}
>
<NameField name="name" control={form.control} />
<DescriptionField name="description" control={form.control} />
<ListboxField {...fields.destType} control={form.control} />
<TextField {...fields.destValue} control={form.control} />
<ListboxField
{...fields.targetType}
control={form.control}
onChange={(value) => {
// 'outbound' is only valid option when targetType is 'internet_gateway'
if (value === 'internet_gateway') {
form.setValue('target.value', 'outbound')
}
if (value === 'drop') {
form.setValue('target.value', '')
}
}}
/>
{targetType !== 'drop' && (
<TextField
{...fields.targetValue}
control={form.control}
// when targetType is 'internet_gateway', we set it to `outbound` and make it non-editable
disabled={targetType === 'internet_gateway'}
description={targetValueDescription(targetType)}
/>
)}
</SideModalForm>
)
}
Loading

0 comments on commit 68e2dc8

Please sign in to comment.