88import { createColumnHelper , getCoreRowModel , useReactTable } from '@tanstack/react-table'
99import { useCallback , useMemo , useState } from 'react'
1010import { type LoaderFunctionArgs } from 'react-router'
11- import { match , P } from 'ts-pattern'
11+ import { match } from 'ts-pattern'
1212
1313import {
1414 apiQueryClient ,
@@ -88,8 +88,11 @@ const SubnetNameFromId = ({ value }: { value: string }) => {
8888 return < span className = "text-default" > { subnet . name } </ span >
8989}
9090
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+ >
9396 < div >
9497 < EmptyCell />
9598 </ div >
@@ -168,14 +171,30 @@ const updateNicStates = fancifyStates(instanceCan.updateNic.states)
168171const ipColHelper = createColumnHelper < ExternalIp > ( )
169172const staticIpCols = [
170173 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+ ) ,
172189 } ) ,
173190 ipColHelper . accessor ( 'kind' , {
174191 header : ( ) => (
175192 < >
176193 Kind
177194 < 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.
179198 </ TipIcon >
180199 </ >
181200 ) ,
@@ -187,15 +206,19 @@ const staticIpCols = [
187206 } ) ,
188207 ipColHelper . accessor ( 'name' , {
189208 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+ ) ,
191214 } ) ,
192215 ipColHelper . accessor ( ( row ) => ( 'description' in row ? row . description : undefined ) , {
193216 header : 'description' ,
194217 cell : ( info ) =>
195- info . row . original . kind === 'ephemeral' ? (
196- < EphemeralIPEmptyCell />
197- ) : (
218+ info . row . original . kind === 'floating' ? (
198219 < DescriptionCell text = { info . getValue ( ) } />
220+ ) : (
221+ < NonFloatingEmptyCell kind = { info . row . original . kind } />
199222 ) ,
200223 } ) ,
201224]
@@ -364,18 +387,38 @@ export default function NetworkingTab() {
364387 } ,
365388 }
366389
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 ( )
379422
380423 return [
381424 copyAction ,
@@ -384,21 +427,12 @@ export default function NetworkingTab() {
384427 onActivate : ( ) =>
385428 confirmAction ( {
386429 actionType : 'danger' ,
387- doAction,
430+ doAction : doDetach ,
388431 modalTitle : `Confirm detach ${ externalIp . kind } IP` ,
389432 modalContent : (
390433 < 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 > .
402436 </ p >
403437 ) ,
404438 errorTitle : `Error detaching ${ externalIp . kind } IP` ,
0 commit comments