8
8
import { createColumnHelper , getCoreRowModel , useReactTable } from '@tanstack/react-table'
9
9
import { useCallback , useMemo , useState } from 'react'
10
10
import { type LoaderFunctionArgs } from 'react-router'
11
- import { match , P } from 'ts-pattern'
11
+ import { match } from 'ts-pattern'
12
12
13
13
import {
14
14
apiQueryClient ,
@@ -88,8 +88,11 @@ const SubnetNameFromId = ({ value }: { value: string }) => {
88
88
return < span className = "text-default" > { subnet . name } </ span >
89
89
}
90
90
91
- const EphemeralIPEmptyCell = ( ) => (
92
- < Tooltip content = "Ephemeral IPs don’t have names or descriptions" placement = "top" >
91
+ const NonFloatingEmptyCell = ( { kind } : { kind : 'snat' | 'ephemeral' } ) => (
92
+ < Tooltip
93
+ content = { `${ kind === 'snat' ? 'SNAT' : 'Ephemeral' } IPs don’t have names or descriptions` }
94
+ placement = "top"
95
+ >
93
96
< div >
94
97
< EmptyCell />
95
98
</ div >
@@ -168,14 +171,30 @@ const updateNicStates = fancifyStates(instanceCan.updateNic.states)
168
171
const ipColHelper = createColumnHelper < ExternalIp > ( )
169
172
const staticIpCols = [
170
173
ipColHelper . accessor ( 'ip' , {
171
- cell : ( info ) => < CopyableIp ip = { info . getValue ( ) } /> ,
174
+ cell : ( info ) => (
175
+ < div className = "flex items-center gap-2" >
176
+ < CopyableIp ip = { info . getValue ( ) } />
177
+ { info . row . original . kind === 'snat' && (
178
+ < Tooltip content = "Outbound traffic uses this IP and port range" placement = "top" >
179
+ { /* div needed for Tooltip */ }
180
+ < div >
181
+ < Badge color = "neutral" >
182
+ { info . row . original . firstPort } –{ info . row . original . lastPort }
183
+ </ Badge >
184
+ </ div >
185
+ </ Tooltip >
186
+ ) }
187
+ </ div >
188
+ ) ,
172
189
} ) ,
173
190
ipColHelper . accessor ( 'kind' , {
174
191
header : ( ) => (
175
192
< >
176
193
Kind
177
194
< TipIcon className = "ml-2" >
178
- Floating IPs can be detached from this instance and attached to another
195
+ Floating IPs can be detached from this instance and attached to another. SNAT IPs
196
+ cannot receive traffic; they are used for outbound traffic when there are no
197
+ ephemeral or floating IPs.
179
198
</ TipIcon >
180
199
</ >
181
200
) ,
@@ -187,15 +206,19 @@ const staticIpCols = [
187
206
} ) ,
188
207
ipColHelper . accessor ( 'name' , {
189
208
cell : ( info ) =>
190
- info . row . original . kind === 'ephemeral' ? < EphemeralIPEmptyCell /> : info . getValue ( ) ,
209
+ info . row . original . kind === 'floating' ? (
210
+ info . getValue ( )
211
+ ) : (
212
+ < NonFloatingEmptyCell kind = { info . row . original . kind } />
213
+ ) ,
191
214
} ) ,
192
215
ipColHelper . accessor ( ( row ) => ( 'description' in row ? row . description : undefined ) , {
193
216
header : 'description' ,
194
217
cell : ( info ) =>
195
- info . row . original . kind === 'ephemeral' ? (
196
- < EphemeralIPEmptyCell />
197
- ) : (
218
+ info . row . original . kind === 'floating' ? (
198
219
< DescriptionCell text = { info . getValue ( ) } />
220
+ ) : (
221
+ < NonFloatingEmptyCell kind = { info . row . original . kind } />
199
222
) ,
200
223
} ) ,
201
224
]
@@ -364,18 +387,38 @@ export default function NetworkingTab() {
364
387
} ,
365
388
}
366
389
367
- const doAction =
368
- externalIp . kind === 'floating'
369
- ? ( ) =>
370
- floatingIpDetach ( {
371
- path : { floatingIp : externalIp . name } ,
372
- query : { project } ,
373
- } )
374
- : ( ) =>
375
- ephemeralIpDetach ( {
376
- path : { instance : instanceName } ,
377
- query : { project } ,
378
- } )
390
+ if ( externalIp . kind === 'snat' ) {
391
+ return [
392
+ copyAction ,
393
+ {
394
+ label : 'Detach' ,
395
+ disabled : "SNAT IPs can't be detached" ,
396
+ onActivate : ( ) => { } ,
397
+ } ,
398
+ ]
399
+ }
400
+
401
+ const doDetach = match ( externalIp )
402
+ . with (
403
+ { kind : 'ephemeral' } ,
404
+ ( ) => ( ) =>
405
+ ephemeralIpDetach ( { path : { instance : instanceName } , query : { project } } )
406
+ )
407
+ . with (
408
+ { kind : 'floating' } ,
409
+ ( { name } ) =>
410
+ ( ) =>
411
+ floatingIpDetach ( { path : { floatingIp : name } , query : { project } } )
412
+ )
413
+ . exhaustive ( )
414
+
415
+ const label = match ( externalIp )
416
+ . with ( { kind : 'ephemeral' } , ( ) => 'this ephemeral IP' )
417
+ . with (
418
+ { kind : 'floating' } ,
419
+ ( { name } ) => < > floating IP < HL > { name } </ HL > </ > // prettier-ignore
420
+ )
421
+ . exhaustive ( )
379
422
380
423
return [
381
424
copyAction ,
@@ -384,21 +427,12 @@ export default function NetworkingTab() {
384
427
onActivate : ( ) =>
385
428
confirmAction ( {
386
429
actionType : 'danger' ,
387
- doAction,
430
+ doAction : doDetach ,
388
431
modalTitle : `Confirm detach ${ externalIp . kind } IP` ,
389
432
modalContent : (
390
433
< p >
391
- Are you sure you want to detach{ ' ' }
392
- { match ( externalIp )
393
- . with ( { kind : P . union ( 'ephemeral' , 'snat' ) } , ( ) => 'this ephemeral IP' )
394
- . with ( { kind : 'floating' } , ( { name } ) => (
395
- < >
396
- floating IP < HL > { name } </ HL >
397
- </ >
398
- ) )
399
- . exhaustive ( ) } { ' ' }
400
- from < HL > { instanceName } </ HL > ? The instance will no longer be reachable at{ ' ' }
401
- < HL > { externalIp . ip } </ HL > .
434
+ Are you sure you want to detach { label } from < HL > { instanceName } </ HL > ? The
435
+ instance will no longer be reachable at < HL > { externalIp . ip } </ HL > .
402
436
</ p >
403
437
) ,
404
438
errorTitle : `Error detaching ${ externalIp . kind } IP` ,
0 commit comments