@@ -18,24 +18,43 @@ import {
1818 TextFieldHint ,
1919} from '@oxide/ui'
2020import type { VpcFirewallRule , ErrorResponse } from '@oxide/api'
21- import { useApiMutation , useApiQueryClient } from '@oxide/api'
21+ import { parsePortRange , useApiMutation , useApiQueryClient } from '@oxide/api'
2222import { getServerError } from 'app/util/errors'
2323
2424type FormProps = {
2525 error : ErrorResponse | null
2626 id : string
2727}
2828
29- // TODO (can pass to useFormikContext to get it to behave)
30- // type FormState = {}
29+ type Values = {
30+ enabled : boolean
31+ priority : string
32+ name : string
33+ description : string
34+ action : VpcFirewallRule [ 'action' ]
35+ direction : VpcFirewallRule [ 'direction' ]
36+
37+ protocols : NonNullable < VpcFirewallRule [ 'filters' ] [ 'protocols' ] >
38+
39+ // port subform
40+ ports : NonNullable < VpcFirewallRule [ 'filters' ] [ 'ports' ] >
41+ portRange : string
42+
43+ // host subform
44+ hosts : NonNullable < VpcFirewallRule [ 'filters' ] [ 'hosts' ] >
45+ hostType : string
46+ hostValue : string
47+
48+ // target subform
49+ targets : VpcFirewallRule [ 'targets' ]
50+ targetType : string
51+ targetValue : string
52+ }
3153
3254// the moment the two forms diverge, inline them rather than introducing BS
3355// props here
3456const CommonForm = ( { id, error } : FormProps ) => {
35- const {
36- setFieldValue,
37- values : { targetName, targetType, targets } ,
38- } = useFormikContext ( )
57+ const { setFieldValue, values } = useFormikContext < Values > ( )
3958 return (
4059 < Form id = { id } >
4160 < SideModal . Section >
@@ -75,28 +94,38 @@ const CommonForm = ({ id, error }: FormProps) => {
7594 { value : 'subnet' , label : 'VPC Subnet' } ,
7695 { value : 'instance' , label : 'Instance' } ,
7796 ] }
78- // TODO: this is kind of a hack? move this inside Dropdown somehow
7997 onChange = { ( item ) => {
8098 setFieldValue ( 'targetType' , item ?. value )
8199 } }
82100 />
83101 < div className = "space-y-0.5" >
84- < FieldTitle htmlFor = "targetName " > Name</ FieldTitle >
85- < TextField id = "targetName " name = "targetName " />
102+ < FieldTitle htmlFor = "targetValue " > Name</ FieldTitle >
103+ < TextField id = "targetValue " name = "targetValue " />
86104 </ div >
87105
88106 < div className = "flex justify-end" >
107+ { /* TODO does this clear out the form or the existing targets? */ }
89108 < Button variant = "ghost" className = "mr-2.5" >
90109 Clear
91110 </ Button >
92111 < Button
93112 variant = "dim"
94113 onClick = { ( ) => {
95- if ( ! targets . some ( ( t ) => t . name === targetName ) ) {
114+ if (
115+ values . targetType &&
116+ values . targetValue && // TODO: validate
117+ ! values . targets . some (
118+ ( t ) =>
119+ t . value === values . targetValue &&
120+ t . type === values . targetType
121+ )
122+ ) {
96123 setFieldValue ( 'targets' , [
97- ...targets ,
98- { type : targetType , name : targetName } ,
124+ ...values . targets ,
125+ { type : values . targetType , value : values . targetValue } ,
99126 ] )
127+ setFieldValue ( 'targetValue' , '' )
128+ // TODO: clear dropdown too?
100129 }
101130 } }
102131 >
@@ -113,17 +142,20 @@ const CommonForm = ({ id, error }: FormProps) => {
113142 </ Table . HeaderRow >
114143 </ Table . Header >
115144 < Table . Body >
116- { targets . map ( ( t ) => (
117- < Table . Row key = { t . name } >
145+ { values . targets . map ( ( t ) => (
146+ < Table . Row key = { `${ t . type } |${ t . value } ` } >
147+ { /* TODO: should be the pretty type label, not the type key */ }
118148 < Table . Cell > { t . type } </ Table . Cell >
119- < Table . Cell > { t . name } </ Table . Cell >
149+ < Table . Cell > { t . value } </ Table . Cell >
120150 < Table . Cell >
121151 < Delete10Icon
122152 className = "cursor-pointer"
123153 onClick = { ( ) => {
124154 setFieldValue (
125155 'targets' ,
126- targets . filter ( ( t1 ) => t1 . name !== t . name )
156+ values . targets . filter (
157+ ( t1 ) => t1 . value !== t . value || t1 . type !== t . type
158+ )
127159 )
128160 } }
129161 />
@@ -144,28 +176,52 @@ const CommonForm = ({ id, error }: FormProps) => {
144176 { value : 'ip' , label : 'IP' } ,
145177 { value : 'internet_gateway' , label : 'Internet Gateway' } ,
146178 ] }
179+ onChange = { ( item ) => {
180+ setFieldValue ( 'hostType' , item ?. value )
181+ } }
147182 />
148183 < div className = "space-y-0.5" >
149184 { /* For everything but IP this is a name, but for IP it's an IP.
150185 So we should probably have the label on this field change when the
151186 host type changes. Also need to confirm that it's just an IP and
152187 not a block. */ }
153- < FieldTitle htmlFor = "host-filter-value " > Value</ FieldTitle >
154- < TextFieldHint id = "host-filter-value -hint" >
188+ < FieldTitle htmlFor = "hostValue " > Value</ FieldTitle >
189+ < TextFieldHint id = "hostValue -hint" >
155190 For IP, an address. For the rest, a name. [TODO: copy]
156191 </ TextFieldHint >
157192 < TextField
158- id = "host-filter-value "
159- name = "host-filter-value "
160- aria-describedby = "host-filter-value -hint"
193+ id = "hostValue "
194+ name = "hostValue "
195+ aria-describedby = "hostValue -hint"
161196 />
162197 </ div >
163198
164199 < div className = "flex justify-end" >
165200 < Button variant = "ghost" className = "mr-2.5" >
166201 Clear
167202 </ Button >
168- < Button variant = "dim" > Add host filter</ Button >
203+ < Button
204+ variant = "dim"
205+ onClick = { ( ) => {
206+ if (
207+ values . hostType &&
208+ values . hostValue && // TODO: validate
209+ ! values . hosts . some (
210+ ( t ) =>
211+ t . value === values . hostValue || t . type === values . hostType
212+ )
213+ ) {
214+ setFieldValue ( 'hosts' , [
215+ ...values . hosts ,
216+ { type : values . hostType , value : values . hostValue } ,
217+ ] )
218+ setFieldValue ( 'hostValue' , '' )
219+ // TODO: clear dropdown too?
220+ }
221+ } }
222+ >
223+ Add host filter
224+ </ Button >
169225 </ div >
170226
171227 < Table className = "w-full" >
@@ -177,13 +233,26 @@ const CommonForm = ({ id, error }: FormProps) => {
177233 </ Table . HeaderRow >
178234 </ Table . Header >
179235 < Table . Body >
180- < Table . Row >
181- < Table . Cell > VPC</ Table . Cell >
182- < Table . Cell > my-vpc</ Table . Cell >
183- < Table . Cell >
184- < Delete10Icon className = "cursor-pointer" />
185- </ Table . Cell >
186- </ Table . Row >
236+ { values . hosts . map ( ( h ) => (
237+ < Table . Row key = { `${ h . type } |${ h . value } ` } >
238+ { /* TODO: should be the pretty type label, not the type key */ }
239+ < Table . Cell > { h . type } </ Table . Cell >
240+ < Table . Cell > { h . value } </ Table . Cell >
241+ < Table . Cell >
242+ < Delete10Icon
243+ className = "cursor-pointer"
244+ onClick = { ( ) => {
245+ setFieldValue (
246+ 'hosts' ,
247+ values . hosts . filter (
248+ ( h1 ) => h1 . value !== h . value && h1 . type !== h . type
249+ )
250+ )
251+ } }
252+ />
253+ </ Table . Cell >
254+ </ Table . Row >
255+ ) ) }
187256 </ Table . Body >
188257 </ Table >
189258 </ SideModal . Section >
@@ -204,21 +273,37 @@ const CommonForm = ({ id, error }: FormProps) => {
204273 < Button variant = "ghost" className = "mr-2.5" >
205274 Clear
206275 </ Button >
207- < Button variant = "dim" > Add port filter</ Button >
276+ < Button
277+ variant = "dim"
278+ onClick = { ( ) => {
279+ const portRange = values . portRange . trim ( )
280+ const ports = parsePortRange ( portRange )
281+ if ( ! ports ) return
282+ const [ p1 , p2 ] = ports
283+ if ( p2 === null || p2 > p1 ) {
284+ // TODO: can ranges overlap? don't see why not, API can union them
285+ setFieldValue ( 'ports' , [ ...values . ports , portRange ] )
286+ }
287+ } }
288+ >
289+ Add port filter
290+ </ Button >
208291 </ div >
209292 < ul >
210- < li >
211- 1234
212- < Delete10Icon className = "cursor-pointer ml-2" />
213- </ li >
214- < li >
215- 456-567
216- < Delete10Icon className = "cursor-pointer ml-2" />
217- </ li >
218- < li >
219- 8080-8086
220- < Delete10Icon className = "cursor-pointer ml-2" />
221- </ li >
293+ { values . ports . map ( ( p ) => (
294+ < li key = { p } >
295+ { p }
296+ < Delete10Icon
297+ className = "cursor-pointer ml-2"
298+ onClick = { ( ) => {
299+ setFieldValue (
300+ 'ports' ,
301+ values . ports . filter ( ( p1 ) => p1 !== p )
302+ )
303+ } }
304+ />
305+ </ li >
306+ ) ) }
222307 </ ul >
223308 </ div >
224309 </ SideModal . Section >
@@ -308,25 +393,34 @@ export function CreateFirewallRuleModal({
308393 onDismiss = { dismiss }
309394 >
310395 < Formik
311- initialValues = { {
312- enabled : false ,
313- priority : '' ,
314- name : '' ,
315- description : '' ,
316- action : 'allow' ,
317- direction : 'inbound' ,
318- // TODO: in the request body, these go in a `filters` object. we probably don't
319- // need such nesting here though. not even sure how to do it
320- // filters
321- protocols : [ ] ,
322- ports : [ ] ,
323- hosts : [ ] ,
396+ initialValues = {
397+ {
398+ enabled : false ,
399+ priority : '' ,
400+ name : '' ,
401+ description : '' ,
402+ action : 'allow' ,
403+ direction : 'inbound' ,
324404
325- // target subform
326- targets : [ ] ,
327- targetType : '' ,
328- targetName : '' ,
329- } }
405+ // in the request body, these go in a `filters` object. we probably don't
406+ // need such nesting here though. not even sure how to do it
407+ protocols : [ ] ,
408+
409+ // port subform
410+ ports : [ ] ,
411+ portRange : '' ,
412+
413+ // host subform
414+ hosts : [ ] ,
415+ hostType : '' ,
416+ hostValue : '' ,
417+
418+ // target subform
419+ targets : [ ] ,
420+ targetType : '' ,
421+ targetValue : '' ,
422+ } as Values // best way to tell formik this type
423+ }
330424 validationSchema = { Yup . object ( {
331425 priority : Yup . number ( )
332426 . integer ( )
0 commit comments