feat: Copy VLANs wizard + CPU display fix for hardware switches (v0.11.0 Beta)
Add 'Copy VLANs from Another Switch' — a 3-step modal available on the VLANs tab of any switch device. Step 1 selects a source switch and which VLAN IDs to copy (VLAN 1 deselected by default with a management risk warning). Step 2 lets the user assign interfaces on the target device per VLAN: clicking a port chip cycles it through None → Tagged → Untagged → None, with a live summary and bridge picker per VLAN. Conflict detection identifies VLANs already present on the target and offers per-VLAN Skip/Overwrite with the port grid disabling itself when Skip is active. Step 3 shows the full plan before a single apply call executes all operations over one device connection and reports per-VLAN results. Also fix CPU load display on the Devices page for hardware switches that report 0% CPU via ASIC offloading — the previous filter (v > 0) silently dropped all valid zero readings; now filters on field presence instead.
This commit is contained in:
@@ -1,17 +1,23 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Plus, Trash2, AlertCircle, RefreshCw, Pencil, Check, X, Network } from 'lucide-react';
|
||||
import { Plus, Trash2, AlertCircle, RefreshCw, Pencil, Check, X, Network, Copy } from 'lucide-react';
|
||||
import { devicesApi } from '../../services/api';
|
||||
import type { Vlan, SwitchPort } from '../../types';
|
||||
import clsx from 'clsx';
|
||||
import { useCanWrite } from '../../hooks/useCanWrite';
|
||||
import CopyVlanModal from './CopyVlanModal';
|
||||
|
||||
interface PortForm {
|
||||
tagged_ports: string;
|
||||
untagged_ports: string;
|
||||
}
|
||||
|
||||
export default function VlansTab({ deviceId, onGoToPorts }: { deviceId: number; onGoToPorts?: (bridgeName: string) => void }) {
|
||||
export default function VlansTab({ deviceId, deviceName, deviceType, onGoToPorts }: {
|
||||
deviceId: number;
|
||||
deviceName?: string;
|
||||
deviceType?: string;
|
||||
onGoToPorts?: (bridgeName: string) => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const canWrite = useCanWrite();
|
||||
|
||||
@@ -32,6 +38,9 @@ export default function VlansTab({ deviceId, onGoToPorts }: { deviceId: number;
|
||||
(p) => p.type === 'bridge' && p.config_json?.['vlan-filtering'] !== 'yes' && p.config_json?.['vlan-filtering'] !== 'true'
|
||||
);
|
||||
|
||||
// ── Copy VLAN modal ──
|
||||
const [showCopyModal, setShowCopyModal] = useState(false);
|
||||
|
||||
// ── Add form ──
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [addForm, setAddForm] = useState({ vlan_id: '', bridge: '', tagged_ports: '', untagged_ports: '' });
|
||||
@@ -158,6 +167,16 @@ export default function VlansTab({ deviceId, onGoToPorts }: { deviceId: number;
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
|
||||
Refresh
|
||||
</button>
|
||||
{canWrite && deviceType === 'switch' && (
|
||||
<button
|
||||
onClick={() => setShowCopyModal(true)}
|
||||
className="btn-secondary flex items-center gap-2 text-sm"
|
||||
title="Copy VLANs from another switch"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
Copy from Switch
|
||||
</button>
|
||||
)}
|
||||
{canWrite && (
|
||||
<button
|
||||
onClick={() => {
|
||||
@@ -312,6 +331,17 @@ export default function VlansTab({ deviceId, onGoToPorts }: { deviceId: number;
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCopyModal && (
|
||||
<CopyVlanModal
|
||||
deviceId={deviceId}
|
||||
deviceName={deviceName ?? `Device ${deviceId}`}
|
||||
onClose={() => {
|
||||
setShowCopyModal(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['vlans', deviceId] });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* VLANs table */}
|
||||
<div className="card overflow-hidden">
|
||||
{(vlans as Vlan[]).length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user