Skip to content

Commit a5c2361

Browse files
authored
Extract common fields and refactor VPC router route forms (#2436)
1 parent ca27233 commit a5c2361

File tree

6 files changed

+141
-181
lines changed

6 files changed

+141
-181
lines changed

app/forms/vpc-router-route-common.tsx

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/*
2+
* This Source Code Form is subject to the terms of the Mozilla Public
3+
* License, v. 2.0. If a copy of the MPL was not distributed with this
4+
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
*
6+
* Copyright Oxide Computer Company
7+
*/
8+
9+
import type { UseFormReturn } from 'react-hook-form'
10+
11+
import type {
12+
RouteDestination,
13+
RouterRouteCreate,
14+
RouterRouteUpdate,
15+
RouteTarget,
16+
} from '~/api'
17+
import { DescriptionField } from '~/components/form/fields/DescriptionField'
18+
import { ListboxField } from '~/components/form/fields/ListboxField'
19+
import { NameField } from '~/components/form/fields/NameField'
20+
import { TextField } from '~/components/form/fields/TextField'
21+
import { Message } from '~/ui/lib/Message'
22+
23+
export type RouteFormValues = RouterRouteCreate | Required<RouterRouteUpdate>
24+
25+
export const routeFormMessage = {
26+
vpcSubnetNotModifiable:
27+
'Routes of type VPC Subnet within the system router are not modifiable',
28+
internetGatewayTargetValue:
29+
'For ‘Internet gateway’ targets, the value must be ‘outbound’',
30+
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L201-L204
31+
noNewRoutesOnSystemRouter: 'User-provided routes cannot be added to a system router',
32+
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L300-L304
33+
noDeletingRoutesOnSystemRouter: 'System routes cannot be deleted',
34+
// https://github.com/oxidecomputer/omicron/blob/914f5fd7d51f9b060dcc0382a30b607e25df49b2/nexus/src/app/vpc_router.rs#L136-L138
35+
noDeletingSystemRouters: 'System routers cannot be deleted',
36+
}
37+
38+
// VPCs cannot be specified as a destination in custom routers
39+
// https://github.com/oxidecomputer/omicron/blob/4f27433d1bca57eb02073a4ea1cd14557f70b8c7/nexus/src/app/vpc_router.rs#L363
40+
const destTypes: Record<Exclude<RouteDestination['type'], 'vpc'>, string> = {
41+
ip: 'IP',
42+
ip_net: 'IP network',
43+
subnet: 'Subnet',
44+
}
45+
46+
// Subnets and VPCs cannot be used as a target in custom routers
47+
// https://github.com/oxidecomputer/omicron/blob/4f27433d1bca57eb02073a4ea1cd14557f70b8c7/nexus/src/app/vpc_router.rs#L362-L368
48+
const targetTypes: Record<Exclude<RouteTarget['type'], 'subnet' | 'vpc'>, string> = {
49+
ip: 'IP',
50+
instance: 'Instance',
51+
internet_gateway: 'Internet gateway',
52+
drop: 'Drop',
53+
}
54+
55+
const toItems = (mapping: Record<string, string>) =>
56+
Object.entries(mapping).map(([value, label]) => ({ value, label }))
57+
58+
type RouteFormFieldsProps = {
59+
form: UseFormReturn<RouteFormValues>
60+
isDisabled?: boolean
61+
}
62+
export const RouteFormFields = ({ form, isDisabled }: RouteFormFieldsProps) => {
63+
const { control } = form
64+
const targetType = form.watch('target.type')
65+
return (
66+
<>
67+
{isDisabled && (
68+
<Message variant="info" content={routeFormMessage.vpcSubnetNotModifiable} />
69+
)}
70+
<NameField name="name" control={control} disabled={isDisabled} />
71+
<DescriptionField name="description" control={control} disabled={isDisabled} />
72+
<ListboxField
73+
name="destination.type"
74+
label="Destination type"
75+
control={control}
76+
items={toItems(destTypes)}
77+
placeholder="Select a destination type"
78+
required
79+
disabled={isDisabled}
80+
/>
81+
<TextField
82+
name="destination.value"
83+
label="Destination value"
84+
control={control}
85+
placeholder="Enter a destination value"
86+
required
87+
disabled={isDisabled}
88+
/>
89+
<ListboxField
90+
name="target.type"
91+
label="Target type"
92+
control={control}
93+
items={toItems(targetTypes)}
94+
placeholder="Select a target type"
95+
required
96+
onChange={(value) => {
97+
form.setValue('target.value', value === 'internet_gateway' ? 'outbound' : '')
98+
}}
99+
disabled={isDisabled}
100+
/>
101+
{targetType !== 'drop' && (
102+
<TextField
103+
name="target.value"
104+
label="Target value"
105+
control={control}
106+
placeholder="Enter a target value"
107+
required
108+
// 'internet_gateway' targetTypes can only have the value 'outbound', so we disable the field
109+
disabled={isDisabled || targetType === 'internet_gateway'}
110+
description={
111+
targetType === 'internet_gateway' && routeFormMessage.internetGatewayTargetValue
112+
}
113+
/>
114+
)}
115+
</>
116+
)
117+
}

app/forms/vpc-router-route-create.tsx

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,15 @@
88
import { useForm } from 'react-hook-form'
99
import { useNavigate } from 'react-router-dom'
1010

11-
import { useApiMutation, useApiQueryClient, type RouterRouteCreate } from '@oxide/api'
11+
import { useApiMutation, useApiQueryClient } from '@oxide/api'
1212

13-
import { DescriptionField } from '~/components/form/fields/DescriptionField'
14-
import { ListboxField } from '~/components/form/fields/ListboxField'
15-
import { NameField } from '~/components/form/fields/NameField'
16-
import { TextField } from '~/components/form/fields/TextField'
1713
import { SideModalForm } from '~/components/form/SideModalForm'
18-
import { fields, targetValueDescription } from '~/forms/vpc-router-route/shared'
14+
import { RouteFormFields, type RouteFormValues } from '~/forms/vpc-router-route-common'
1915
import { useVpcRouterSelector } from '~/hooks/use-params'
2016
import { addToast } from '~/stores/toast'
2117
import { pb } from '~/util/path-builder'
2218

23-
const defaultValues: RouterRouteCreate = {
19+
const defaultValues: RouteFormValues = {
2420
name: '',
2521
description: '',
2622
destination: { type: 'ip', value: '' },
@@ -32,27 +28,22 @@ export function CreateRouterRouteSideModalForm() {
3228
const routerSelector = useVpcRouterSelector()
3329
const navigate = useNavigate()
3430

35-
const onDismiss = () => {
36-
navigate(pb.vpcRouter(routerSelector))
37-
}
31+
const form = useForm({ defaultValues })
3832

3933
const createRouterRoute = useApiMutation('vpcRouterRouteCreate', {
4034
onSuccess() {
4135
queryClient.invalidateQueries('vpcRouterRouteList')
4236
addToast({ content: 'Your route has been created' })
43-
onDismiss()
37+
navigate(pb.vpcRouter(routerSelector))
4438
},
4539
})
4640

47-
const form = useForm({ defaultValues })
48-
const targetType = form.watch('target.type')
49-
5041
return (
5142
<SideModalForm
5243
form={form}
5344
formType="create"
5445
resourceName="route"
55-
onDismiss={onDismiss}
46+
onDismiss={() => navigate(pb.vpcRouter(routerSelector))}
5647
onSubmit={({ name, description, destination, target }) =>
5748
createRouterRoute.mutate({
5849
query: routerSelector,
@@ -68,32 +59,7 @@ export function CreateRouterRouteSideModalForm() {
6859
loading={createRouterRoute.isPending}
6960
submitError={createRouterRoute.error}
7061
>
71-
<NameField name="name" control={form.control} />
72-
<DescriptionField name="description" control={form.control} />
73-
<ListboxField {...fields.destType} control={form.control} />
74-
<TextField {...fields.destValue} control={form.control} />
75-
<ListboxField
76-
{...fields.targetType}
77-
control={form.control}
78-
onChange={(value) => {
79-
// 'outbound' is only valid option when targetType is 'internet_gateway'
80-
if (value === 'internet_gateway') {
81-
form.setValue('target.value', 'outbound')
82-
}
83-
if (value === 'drop') {
84-
form.setValue('target.value', '')
85-
}
86-
}}
87-
/>
88-
{targetType !== 'drop' && (
89-
<TextField
90-
{...fields.targetValue}
91-
control={form.control}
92-
// when targetType is 'internet_gateway', we set it to `outbound` and make it non-editable
93-
disabled={targetType === 'internet_gateway'}
94-
description={targetValueDescription(targetType)}
95-
/>
96-
)}
62+
<RouteFormFields form={form} />
9763
</SideModalForm>
9864
)
9965
}

app/forms/vpc-router-route-edit.tsx

Lines changed: 15 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,84 +14,63 @@ import {
1414
useApiMutation,
1515
useApiQueryClient,
1616
usePrefetchedApiQuery,
17-
type RouterRouteUpdate,
1817
} from '@oxide/api'
1918

20-
import { DescriptionField } from '~/components/form/fields/DescriptionField'
21-
import { ListboxField } from '~/components/form/fields/ListboxField'
22-
import { NameField } from '~/components/form/fields/NameField'
23-
import { TextField } from '~/components/form/fields/TextField'
2419
import { SideModalForm } from '~/components/form/SideModalForm'
2520
import {
26-
fields,
21+
RouteFormFields,
2722
routeFormMessage,
28-
targetValueDescription,
29-
} from '~/forms/vpc-router-route/shared'
23+
type RouteFormValues,
24+
} from '~/forms/vpc-router-route-common'
3025
import { getVpcRouterRouteSelector, useVpcRouterRouteSelector } from '~/hooks/use-params'
3126
import { addToast } from '~/stores/toast'
32-
import { Message } from '~/ui/lib/Message'
3327
import { pb } from '~/util/path-builder'
3428

3529
EditRouterRouteSideModalForm.loader = async ({ params }: LoaderFunctionArgs) => {
36-
const { project, vpc, router, route } = getVpcRouterRouteSelector(params)
30+
const { route, ...routerSelector } = getVpcRouterRouteSelector(params)
3731
await apiQueryClient.prefetchQuery('vpcRouterRouteView', {
3832
path: { route },
39-
query: { project, vpc, router },
33+
query: routerSelector,
4034
})
4135
return null
4236
}
4337

4438
export function EditRouterRouteSideModalForm() {
4539
const queryClient = useApiQueryClient()
46-
const routeSelector = useVpcRouterRouteSelector()
47-
const { project, vpc, router: routerName, route: routeName } = routeSelector
40+
const { route: routeName, ...routerSelector } = useVpcRouterRouteSelector()
4841
const navigate = useNavigate()
4942
const { data: route } = usePrefetchedApiQuery('vpcRouterRouteView', {
5043
path: { route: routeName },
51-
query: { project, vpc, router: routerName },
44+
query: routerSelector,
5245
})
5346

54-
const defaultValues: RouterRouteUpdate = R.pick(route, [
47+
const defaultValues: RouteFormValues = R.pick(route, [
5548
'name',
5649
'description',
5750
'target',
5851
'destination',
5952
])
60-
61-
const onDismiss = () => {
62-
navigate(pb.vpcRouter({ project, vpc, router: routerName }))
63-
}
53+
const form = useForm({ defaultValues })
54+
const isDisabled = route?.kind === 'vpc_subnet'
6455

6556
const updateRouterRoute = useApiMutation('vpcRouterRouteUpdate', {
6657
onSuccess() {
6758
queryClient.invalidateQueries('vpcRouterRouteList')
6859
addToast({ content: 'Your route has been updated' })
69-
onDismiss()
60+
navigate(pb.vpcRouter(routerSelector))
7061
},
7162
})
7263

73-
const form = useForm({ defaultValues })
74-
const targetType = form.watch('target.type')
75-
76-
let isDisabled = false
77-
let disabledReason = ''
78-
79-
// Can simplify this if there aren't other disabling reasons
80-
if (route?.kind === 'vpc_subnet') {
81-
isDisabled = true
82-
disabledReason = routeFormMessage.vpcSubnetNotModifiable
83-
}
84-
8564
return (
8665
<SideModalForm
8766
form={form}
8867
formType="edit"
8968
resourceName="route"
90-
onDismiss={onDismiss}
69+
onDismiss={() => navigate(pb.vpcRouter(routerSelector))}
9170
onSubmit={({ name, description, destination, target }) =>
9271
updateRouterRoute.mutate({
93-
query: { project, vpc, router: routerName },
9472
path: { route: routeName },
73+
query: routerSelector,
9574
body: {
9675
name,
9776
description,
@@ -103,35 +82,9 @@ export function EditRouterRouteSideModalForm() {
10382
}
10483
loading={updateRouterRoute.isPending}
10584
submitError={updateRouterRoute.error}
85+
submitDisabled={isDisabled ? routeFormMessage.vpcSubnetNotModifiable : undefined}
10686
>
107-
{isDisabled && <Message variant="info" content={disabledReason} />}
108-
<NameField name="name" control={form.control} disabled={isDisabled} />
109-
<DescriptionField name="description" control={form.control} disabled={isDisabled} />
110-
<ListboxField {...fields.destType} control={form.control} disabled={isDisabled} />
111-
<TextField {...fields.destValue} control={form.control} disabled={isDisabled} />
112-
<ListboxField
113-
{...fields.targetType}
114-
control={form.control}
115-
disabled={isDisabled}
116-
onChange={(value) => {
117-
// 'outbound' is only valid option when targetType is 'internet_gateway'
118-
if (value === 'internet_gateway') {
119-
form.setValue('target.value', 'outbound')
120-
}
121-
if (value === 'drop') {
122-
form.setValue('target.value', '')
123-
}
124-
}}
125-
/>
126-
{targetType !== 'drop' && (
127-
<TextField
128-
{...fields.targetValue}
129-
control={form.control}
130-
// when targetType is 'internet_gateway', we set it to `outbound` and make it non-editable
131-
disabled={isDisabled || targetType === 'internet_gateway'}
132-
description={targetValueDescription(targetType)}
133-
/>
134-
)}
87+
<RouteFormFields form={form} isDisabled={isDisabled} />
13588
</SideModalForm>
13689
)
13790
}

0 commit comments

Comments
 (0)