@@ -18,24 +18,45 @@ 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
+ }
53
+
54
+ // TODO: pressing enter in ports, hosts, and targets value field should "submit" subform
31
55
32
56
// the moment the two forms diverge, inline them rather than introducing BS
33
57
// props here
34
58
const CommonForm = ( { id, error } : FormProps ) => {
35
- const {
36
- setFieldValue,
37
- values : { targetName, targetType, targets } ,
38
- } = useFormikContext ( )
59
+ const { setFieldValue, values } = useFormikContext < Values > ( )
39
60
return (
40
61
< Form id = { id } >
41
62
< SideModal . Section >
@@ -75,28 +96,38 @@ const CommonForm = ({ id, error }: FormProps) => {
75
96
{ value : 'subnet' , label : 'VPC Subnet' } ,
76
97
{ value : 'instance' , label : 'Instance' } ,
77
98
] }
78
- // TODO: this is kind of a hack? move this inside Dropdown somehow
79
99
onChange = { ( item ) => {
80
100
setFieldValue ( 'targetType' , item ?. value )
81
101
} }
82
102
/>
83
103
< div className = "space-y-0.5" >
84
- < FieldTitle htmlFor = "targetName " > Name</ FieldTitle >
85
- < TextField id = "targetName " name = "targetName " />
104
+ < FieldTitle htmlFor = "targetValue " > Name</ FieldTitle >
105
+ < TextField id = "targetValue " name = "targetValue " />
86
106
</ div >
87
107
88
108
< div className = "flex justify-end" >
109
+ { /* TODO does this clear out the form or the existing targets? */ }
89
110
< Button variant = "ghost" className = "mr-2.5" >
90
111
Clear
91
112
</ Button >
92
113
< Button
93
114
variant = "dim"
94
115
onClick = { ( ) => {
95
- if ( ! targets . some ( ( t ) => t . name === targetName ) ) {
116
+ if (
117
+ values . targetType &&
118
+ values . targetValue && // TODO: validate
119
+ ! values . targets . some (
120
+ ( t ) =>
121
+ t . value === values . targetValue &&
122
+ t . type === values . targetType
123
+ )
124
+ ) {
96
125
setFieldValue ( 'targets' , [
97
- ...targets ,
98
- { type : targetType , name : targetName } ,
126
+ ...values . targets ,
127
+ { type : values . targetType , value : values . targetValue } ,
99
128
] )
129
+ setFieldValue ( 'targetValue' , '' )
130
+ // TODO: clear dropdown too?
100
131
}
101
132
} }
102
133
>
@@ -113,17 +144,20 @@ const CommonForm = ({ id, error }: FormProps) => {
113
144
</ Table . HeaderRow >
114
145
</ Table . Header >
115
146
< Table . Body >
116
- { targets . map ( ( t ) => (
117
- < Table . Row key = { t . name } >
147
+ { values . targets . map ( ( t ) => (
148
+ < Table . Row key = { `${ t . type } |${ t . value } ` } >
149
+ { /* TODO: should be the pretty type label, not the type key */ }
118
150
< Table . Cell > { t . type } </ Table . Cell >
119
- < Table . Cell > { t . name } </ Table . Cell >
151
+ < Table . Cell > { t . value } </ Table . Cell >
120
152
< Table . Cell >
121
153
< Delete10Icon
122
154
className = "cursor-pointer"
123
155
onClick = { ( ) => {
124
156
setFieldValue (
125
157
'targets' ,
126
- targets . filter ( ( t1 ) => t1 . name !== t . name )
158
+ values . targets . filter (
159
+ ( t1 ) => t1 . value !== t . value || t1 . type !== t . type
160
+ )
127
161
)
128
162
} }
129
163
/>
@@ -144,28 +178,52 @@ const CommonForm = ({ id, error }: FormProps) => {
144
178
{ value : 'ip' , label : 'IP' } ,
145
179
{ value : 'internet_gateway' , label : 'Internet Gateway' } ,
146
180
] }
181
+ onChange = { ( item ) => {
182
+ setFieldValue ( 'hostType' , item ?. value )
183
+ } }
147
184
/>
148
185
< div className = "space-y-0.5" >
149
186
{ /* For everything but IP this is a name, but for IP it's an IP.
150
187
So we should probably have the label on this field change when the
151
188
host type changes. Also need to confirm that it's just an IP and
152
189
not a block. */ }
153
- < FieldTitle htmlFor = "host-filter-value " > Value</ FieldTitle >
154
- < TextFieldHint id = "host-filter-value -hint" >
190
+ < FieldTitle htmlFor = "hostValue " > Value</ FieldTitle >
191
+ < TextFieldHint id = "hostValue -hint" >
155
192
For IP, an address. For the rest, a name. [TODO: copy]
156
193
</ TextFieldHint >
157
194
< TextField
158
- id = "host-filter-value "
159
- name = "host-filter-value "
160
- aria-describedby = "host-filter-value -hint"
195
+ id = "hostValue "
196
+ name = "hostValue "
197
+ aria-describedby = "hostValue -hint"
161
198
/>
162
199
</ div >
163
200
164
201
< div className = "flex justify-end" >
165
202
< Button variant = "ghost" className = "mr-2.5" >
166
203
Clear
167
204
</ Button >
168
- < Button variant = "dim" > Add host filter</ Button >
205
+ < Button
206
+ variant = "dim"
207
+ onClick = { ( ) => {
208
+ if (
209
+ values . hostType &&
210
+ values . hostValue && // TODO: validate
211
+ ! values . hosts . some (
212
+ ( t ) =>
213
+ t . value === values . hostValue || t . type === values . hostType
214
+ )
215
+ ) {
216
+ setFieldValue ( 'hosts' , [
217
+ ...values . hosts ,
218
+ { type : values . hostType , value : values . hostValue } ,
219
+ ] )
220
+ setFieldValue ( 'hostValue' , '' )
221
+ // TODO: clear dropdown too?
222
+ }
223
+ } }
224
+ >
225
+ Add host filter
226
+ </ Button >
169
227
</ div >
170
228
171
229
< Table className = "w-full" >
@@ -177,13 +235,26 @@ const CommonForm = ({ id, error }: FormProps) => {
177
235
</ Table . HeaderRow >
178
236
</ Table . Header >
179
237
< 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 >
238
+ { values . hosts . map ( ( h ) => (
239
+ < Table . Row key = { `${ h . type } |${ h . value } ` } >
240
+ { /* TODO: should be the pretty type label, not the type key */ }
241
+ < Table . Cell > { h . type } </ Table . Cell >
242
+ < Table . Cell > { h . value } </ Table . Cell >
243
+ < Table . Cell >
244
+ < Delete10Icon
245
+ className = "cursor-pointer"
246
+ onClick = { ( ) => {
247
+ setFieldValue (
248
+ 'hosts' ,
249
+ values . hosts . filter (
250
+ ( h1 ) => h1 . value !== h . value && h1 . type !== h . type
251
+ )
252
+ )
253
+ } }
254
+ />
255
+ </ Table . Cell >
256
+ </ Table . Row >
257
+ ) ) }
187
258
</ Table . Body >
188
259
</ Table >
189
260
</ SideModal . Section >
@@ -204,21 +275,37 @@ const CommonForm = ({ id, error }: FormProps) => {
204
275
< Button variant = "ghost" className = "mr-2.5" >
205
276
Clear
206
277
</ Button >
207
- < Button variant = "dim" > Add port filter</ Button >
278
+ < Button
279
+ variant = "dim"
280
+ onClick = { ( ) => {
281
+ const portRange = values . portRange . trim ( )
282
+ const ports = parsePortRange ( portRange )
283
+ if ( ! ports ) return
284
+ const [ p1 , p2 ] = ports
285
+ if ( p2 === null || p2 > p1 ) {
286
+ // TODO: can ranges overlap? don't see why not, API can union them
287
+ setFieldValue ( 'ports' , [ ...values . ports , portRange ] )
288
+ }
289
+ } }
290
+ >
291
+ Add port filter
292
+ </ Button >
208
293
</ div >
209
294
< 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 >
295
+ { values . ports . map ( ( p ) => (
296
+ < li key = { p } >
297
+ { p }
298
+ < Delete10Icon
299
+ className = "cursor-pointer ml-2"
300
+ onClick = { ( ) => {
301
+ setFieldValue (
302
+ 'ports' ,
303
+ values . ports . filter ( ( p1 ) => p1 !== p )
304
+ )
305
+ } }
306
+ />
307
+ </ li >
308
+ ) ) }
222
309
</ ul >
223
310
</ div >
224
311
</ SideModal . Section >
@@ -308,25 +395,34 @@ export function CreateFirewallRuleModal({
308
395
onDismiss = { dismiss }
309
396
>
310
397
< 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 : [ ] ,
398
+ initialValues = {
399
+ {
400
+ enabled : false ,
401
+ priority : '' ,
402
+ name : '' ,
403
+ description : '' ,
404
+ action : 'allow' ,
405
+ direction : 'inbound' ,
324
406
325
- // target subform
326
- targets : [ ] ,
327
- targetType : '' ,
328
- targetName : '' ,
329
- } }
407
+ // in the request body, these go in a `filters` object. we probably don't
408
+ // need such nesting here though. not even sure how to do it
409
+ protocols : [ ] ,
410
+
411
+ // port subform
412
+ ports : [ ] ,
413
+ portRange : '' ,
414
+
415
+ // host subform
416
+ hosts : [ ] ,
417
+ hostType : '' ,
418
+ hostValue : '' ,
419
+
420
+ // target subform
421
+ targets : [ ] ,
422
+ targetType : '' ,
423
+ targetValue : '' ,
424
+ } as Values // best way to tell formik this type
425
+ }
330
426
validationSchema = { Yup . object ( {
331
427
priority : Yup . number ( )
332
428
. integer ( )
0 commit comments