11import { useState , useEffect } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import { toast } from 'sonner' ;
4- import { Loader2 , X , Plus , Check , ChevronsUpDown , ChevronUp , ChevronDown , Unplug , ListPlus , Network , File , Globe , List } from 'lucide-react' ;
4+ import { Loader2 , X , Plus , Check , ChevronsUpDown , ListPlus , File , Globe , List } from 'lucide-react' ;
55import { useCreateIPSet , useUpdateIPSet } from '../../src/hooks/useIPSets' ;
6- import { useLists } from '../../src/hooks/useLists' ;
7- import { useInterfaces } from '../../src/hooks/useInterfaces' ;
86import {
97 ResponsiveDialog ,
108 ResponsiveDialogContent ,
@@ -25,6 +23,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '..
2523import { Select , SelectContent , SelectItem , SelectTrigger , SelectValue } from '../ui/select' ;
2624import { Textarea } from '../ui/textarea' ;
2725import { Empty , EmptyHeader , EmptyMedia , EmptyTitle , EmptyDescription } from '../ui/empty' ;
26+ import { InterfaceSelector } from '../ui/interface-selector' ;
2827import { cn } from '../../src/lib/utils' ;
2928import type { IPSetConfig , CreateIPSetRequest , IPTablesRule , ListInfo } from '../../src/api/client' ;
3029
@@ -64,12 +63,7 @@ export function RuleDialog({ ipset, open, onOpenChange, availableLists }: RuleDi
6463 const isEditMode = ! ! ipset ;
6564 const createIPSet = useCreateIPSet ( ) ;
6665 const updateIPSet = useUpdateIPSet ( ) ;
67- const [ interfacesOpen , setInterfacesOpen ] = useState ( false ) ;
6866 const [ listsOpen , setListsOpen ] = useState ( false ) ;
69- const [ customInterface , setCustomInterface ] = useState ( '' ) ;
70-
71- // Fetch interfaces while dialog is open
72- const { data : interfacesData } = useInterfaces ( open ) ;
7367
7468 const [ formData , setFormData ] = useState < CreateIPSetRequest > ( {
7569 ipset_name : '' ,
@@ -122,7 +116,6 @@ export function RuleDialog({ ipset, open, onOpenChange, availableLists }: RuleDi
122116 } ,
123117 iptables_rule : [ { ...DEFAULT_IPTABLES_RULE } ] ,
124118 } ) ;
125- setCustomInterface ( '' ) ;
126119 }
127120 } , [ ipset , open ] ) ;
128121
@@ -168,74 +161,6 @@ export function RuleDialog({ ipset, open, onOpenChange, availableLists }: RuleDi
168161 } ) ;
169162 } ;
170163
171- const addInterface = ( iface : string ) => {
172- if ( formData . routing && ! formData . routing . interfaces . includes ( iface ) ) {
173- setFormData ( {
174- ...formData ,
175- routing : {
176- ...formData . routing ,
177- interfaces : [ ...formData . routing . interfaces , iface ] ,
178- } ,
179- } ) ;
180- }
181- setInterfacesOpen ( false ) ;
182- } ;
183-
184- const addCustomInterface = ( ) => {
185- const trimmedInterface = customInterface . trim ( ) ;
186- if ( trimmedInterface && formData . routing && ! formData . routing . interfaces . includes ( trimmedInterface ) ) {
187- setFormData ( {
188- ...formData ,
189- routing : {
190- ...formData . routing ,
191- interfaces : [ ...formData . routing . interfaces , trimmedInterface ] ,
192- } ,
193- } ) ;
194- setCustomInterface ( '' ) ;
195- setInterfacesOpen ( false ) ;
196- }
197- } ;
198-
199- const removeInterface = ( iface : string ) => {
200- if ( formData . routing ) {
201- setFormData ( {
202- ...formData ,
203- routing : {
204- ...formData . routing ,
205- interfaces : formData . routing . interfaces . filter ( ( i ) => i !== iface ) ,
206- } ,
207- } ) ;
208- }
209- } ;
210-
211- const moveInterfaceUp = ( index : number ) => {
212- if ( index > 0 && formData . routing ) {
213- const newInterfaces = [ ...formData . routing . interfaces ] ;
214- [ newInterfaces [ index - 1 ] , newInterfaces [ index ] ] = [ newInterfaces [ index ] , newInterfaces [ index - 1 ] ] ;
215- setFormData ( {
216- ...formData ,
217- routing : {
218- ...formData . routing ,
219- interfaces : newInterfaces ,
220- } ,
221- } ) ;
222- }
223- } ;
224-
225- const moveInterfaceDown = ( index : number ) => {
226- if ( formData . routing && index < formData . routing . interfaces . length - 1 ) {
227- const newInterfaces = [ ...formData . routing . interfaces ] ;
228- [ newInterfaces [ index ] , newInterfaces [ index + 1 ] ] = [ newInterfaces [ index + 1 ] , newInterfaces [ index ] ] ;
229- setFormData ( {
230- ...formData ,
231- routing : {
232- ...formData . routing ,
233- interfaces : newInterfaces ,
234- } ,
235- } ) ;
236- }
237- } ;
238-
239164 // IPTables Rules functions
240165 const addIPTablesRule = ( ) => {
241166 setFormData ( {
@@ -280,9 +205,6 @@ export function RuleDialog({ ipset, open, onOpenChange, availableLists }: RuleDi
280205 updateIPTablesRule ( ruleIndex , 'rule' , newRuleString . split ( ' ' ) ) ;
281206 } ;
282207
283- // Get interface options (keep full objects for UP/DOWN state)
284- const interfaceOptions = interfacesData || [ ] ;
285-
286208 const isPending = isEditMode ? updateIPSet . isPending : createIPSet . isPending ;
287209
288210 return (
@@ -440,129 +362,17 @@ export function RuleDialog({ ipset, open, onOpenChange, availableLists }: RuleDi
440362 { t ( 'routingRules.dialog.interfacesDescription' ) }
441363 </ FieldDescription >
442364
443- < Popover open = { interfacesOpen } onOpenChange = { setInterfacesOpen } >
444- < PopoverTrigger asChild >
445- < Button
446- variant = "outline"
447- role = "combobox"
448- aria-expanded = { interfacesOpen }
449- className = "w-full justify-between"
450- >
451- < Plus className = "mr-2 h-4 w-4" />
452- { t ( 'routingRules.dialog.addInterface' ) }
453- < ChevronsUpDown className = "ml-2 h-4 w-4 shrink-0 opacity-50" />
454- </ Button >
455- </ PopoverTrigger >
456- < PopoverContent className = "w-full p-0" >
457- < Command >
458- < CommandInput
459- placeholder = { t ( 'routingRules.dialog.searchInterfaces' ) }
460- value = { customInterface }
461- onValueChange = { setCustomInterface }
462- />
463- < CommandList className = "max-h-[300px] overflow-y-auto" >
464- { customInterface && ! interfaceOptions . some ( i => i . name === customInterface ) && (
465- < CommandItem
466- onSelect = { ( ) => addCustomInterface ( ) }
467- className = "text-sm"
468- >
469- < Plus className = "mr-2 h-4 w-4" />
470- Add custom: < span className = "font-mono ml-1" > { customInterface } </ span >
471- </ CommandItem >
472- ) }
473- { interfaceOptions . length === 0 && ! customInterface && (
474- < CommandEmpty > { t ( 'routingRules.dialog.noInterfaces' ) } </ CommandEmpty >
475- ) }
476- < CommandGroup >
477- { interfaceOptions . map ( ( iface ) => (
478- < CommandItem
479- key = { iface . name }
480- value = { iface . name }
481- onSelect = { ( ) => addInterface ( iface . name ) }
482- disabled = { formData . routing ?. interfaces . includes ( iface . name ) }
483- >
484- { iface . is_up ? (
485- < Unplug className = "mr-2 h-4 w-4 text-green-600" />
486- ) : (
487- < Unplug className = "mr-2 h-4 w-4 text-red-600" />
488- ) }
489- < Check
490- className = { cn (
491- "mr-2 h-4 w-4" ,
492- formData . routing ?. interfaces . includes ( iface . name ) ? "opacity-100" : "opacity-0"
493- ) }
494- />
495- { iface . name }
496- </ CommandItem >
497- ) ) }
498- </ CommandGroup >
499- </ CommandList >
500- </ Command >
501- </ PopoverContent >
502- </ Popover >
503-
504- { formData . routing . interfaces . length > 0 ? (
505- < div className = "mt-2 space-y-1 border rounded-md p-2" >
506- { formData . routing . interfaces . map ( ( iface , index ) => {
507- const interfaceInfo = interfaceOptions . find ( i => i . name === iface ) ;
508- return (
509- < div key = { `${ iface } -${ index } ` } className = "flex items-center justify-between text-sm py-1 px-2 hover:bg-accent rounded" >
510- < div className = "flex items-center gap-2" >
511- < span className = "text-xs text-muted-foreground" > { index + 1 } .</ span >
512- { interfaceInfo ?. is_up ? (
513- < Unplug className = "h-4 w-4 text-green-600" />
514- ) : interfaceInfo ? (
515- < Unplug className = "h-4 w-4 text-red-600" />
516- ) : null }
517- < span className = "font-mono" > { iface } </ span >
518- </ div >
519- < div className = "flex items-center gap-1" >
520- < Button
521- type = "button"
522- variant = "ghost"
523- size = "sm"
524- className = "h-6 w-6 p-0"
525- onClick = { ( ) => moveInterfaceUp ( index ) }
526- disabled = { index === 0 }
527- >
528- < ChevronUp className = "h-3 w-3" />
529- </ Button >
530- < Button
531- type = "button"
532- variant = "ghost"
533- size = "sm"
534- className = "h-6 w-6 p-0"
535- onClick = { ( ) => moveInterfaceDown ( index ) }
536- disabled = { index === formData . routing ! . interfaces . length - 1 }
537- >
538- < ChevronDown className = "h-3 w-3" />
539- </ Button >
540- < Button
541- type = "button"
542- variant = "ghost"
543- size = "sm"
544- className = "h-6 w-6 p-0"
545- onClick = { ( ) => removeInterface ( iface ) }
546- >
547- < X className = "h-3 w-3" />
548- </ Button >
549- </ div >
550- </ div >
551- ) } ) }
552- </ div >
553- ) : (
554- < Empty className = "mt-2 border" >
555- < EmptyHeader >
556- < EmptyMedia variant = "icon" >
557- < Network className = "h-5 w-5" />
558- </ EmptyMedia >
559- < EmptyTitle className = "text-base" > { t ( 'routingRules.dialog.emptyInterfaces.title' ) } </ EmptyTitle >
560- < EmptyDescription >
561- { t ( 'routingRules.dialog.emptyInterfaces.description' ) }
562- </ EmptyDescription >
563- </ EmptyHeader >
564- </ Empty >
565- ) }
365+ < InterfaceSelector
366+ value = { formData . routing . interfaces }
367+ onChange = { ( interfaces ) => setFormData ( {
368+ ...formData ,
369+ routing : {
370+ ...formData . routing ! ,
371+ interfaces,
372+ } ,
373+ } ) }
374+ allowReorder = { true }
375+ />
566376 </ Field >
567377
568378 < Field >
0 commit comments