Skip to content

Commit d224a81

Browse files
committed
Add DNS settings
1 parent 7e63bd1 commit d224a81

File tree

6 files changed

+486
-207
lines changed

6 files changed

+486
-207
lines changed

src/frontend/components/routing-rules/RuleDialog.tsx

Lines changed: 13 additions & 203 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
import { useState, useEffect } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { 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';
55
import { useCreateIPSet, useUpdateIPSet } from '../../src/hooks/useIPSets';
6-
import { useLists } from '../../src/hooks/useLists';
7-
import { useInterfaces } from '../../src/hooks/useInterfaces';
86
import {
97
ResponsiveDialog,
108
ResponsiveDialogContent,
@@ -25,6 +23,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '..
2523
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
2624
import { Textarea } from '../ui/textarea';
2725
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '../ui/empty';
26+
import { InterfaceSelector } from '../ui/interface-selector';
2827
import { cn } from '../../src/lib/utils';
2928
import 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

Comments
 (0)