@@ -18,24 +18,43 @@ import {
18
18
TextFieldHint ,
19
19
} from '@oxide/ui'
20
20
import type { VpcFirewallRule , ErrorResponse } from '@oxide/api'
21
- import { useApiMutation , useApiQueryClient } from '@oxide/api'
21
+ import { parsePortRange , useApiMutation , useApiQueryClient } from '@oxide/api'
22
22
import { getServerError } from 'app/util/errors'
23
23
24
24
type FormProps = {
25
25
error : ErrorResponse | null
26
26
id : string
27
27
}
28
28
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
+ }
31
53
32
54
// the moment the two forms diverge, inline them rather than introducing BS
33
55
// props here
34
56
const CommonForm = ( { id, error } : FormProps ) => {
35
- const {
36
- setFieldValue,
37
- values : { targetName, targetType, targets } ,
38
- } = useFormikContext ( )
57
+ const { setFieldValue, values } = useFormikContext < Values > ( )
39
58
return (
40
59
< Form id = { id } >
41
60
< SideModal . Section >
@@ -75,28 +94,38 @@ const CommonForm = ({ id, error }: FormProps) => {
75
94
{ value : 'subnet' , label : 'VPC Subnet' } ,
76
95
{ value : 'instance' , label : 'Instance' } ,
77
96
] }
78
- // TODO: this is kind of a hack? move this inside Dropdown somehow
79
97
onChange = { ( item ) => {
80
98
setFieldValue ( 'targetType' , item ?. value )
81
99
} }
82
100
/>
83
101
< 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 " />
86
104
</ div >
87
105
88
106
< div className = "flex justify-end" >
107
+ { /* TODO does this clear out the form or the existing targets? */ }
89
108
< Button variant = "ghost" className = "mr-2.5" >
90
109
Clear
91
110
</ Button >
92
111
< Button
93
112
variant = "dim"
94
113
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
+ ) {
96
123
setFieldValue ( 'targets' , [
97
- ...targets ,
98
- { type : targetType , name : targetName } ,
124
+ ...values . targets ,
125
+ { type : values . targetType , value : values . targetValue } ,
99
126
] )
127
+ setFieldValue ( 'targetValue' , '' )
128
+ // TODO: clear dropdown too?
100
129
}
101
130
} }
102
131
>
@@ -113,17 +142,20 @@ const CommonForm = ({ id, error }: FormProps) => {
113
142
</ Table . HeaderRow >
114
143
</ Table . Header >
115
144
< 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 */ }
118
148
< Table . Cell > { t . type } </ Table . Cell >
119
- < Table . Cell > { t . name } </ Table . Cell >
149
+ < Table . Cell > { t . value } </ Table . Cell >
120
150
< Table . Cell >
121
151
< Delete10Icon
122
152
className = "cursor-pointer"
123
153
onClick = { ( ) => {
124
154
setFieldValue (
125
155
'targets' ,
126
- targets . filter ( ( t1 ) => t1 . name !== t . name )
156
+ values . targets . filter (
157
+ ( t1 ) => t1 . value !== t . value || t1 . type !== t . type
158
+ )
127
159
)
128
160
} }
129
161
/>
@@ -144,28 +176,52 @@ const CommonForm = ({ id, error }: FormProps) => {
144
176
{ value : 'ip' , label : 'IP' } ,
145
177
{ value : 'internet_gateway' , label : 'Internet Gateway' } ,
146
178
] }
179
+ onChange = { ( item ) => {
180
+ setFieldValue ( 'hostType' , item ?. value )
181
+ } }
147
182
/>
148
183
< div className = "space-y-0.5" >
149
184
{ /* For everything but IP this is a name, but for IP it's an IP.
150
185
So we should probably have the label on this field change when the
151
186
host type changes. Also need to confirm that it's just an IP and
152
187
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" >
155
190
For IP, an address. For the rest, a name. [TODO: copy]
156
191
</ TextFieldHint >
157
192
< 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"
161
196
/>
162
197
</ div >
163
198
164
199
< div className = "flex justify-end" >
165
200
< Button variant = "ghost" className = "mr-2.5" >
166
201
Clear
167
202
</ 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 >
169
225
</ div >
170
226
171
227
< Table className = "w-full" >
@@ -177,13 +233,26 @@ const CommonForm = ({ id, error }: FormProps) => {
177
233
</ Table . HeaderRow >
178
234
</ Table . Header >
179
235
< 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
+ ) ) }
187
256
</ Table . Body >
188
257
</ Table >
189
258
</ SideModal . Section >
@@ -204,21 +273,37 @@ const CommonForm = ({ id, error }: FormProps) => {
204
273
< Button variant = "ghost" className = "mr-2.5" >
205
274
Clear
206
275
</ 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 >
208
291
</ div >
209
292
< 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
+ ) ) }
222
307
</ ul >
223
308
</ div >
224
309
</ SideModal . Section >
@@ -308,25 +393,34 @@ export function CreateFirewallRuleModal({
308
393
onDismiss = { dismiss }
309
394
>
310
395
< 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' ,
324
404
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
+ }
330
424
validationSchema = { Yup . object ( {
331
425
priority : Yup . number ( )
332
426
. integer ( )
0 commit comments