@@ -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,39 @@ 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
+ // TODO: show error instead of ignoring click
117
+ if (
118
+ values . targetType &&
119
+ values . targetValue && // TODO: validate
120
+ ! values . targets . some (
121
+ ( t ) =>
122
+ t . value === values . targetValue &&
123
+ t . type === values . targetType
124
+ )
125
+ ) {
96
126
setFieldValue ( 'targets' , [
97
- ...targets ,
98
- { type : targetType , name : targetName } ,
127
+ ...values . targets ,
128
+ { type : values . targetType , value : values . targetValue } ,
99
129
] )
130
+ setFieldValue ( 'targetValue' , '' )
131
+ // TODO: clear dropdown too?
100
132
}
101
133
} }
102
134
>
@@ -113,17 +145,20 @@ const CommonForm = ({ id, error }: FormProps) => {
113
145
</ Table . HeaderRow >
114
146
</ Table . Header >
115
147
< Table . Body >
116
- { targets . map ( ( t ) => (
117
- < Table . Row key = { t . name } >
148
+ { values . targets . map ( ( t ) => (
149
+ < Table . Row key = { `${ t . type } |${ t . value } ` } >
150
+ { /* TODO: should be the pretty type label, not the type key */ }
118
151
< Table . Cell > { t . type } </ Table . Cell >
119
- < Table . Cell > { t . name } </ Table . Cell >
152
+ < Table . Cell > { t . value } </ Table . Cell >
120
153
< Table . Cell >
121
154
< Delete10Icon
122
155
className = "cursor-pointer"
123
156
onClick = { ( ) => {
124
157
setFieldValue (
125
158
'targets' ,
126
- targets . filter ( ( t1 ) => t1 . name !== t . name )
159
+ values . targets . filter (
160
+ ( t1 ) => t1 . value !== t . value || t1 . type !== t . type
161
+ )
127
162
)
128
163
} }
129
164
/>
@@ -144,28 +179,53 @@ const CommonForm = ({ id, error }: FormProps) => {
144
179
{ value : 'ip' , label : 'IP' } ,
145
180
{ value : 'internet_gateway' , label : 'Internet Gateway' } ,
146
181
] }
182
+ onChange = { ( item ) => {
183
+ setFieldValue ( 'hostType' , item ?. value )
184
+ } }
147
185
/>
148
186
< div className = "space-y-0.5" >
149
187
{ /* For everything but IP this is a name, but for IP it's an IP.
150
188
So we should probably have the label on this field change when the
151
189
host type changes. Also need to confirm that it's just an IP and
152
190
not a block. */ }
153
- < FieldTitle htmlFor = "host-filter-value " > Value</ FieldTitle >
154
- < TextFieldHint id = "host-filter-value -hint" >
191
+ < FieldTitle htmlFor = "hostValue " > Value</ FieldTitle >
192
+ < TextFieldHint id = "hostValue -hint" >
155
193
For IP, an address. For the rest, a name. [TODO: copy]
156
194
</ TextFieldHint >
157
195
< TextField
158
- id = "host-filter-value "
159
- name = "host-filter-value "
160
- aria-describedby = "host-filter-value -hint"
196
+ id = "hostValue "
197
+ name = "hostValue "
198
+ aria-describedby = "hostValue -hint"
161
199
/>
162
200
</ div >
163
201
164
202
< div className = "flex justify-end" >
165
203
< Button variant = "ghost" className = "mr-2.5" >
166
204
Clear
167
205
</ Button >
168
- < Button variant = "dim" > Add host filter</ Button >
206
+ < Button
207
+ variant = "dim"
208
+ onClick = { ( ) => {
209
+ // TODO: show error instead of ignoring click
210
+ if (
211
+ values . hostType &&
212
+ values . hostValue && // TODO: validate
213
+ ! values . hosts . some (
214
+ ( t ) =>
215
+ t . value === values . hostValue || t . type === values . hostType
216
+ )
217
+ ) {
218
+ setFieldValue ( 'hosts' , [
219
+ ...values . hosts ,
220
+ { type : values . hostType , value : values . hostValue } ,
221
+ ] )
222
+ setFieldValue ( 'hostValue' , '' )
223
+ // TODO: clear dropdown too?
224
+ }
225
+ } }
226
+ >
227
+ Add host filter
228
+ </ Button >
169
229
</ div >
170
230
171
231
< Table className = "w-full" >
@@ -177,13 +237,26 @@ const CommonForm = ({ id, error }: FormProps) => {
177
237
</ Table . HeaderRow >
178
238
</ Table . Header >
179
239
< 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 >
240
+ { values . hosts . map ( ( h ) => (
241
+ < Table . Row key = { `${ h . type } |${ h . value } ` } >
242
+ { /* TODO: should be the pretty type label, not the type key */ }
243
+ < Table . Cell > { h . type } </ Table . Cell >
244
+ < Table . Cell > { h . value } </ Table . Cell >
245
+ < Table . Cell >
246
+ < Delete10Icon
247
+ className = "cursor-pointer"
248
+ onClick = { ( ) => {
249
+ setFieldValue (
250
+ 'hosts' ,
251
+ values . hosts . filter (
252
+ ( h1 ) => h1 . value !== h . value && h1 . type !== h . type
253
+ )
254
+ )
255
+ } }
256
+ />
257
+ </ Table . Cell >
258
+ </ Table . Row >
259
+ ) ) }
187
260
</ Table . Body >
188
261
</ Table >
189
262
</ SideModal . Section >
@@ -204,21 +277,34 @@ const CommonForm = ({ id, error }: FormProps) => {
204
277
< Button variant = "ghost" className = "mr-2.5" >
205
278
Clear
206
279
</ Button >
207
- < Button variant = "dim" > Add port filter</ Button >
280
+ < Button
281
+ variant = "dim"
282
+ onClick = { ( ) => {
283
+ const portRange = values . portRange . trim ( )
284
+ // TODO: show error instead of ignoring the click
285
+ if ( ! parsePortRange ( portRange ) ) return
286
+ setFieldValue ( 'ports' , [ ...values . ports , portRange ] )
287
+ setFieldValue ( 'portRange' , '' )
288
+ } }
289
+ >
290
+ Add port filter
291
+ </ Button >
208
292
</ div >
209
293
< 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 >
294
+ { values . ports . map ( ( p ) => (
295
+ < li key = { p } >
296
+ { p }
297
+ < Delete10Icon
298
+ className = "cursor-pointer ml-2"
299
+ onClick = { ( ) => {
300
+ setFieldValue (
301
+ 'ports' ,
302
+ values . ports . filter ( ( p1 ) => p1 !== p )
303
+ )
304
+ } }
305
+ />
306
+ </ li >
307
+ ) ) }
222
308
</ ul >
223
309
</ div >
224
310
</ SideModal . Section >
@@ -308,25 +394,34 @@ export function CreateFirewallRuleModal({
308
394
onDismiss = { dismiss }
309
395
>
310
396
< 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 : [ ] ,
397
+ initialValues = {
398
+ {
399
+ enabled : false ,
400
+ priority : '' ,
401
+ name : '' ,
402
+ description : '' ,
403
+ action : 'allow' ,
404
+ direction : 'inbound' ,
324
405
325
- // target subform
326
- targets : [ ] ,
327
- targetType : '' ,
328
- targetName : '' ,
329
- } }
406
+ // in the request body, these go in a `filters` object. we probably don't
407
+ // need such nesting here though. not even sure how to do it
408
+ protocols : [ ] ,
409
+
410
+ // port subform
411
+ ports : [ ] ,
412
+ portRange : '' ,
413
+
414
+ // host subform
415
+ hosts : [ ] ,
416
+ hostType : '' ,
417
+ hostValue : '' ,
418
+
419
+ // target subform
420
+ targets : [ ] ,
421
+ targetType : '' ,
422
+ targetValue : '' ,
423
+ } as Values // best way to tell formik this type
424
+ }
330
425
validationSchema = { Yup . object ( {
331
426
priority : Yup . number ( )
332
427
. integer ( )
0 commit comments