Skip to content

Commit

Permalink
Extract common fields and refactor VPC router route forms (#2436)
Browse files Browse the repository at this point in the history
  • Loading branch information
charliepark authored Sep 16, 2024
1 parent ca27233 commit a5c2361
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 181 deletions.
117 changes: 117 additions & 0 deletions app/forms/vpc-router-route-common.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 type { UseFormReturn } from 'react-hook-form'

import type {
RouteDestination,
RouterRouteCreate,
RouterRouteUpdate,
RouteTarget,
} from '~/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 { Message } from '~/ui/lib/Message'

export type RouteFormValues = RouterRouteCreate | Required<RouterRouteUpdate>

export const routeFormMessage = {
vpcSubnetNotModifiable:
'Routes of type VPC Subnet within the system router are not modifiable',
internetGatewayTargetValue:
'For ‘Internet gateway’ targets, the value must be ‘outbound’',
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L201-L204
noNewRoutesOnSystemRouter: 'User-provided routes cannot be added to a system router',
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L300-L304
noDeletingRoutesOnSystemRouter: 'System routes cannot be deleted',
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L136-L138
noDeletingSystemRouters: 'System routers cannot be deleted',
}

// VPCs cannot be specified as a destination in custom routers
// https://github.com/oxidecomputer/omicron/blob/4f27433d1bca57eb02073a4ea1cd14557f70b8c7/nexus/src/app/vpc_router.rs#L363
const destTypes: Record<Exclude<RouteDestination['type'], 'vpc'>, string> = {
ip: 'IP',
ip_net: 'IP network',
subnet: 'Subnet',
}

// Subnets and VPCs cannot be used as a target in custom routers
// https://github.com/oxidecomputer/omicron/blob/4f27433d1bca57eb02073a4ea1cd14557f70b8c7/nexus/src/app/vpc_router.rs#L362-L368
const targetTypes: Record<Exclude<RouteTarget['type'], 'subnet' | 'vpc'>, string> = {
ip: 'IP',
instance: 'Instance',
internet_gateway: 'Internet gateway',
drop: 'Drop',
}

const toItems = (mapping: Record<string, string>) =>
Object.entries(mapping).map(([value, label]) => ({ value, label }))

type RouteFormFieldsProps = {
form: UseFormReturn<RouteFormValues>
isDisabled?: boolean
}
export const RouteFormFields = ({ form, isDisabled }: RouteFormFieldsProps) => {
const { control } = form
const targetType = form.watch('target.type')
return (
<>
{isDisabled && (
<Message variant="info" content={routeFormMessage.vpcSubnetNotModifiable} />
)}
<NameField name="name" control={control} disabled={isDisabled} />
<DescriptionField name="description" control={control} disabled={isDisabled} />
<ListboxField
name="destination.type"
label="Destination type"
control={control}
items={toItems(destTypes)}
placeholder="Select a destination type"
required
disabled={isDisabled}
/>
<TextField
name="destination.value"
label="Destination value"
control={control}
placeholder="Enter a destination value"
required
disabled={isDisabled}
/>
<ListboxField
name="target.type"
label="Target type"
control={control}
items={toItems(targetTypes)}
placeholder="Select a target type"
required
onChange={(value) => {
form.setValue('target.value', value === 'internet_gateway' ? 'outbound' : '')
}}
disabled={isDisabled}
/>
{targetType !== 'drop' && (
<TextField
name="target.value"
label="Target value"
control={control}
placeholder="Enter a target value"
required
// 'internet_gateway' targetTypes can only have the value 'outbound', so we disable the field
disabled={isDisabled || targetType === 'internet_gateway'}
description={
targetType === 'internet_gateway' && routeFormMessage.internetGatewayTargetValue
}
/>
)}
</>
)
}
48 changes: 7 additions & 41 deletions app/forms/vpc-router-route-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,15 @@
import { useForm } from 'react-hook-form'
import { useNavigate } from 'react-router-dom'

import { useApiMutation, useApiQueryClient, type RouterRouteCreate } from '@oxide/api'
import { useApiMutation, useApiQueryClient } 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 { RouteFormFields, type RouteFormValues } from '~/forms/vpc-router-route-common'
import { useVpcRouterSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { pb } from '~/util/path-builder'

const defaultValues: RouterRouteCreate = {
const defaultValues: RouteFormValues = {
name: '',
description: '',
destination: { type: 'ip', value: '' },
Expand All @@ -32,27 +28,22 @@ export function CreateRouterRouteSideModalForm() {
const routerSelector = useVpcRouterSelector()
const navigate = useNavigate()

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

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

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

return (
<SideModalForm
form={form}
formType="create"
resourceName="route"
onDismiss={onDismiss}
onDismiss={() => navigate(pb.vpcRouter(routerSelector))}
onSubmit={({ name, description, destination, target }) =>
createRouterRoute.mutate({
query: routerSelector,
Expand All @@ -68,32 +59,7 @@ export function CreateRouterRouteSideModalForm() {
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)}
/>
)}
<RouteFormFields form={form} />
</SideModalForm>
)
}
77 changes: 15 additions & 62 deletions app/forms/vpc-router-route-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,84 +14,63 @@ import {
useApiMutation,
useApiQueryClient,
usePrefetchedApiQuery,
type RouterRouteUpdate,
} 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,
RouteFormFields,
routeFormMessage,
targetValueDescription,
} from '~/forms/vpc-router-route/shared'
type RouteFormValues,
} from '~/forms/vpc-router-route-common'
import { getVpcRouterRouteSelector, useVpcRouterRouteSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { Message } from '~/ui/lib/Message'
import { pb } from '~/util/path-builder'

EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
const { project, vpc, router, route } = getVpcRouterRouteSelector(params)
const { route, ...routerSelector } = getVpcRouterRouteSelector(params)
await apiQueryClient.prefetchQuery('vpcRouterRouteView', {
path: { route },
query: { project, vpc, router },
query: routerSelector,
})
return null
}

export function EditRouterRouteSideModalForm() {
const queryClient = useApiQueryClient()
const routeSelector = useVpcRouterRouteSelector()
const { project, vpc, router: routerName, route: routeName } = routeSelector
const { route: routeName, ...routerSelector } = useVpcRouterRouteSelector()
const navigate = useNavigate()
const { data: route } = usePrefetchedApiQuery('vpcRouterRouteView', {
path: { route: routeName },
query: { project, vpc, router: routerName },
query: routerSelector,
})

const defaultValues: RouterRouteUpdate = R.pick(route, [
const defaultValues: RouteFormValues = R.pick(route, [
'name',
'description',
'target',
'destination',
])

const onDismiss = () => {
navigate(pb.vpcRouter({ project, vpc, router: routerName }))
}
const form = useForm({ defaultValues })
const isDisabled = route?.kind === 'vpc_subnet'

const updateRouterRoute = useApiMutation('vpcRouterRouteUpdate', {
onSuccess() {
queryClient.invalidateQueries('vpcRouterRouteList')
addToast({ content: 'Your route has been updated' })
onDismiss()
navigate(pb.vpcRouter(routerSelector))
},
})

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

let isDisabled = false
let disabledReason = ''

// Can simplify this if there aren't other disabling reasons
if (route?.kind === 'vpc_subnet') {
isDisabled = true
disabledReason = routeFormMessage.vpcSubnetNotModifiable
}

return (
<SideModalForm
form={form}
formType="edit"
resourceName="route"
onDismiss={onDismiss}
onDismiss={() => navigate(pb.vpcRouter(routerSelector))}
onSubmit={({ name, description, destination, target }) =>
updateRouterRoute.mutate({
query: { project, vpc, router: routerName },
path: { route: routeName },
query: routerSelector,
body: {
name,
description,
Expand All @@ -103,35 +82,9 @@ export function EditRouterRouteSideModalForm() {
}
loading={updateRouterRoute.isPending}
submitError={updateRouterRoute.error}
submitDisabled={isDisabled ? routeFormMessage.vpcSubnetNotModifiable : undefined}
>
{isDisabled && <Message variant="info" content={disabledReason} />}
<NameField name="name" control={form.control} disabled={isDisabled} />
<DescriptionField name="description" control={form.control} disabled={isDisabled} />
<ListboxField {...fields.destType} control={form.control} disabled={isDisabled} />
<TextField {...fields.destValue} control={form.control} disabled={isDisabled} />
<ListboxField
{...fields.targetType}
control={form.control}
disabled={isDisabled}
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={isDisabled || targetType === 'internet_gateway'}
description={targetValueDescription(targetType)}
/>
)}
<RouteFormFields form={form} isDisabled={isDisabled} />
</SideModalForm>
)
}
Loading

0 comments on commit a5c2361

Please sign in to comment.