Ver código fonte

fix(ui): hide DHCP button on unsupported interfaces and fix table sort mutation loop breaking edits

Proxmox Router 6 dias atrás
pai
commit
1b5a6dfd01
1 arquivos alterados com 83 adições e 6 exclusões
  1. 83 6
      internal/web/static/app.js

+ 83 - 6
internal/web/static/app.js

@@ -770,6 +770,36 @@ function supportsVLAN(iface) {
   return true;
 }
 
+// DHCP Client Support Checker: Only broadcast-capable layer-2 interfaces (Ethernet, veth, VLAN, bridge, bond)
+function supportsDHCPClient(iface) {
+  if (!iface) return false;
+
+  const name = (iface.name || '').toLowerCase();
+  const type = (iface.type || '').toLowerCase();
+  const role = (iface.role || '').toLowerCase();
+
+  // Exclude loopback
+  if (name === 'lo' || type === 'loopback' || role === 'loopback') return false;
+
+  // Exclude WireGuard (Layer 3 point-to-point tunnel, no broadcast/DHCP)
+  if (type === 'wireguard' || name.startsWith('wg')) return false;
+
+  // Exclude PPP / PPPoE / L2TP / PPTP tunnels
+  if (type === 'ppp' || name.startsWith('ppp') || name.startsWith('l2tp') || name.startsWith('pptp')) return false;
+
+  // Exclude other Layer 3 tunnels and dummy devices
+  if (type === 'dummy' || type === 'gre' || type === 'sit' || type === 'tun' || type === 'tap' ||
+      name.startsWith('gre') || name.startsWith('sit') || name.startsWith('ipsec') || name.startsWith('xfrm') || name.startsWith('dummy')) {
+    return false;
+  }
+
+  // Exclude bridge member/slave ports (bridge itself handles IP / DHCP)
+  const isBridgePort = (state.bridgePorts || []).some(bp => bp.interface === iface.name);
+  if (isBridgePort) return false;
+
+  return true;
+}
+
 // Expand / Collapse Handlers
 function toggleExpand(parentName) {
   const childVlans = (state.vlans || []).filter(v => v.parent_name === parentName || v.name.startsWith(parentName + '.'));
@@ -1111,7 +1141,8 @@ function renderInterfacesList() {
 
   const sig = `${state.ifaceFilter}|${query}|` + filteredParents.map(p => {
     const cVlans = allVlans.filter(v => v.parent_name === p.name || v.name.startsWith(p.name + '.'));
-    return `${p.name}:${p.state}:${p.role||'LAN'}:${(p.ipv4||[]).join(',')}:${p.mtu}:${cVlans.length}:${state.expandedParents.has(p.name)}`;
+    const dhcp = (state.dhcpClients || []).find(dc => dc.interface === p.name);
+    return `${p.name}:${p.state}:${p.role||'LAN'}:${(p.ipv4||[]).join(',')}:${p.mtu}:${cVlans.length}:${state.expandedParents.has(p.name)}:${dhcp?.status || ''}`;
   }).join(';');
 
   if (tbody.dataset.sig === sig) {
@@ -1209,6 +1240,9 @@ function renderInterfacesList() {
           if (c) {
             return `<button onclick="navigate('dhcp-client')" title="Manage DHCP Client (${c.status})" class="text-xs bg-amber-500/10 hover:bg-amber-500/20 text-amber-400 font-medium px-2.5 py-1 rounded-lg border border-amber-500/30 transition">⚡ DHCP</button>`;
           }
+          if (!supportsDHCPClient(parent)) {
+            return '';
+          }
           return `<button onclick="openDHCPClientModalForIface('${parent.name}')" title="Add DHCP Client on ${parent.name}" class="text-xs bg-slate-800/80 hover:bg-slate-700 text-slate-300 hover:text-amber-400 font-medium px-2 py-1 rounded-lg border border-slate-700 transition">+ DHCP</button>`;
         })()}
         ${canHaveVlan ? `
@@ -1219,7 +1253,7 @@ function renderInterfacesList() {
         ` : ''}
         <button onclick="openIfaceModal('${parent.name}')"
                 class="text-xs bg-slate-800 hover:bg-slate-700 text-white font-medium px-2.5 py-1 rounded-lg border border-slate-700 transition">
-          Configure
+          Edit
         </button>
       </td>
     `;
@@ -1324,6 +1358,9 @@ function renderInterfacesList() {
                             if (c) {
                               return `<button onclick="navigate('dhcp-client')" class="text-xs text-amber-400 hover:text-amber-300 px-2 py-0.5 rounded bg-amber-950/40 border border-amber-800/50">⚡ DHCP</button>`;
                             }
+                            if (!supportsDHCPClient(v)) {
+                              return '';
+                            }
                             return `<button onclick="openDHCPClientModalForIface('${v.name}')" class="text-xs text-slate-400 hover:text-amber-400 px-1.5 py-0.5 rounded border border-slate-700">+ DHCP</button>`;
                           })()}
                           <button onclick="openIfaceModal('${v.name}')" class="text-xs bg-slate-800 hover:bg-slate-700 text-slate-200 px-2 py-0.5 rounded border border-slate-700">Edit</button>
@@ -13159,7 +13196,7 @@ function openDHCPClientModal(clientId = null) {
 
     // Group 1: Physical & Bridge Interfaces
     opts += '<optgroup label="Physical & Bridge Interfaces">';
-    allIfaces.forEach(i => {
+    allIfaces.filter(i => supportsDHCPClient(i)).forEach(i => {
       const isConfigured = usedIfaces.has(i.name) ? ' (Configured)' : '';
       opts += `<option value="${escapeHtml(i.name)}">${escapeHtml(i.name)} [${escapeHtml(i.role || i.type)}]${isConfigured}</option>`;
     });
@@ -14250,8 +14287,9 @@ function setupSortableTable(table) {
         } else {
           tagOriginalRowOrder(tbody);
         }
-      }, 10);
+      }, 50);
     });
+    tbody._sortObserver = observer;
     observer.observe(tbody, { childList: true });
   }
 
@@ -14346,12 +14384,46 @@ function applyTableSort(table, colIdx, dir, userInitiated) {
     });
   }
 
+  // Check if rows are already in correct order to avoid modifying DOM
+  let alreadyOrdered = true;
+  let childIdx = 0;
+  for (const g of groups) {
+    if (tbody.children[childIdx] !== g.mainRow) {
+      alreadyOrdered = false;
+      break;
+    }
+    childIdx++;
+    for (const sub of g.subRows) {
+      if (tbody.children[childIdx] !== sub) {
+        alreadyOrdered = false;
+        break;
+      }
+      childIdx++;
+    }
+  }
+
+  if (alreadyOrdered) {
+    tbody._isSorting = false;
+    return;
+  }
+
+  // Disconnect observer while re-ordering to prevent recursion and thrashing
+  if (tbody._sortObserver) {
+    tbody._sortObserver.disconnect();
+  }
+
   // Re-append rows in sorted order
   groups.forEach(g => {
     tbody.appendChild(g.mainRow);
     g.subRows.forEach(sub => tbody.appendChild(sub));
   });
 
+  // Drain any queued mutations and re-connect observer
+  if (tbody._sortObserver) {
+    tbody._sortObserver.takeRecords();
+    tbody._sortObserver.observe(tbody, { childList: true });
+  }
+
   tbody._isSorting = false;
 }
 
@@ -14694,12 +14766,16 @@ async function toggleVRRPState(id, disabled) {
 
 // Global observer for dynamically injected tables (e.g. inside modals or drawers)
 if (typeof MutationObserver !== 'undefined' && typeof document !== 'undefined') {
+  let docSortDebounce = null;
   const docTableObserver = new MutationObserver((mutations) => {
     let needsInit = false;
     for (const m of mutations) {
       for (const node of m.addedNodes) {
         if (node.nodeType === 1) {
-          if (node.tagName === 'TABLE' || (node.querySelector && node.querySelector('table'))) {
+          if (node.tagName === 'TABLE' && !node.closest('tbody, thead')) {
+            needsInit = true;
+            break;
+          } else if (node.querySelector && !node.closest('table') && node.querySelector('table')) {
             needsInit = true;
             break;
           }
@@ -14708,7 +14784,8 @@ if (typeof MutationObserver !== 'undefined' && typeof document !== 'undefined')
       if (needsInit) break;
     }
     if (needsInit) {
-      initTableSorting();
+      clearTimeout(docSortDebounce);
+      docSortDebounce = setTimeout(initTableSorting, 100);
     }
   });
   docTableObserver.observe(document.body || document.documentElement, { childList: true, subtree: true });