Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions app/forms/firewall-rules-common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { validateIp, validateIpNet } from '~/util/ip'
import { links } from '~/util/links'
import { capitalize } from '~/util/str'

import { type ActiveSubforms } from './firewall-rules-create'
import { type FirewallRuleValues } from './firewall-rules-util'

/**
Expand Down Expand Up @@ -88,10 +89,12 @@ const TargetAndHostFilterSubform = ({
sectionType,
control,
messageContent,
updateSubformStates,
}: {
sectionType: 'target' | 'host'
control: Control<FirewallRuleValues>
messageContent: ReactNode
updateSubformStates: (subform: keyof ActiveSubforms, value: boolean) => void
}) => {
const { project, vpc } = useVpcSelector()
// prefetchedApiQueries below are prefetched in firewall-rules-create and -edit
Expand Down Expand Up @@ -125,8 +128,11 @@ const TargetAndHostFilterSubform = ({
// https://github.com/react-hook-form/react-hook-form/blob/9a497a70a/src/logic/createFormControl.ts#L1194-L1203
const { isSubmitSuccessful: subformSubmitSuccessful } = subform.formState
useEffect(() => {
if (subformSubmitSuccessful) subform.reset(targetAndHostDefaultValues)
}, [subformSubmitSuccessful, subform])
if (subformSubmitSuccessful) {
subform.reset(targetAndHostDefaultValues)
updateSubformStates(sectionType, false)
}
}, [subformSubmitSuccessful, subform, updateSubformStates, sectionType])

const [valueType, value] = subform.watch(['type', 'value'])
const sectionItems = {
Expand All @@ -143,9 +149,15 @@ const TargetAndHostFilterSubform = ({
// back to validating on submit instead of change. Also resets readyToSubmit.
const onTypeChange = () => {
subform.reset({ type: subform.getValues('type'), value: '' })
updateSubformStates(sectionType, false)
}
const onInputChange = (value: string) => {
subform.setValue('value', value)
updateSubformStates(sectionType, value.length > 0)
}
const onClear = () => {
subform.reset()
updateSubformStates(sectionType, false)
}

return (
Expand Down Expand Up @@ -178,6 +190,7 @@ const TargetAndHostFilterSubform = ({
description="Select an option or enter a custom value"
control={subformControl}
onEnter={submitSubform}
onChange={() => updateSubformStates(sectionType, true)}
onInputChange={onInputChange}
items={items}
allowArbitraryValues
Expand Down Expand Up @@ -212,7 +225,7 @@ const TargetAndHostFilterSubform = ({
<MiniTable.ClearAndAddButtons
addButtonCopy={`Add ${sectionType === 'host' ? 'host filter' : 'target'}`}
disabled={!value}
onClear={() => subform.reset()}
onClear={onClear}
onSubmit={submitSubform}
/>
{field.value.length > 0 && (
Expand Down Expand Up @@ -289,14 +302,20 @@ type CommonFieldsProps = {
control: Control<FirewallRuleValues>
nameTaken: (name: string) => boolean
error: ApiError | null
updateSubformStates: (subform: keyof ActiveSubforms, value: boolean) => void
}

const targetAndHostDefaultValues: TargetAndHostFormValues = {
type: 'vpc',
value: '',
}

export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) => {
export const CommonFields = ({
control,
nameTaken,
error,
updateSubformStates,
}: CommonFieldsProps) => {
// Ports
const portRangeForm = useForm({ defaultValues: { portRange: '' } })
const ports = useController({ name: 'ports', control }).field
Expand All @@ -307,7 +326,11 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) =
// that it is not already in the list
ports.onChange([...ports.value, portRangeValue])
portRangeForm.reset()
updateSubformStates('port', false)
})
useEffect(() => {
updateSubformStates('port', portValue.length > 0)
}, [updateSubformStates, portValue])

return (
<>
Expand Down Expand Up @@ -406,6 +429,7 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) =
</p>
</>
}
updateSubformStates={updateSubformStates}
/>

<FormDivider />
Expand Down Expand Up @@ -453,7 +477,10 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) =
<MiniTable.ClearAndAddButtons
addButtonCopy="Add port filter"
disabled={!portValue}
onClear={() => portRangeForm.reset()}
onClear={() => {
portRangeForm.reset()
updateSubformStates('port', false)
}}
onSubmit={submitPortRange}
/>
</div>
Expand Down Expand Up @@ -500,6 +527,7 @@ export const CommonFields = ({ control, nameTaken, error }: CommonFieldsProps) =
traffic. For an outbound rule, they match the destination.
</>
}
updateSubformStates={updateSubformStates}
/>

{error && (
Expand Down
25 changes: 25 additions & 0 deletions app/forms/firewall-rules-create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { useNavigate, useParams, type LoaderFunctionArgs } from 'react-router'

Expand All @@ -23,6 +24,7 @@ import { getVpcSelector, useVpcSelector } from '~/hooks/use-params'
import { addToast } from '~/stores/toast'
import { ALL_ISH } from '~/util/consts'
import { pb } from '~/util/path-builder'
import { commaSeries } from '~/util/str'

import { CommonFields } from './firewall-rules-common'
import { valuesToRuleUpdate, type FirewallRuleValues } from './firewall-rules-util'
Expand Down Expand Up @@ -69,7 +71,24 @@ CreateFirewallRuleForm.loader = async ({ params }: LoaderFunctionArgs) => {
return null
}

export type ActiveSubforms = { target: boolean; port: boolean; host: boolean }
const defaultActiveSubforms: ActiveSubforms = { target: false, port: false, host: false }

export function CreateFirewallRuleForm() {
const [subformStates, setSubformStates] = useState(defaultActiveSubforms)
const updateSubformStates = (subform: keyof ActiveSubforms, value: boolean) => {
setSubformStates({
...subformStates,
[subform]: value,
})
}
const activeSubformList = commaSeries(
Object.keys(subformStates).filter((key) => subformStates[key as keyof ActiveSubforms]),
'and'
)
.replace('port', 'port filter')
.replace('host', 'host filter')

const vpcSelector = useVpcSelector()
const queryClient = useApiQueryClient()

Expand Down Expand Up @@ -120,12 +139,18 @@ export function CreateFirewallRuleForm() {
loading={updateRules.isPending}
submitError={updateRules.error}
submitLabel="Add rule"
submitDisabled={
activeSubformList.length
? `You have an unsaved ${activeSubformList} entry; save or clear ${activeSubformList.includes('and') ? 'them' : 'it'} to create this firewall rule`
: undefined
}
>
<CommonFields
control={form.control}
// error if name is already in use
nameTaken={(name) => !!existingRules.find((r) => r.name === name)}
error={updateRules.error}
updateSubformStates={updateSubformStates}
// TODO: there should also be a form-level error so if the name is off
// screen, it doesn't look like the submit button isn't working. Maybe
// instead of setting a root error, it would be more robust to show a
Expand Down
Loading