|
@@ -288,6 +288,11 @@ function navigate(tabName) {
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ if (tabName === 'interfaces') {
|
|
|
|
|
+ loadRoles();
|
|
|
|
|
+ if (state.activeIfaceSubTab === 'roles') switchIfaceSubTab('roles');
|
|
|
|
|
+ else switchIfaceSubTab('tree');
|
|
|
|
|
+ }
|
|
|
if (tabName === 'bridge') loadBridge();
|
|
if (tabName === 'bridge') loadBridge();
|
|
|
if (tabName === 'firewall') loadFirewall();
|
|
if (tabName === 'firewall') loadFirewall();
|
|
|
if (tabName === 'routes') loadRoutes();
|
|
if (tabName === 'routes') loadRoutes();
|
|
@@ -325,7 +330,7 @@ async function fetchData(isManual = false) {
|
|
|
if (isManual && spinner) spinner.classList.add('animate-spin');
|
|
if (isManual && spinner) spinner.classList.add('animate-spin');
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const [statusRes, ifacesRes, vlansRes, vpnRes, fwRes, dynRes, brSummaryRes, dhcpSumRes, dhcpClientsRes, vrrpSumRes] = await Promise.all([
|
|
|
|
|
|
|
+ const [statusRes, ifacesRes, vlansRes, vpnRes, fwRes, dynRes, brSummaryRes, dhcpSumRes, dhcpClientsRes, vrrpSumRes, rolesRes] = await Promise.all([
|
|
|
fetch(`${API_BASE}/system/status`),
|
|
fetch(`${API_BASE}/system/status`),
|
|
|
fetch(`${API_BASE}/interfaces`),
|
|
fetch(`${API_BASE}/interfaces`),
|
|
|
fetch(`${API_BASE}/vlans`),
|
|
fetch(`${API_BASE}/vlans`),
|
|
@@ -335,9 +340,16 @@ async function fetchData(isManual = false) {
|
|
|
fetch(`${API_BASE}/bridge/summary`),
|
|
fetch(`${API_BASE}/bridge/summary`),
|
|
|
fetch(`${API_BASE}/dhcp/clients/summary`),
|
|
fetch(`${API_BASE}/dhcp/clients/summary`),
|
|
|
fetch(`${API_BASE}/dhcp/clients`),
|
|
fetch(`${API_BASE}/dhcp/clients`),
|
|
|
- fetch(`${API_BASE}/vrrp/summary`).catch(() => null)
|
|
|
|
|
|
|
+ fetch(`${API_BASE}/vrrp/summary`).catch(() => null),
|
|
|
|
|
+ fetch(`${API_BASE}/roles`).catch(() => null)
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
|
|
+ if (rolesRes && rolesRes.ok) {
|
|
|
|
|
+ state.roles = await rolesRes.json();
|
|
|
|
|
+ const countEl = document.getElementById('badge-roles-count');
|
|
|
|
|
+ if (countEl) countEl.innerText = (state.roles || []).length;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (vrrpSumRes && vrrpSumRes.ok) {
|
|
if (vrrpSumRes && vrrpSumRes.ok) {
|
|
|
state.vrrpSummary = await vrrpSumRes.json();
|
|
state.vrrpSummary = await vrrpSumRes.json();
|
|
|
renderVRRPSummary();
|
|
renderVRRPSummary();
|
|
@@ -387,7 +399,10 @@ async function fetchData(isManual = false) {
|
|
|
updateBadges();
|
|
updateBadges();
|
|
|
|
|
|
|
|
if (state.activeTab === 'dashboard') renderDashboard();
|
|
if (state.activeTab === 'dashboard') renderDashboard();
|
|
|
- if (state.activeTab === 'interfaces') renderInterfacesList();
|
|
|
|
|
|
|
+ if (state.activeTab === 'interfaces') {
|
|
|
|
|
+ if (state.activeIfaceSubTab === 'roles') renderRolesTable();
|
|
|
|
|
+ else renderInterfacesList();
|
|
|
|
|
+ }
|
|
|
if (state.activeTab === 'vlans') renderVlansList();
|
|
if (state.activeTab === 'vlans') renderVlansList();
|
|
|
if (state.activeTab === 'firewall') loadFirewall(false);
|
|
if (state.activeTab === 'firewall') loadFirewall(false);
|
|
|
if (state.activeTab === 'routes') loadRoutes(false);
|
|
if (state.activeTab === 'routes') loadRoutes(false);
|
|
@@ -694,7 +709,7 @@ function renderDashboard() {
|
|
|
<span>${i.name}</span>
|
|
<span>${i.name}</span>
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 text-slate-300 font-sans">
|
|
<td class="py-2.5 text-slate-300 font-sans">
|
|
|
- <span class="px-2 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 font-medium">${i.role || i.type || 'Ethernet'}</span>
|
|
|
|
|
|
|
+ ${getRoleBadge(i.role || 'LAN')}
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 text-slate-400 text-[11px] font-mono">${i.mac || '-'}</td>
|
|
<td class="py-2.5 text-slate-400 text-[11px] font-mono">${i.mac || '-'}</td>
|
|
|
<td class="py-2.5 text-slate-200 font-semibold">${i.ipv4 && i.ipv4.length > 0 ? i.ipv4.join(', ') : '<span class="text-slate-600 font-normal">Unassigned</span>'}</td>
|
|
<td class="py-2.5 text-slate-200 font-semibold">${i.ipv4 && i.ipv4.length > 0 ? i.ipv4.join(', ') : '<span class="text-slate-600 font-normal">Unassigned</span>'}</td>
|
|
@@ -783,6 +798,256 @@ function expandAllIfaces(expand) {
|
|
|
renderInterfacesList();
|
|
renderInterfacesList();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// =========================================================================
|
|
|
|
|
+// INTERFACE ROLE TAGS MANAGEMENT
|
|
|
|
|
+// =========================================================================
|
|
|
|
|
+
|
|
|
|
|
+const ROLE_COLORS = {
|
|
|
|
|
+ amber: { bg: 'bg-amber-500/20', text: 'text-amber-300', border: 'border-amber-500/30' },
|
|
|
|
|
+ blue: { bg: 'bg-blue-500/20', text: 'text-blue-300', border: 'border-blue-500/30' },
|
|
|
|
|
+ cyan: { bg: 'bg-cyan-500/20', text: 'text-cyan-300', border: 'border-cyan-500/30' },
|
|
|
|
|
+ emerald: { bg: 'bg-emerald-500/20', text: 'text-emerald-300', border: 'border-emerald-500/30' },
|
|
|
|
|
+ purple: { bg: 'bg-purple-500/20', text: 'text-purple-300', border: 'border-purple-500/30' },
|
|
|
|
|
+ rose: { bg: 'bg-rose-500/20', text: 'text-rose-300', border: 'border-rose-500/30' },
|
|
|
|
|
+ indigo: { bg: 'bg-indigo-500/20', text: 'text-indigo-300', border: 'border-indigo-500/30' },
|
|
|
|
|
+ sky: { bg: 'bg-sky-500/20', text: 'text-sky-300', border: 'border-sky-500/30' },
|
|
|
|
|
+ slate: { bg: 'bg-slate-800', text: 'text-slate-300', border: 'border-slate-700' }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+function getRoleBadge(roleName) {
|
|
|
|
|
+ const name = roleName || 'LAN';
|
|
|
|
|
+ const role = (state.roles || []).find(r => r.name.toLowerCase() === name.toLowerCase() || r.id.toLowerCase() === name.toLowerCase());
|
|
|
|
|
+ const colorKey = (role?.color || 'blue').toLowerCase();
|
|
|
|
|
+ const theme = ROLE_COLORS[colorKey] || ROLE_COLORS.blue;
|
|
|
|
|
+ return `<span class="px-2 py-0.5 rounded text-[10px] font-semibold ${theme.bg} ${theme.text} border ${theme.border}" title="Role Tag: ${escapeHtml(name)}">${escapeHtml(name)}</span>`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function switchIfaceSubTab(subTab) {
|
|
|
|
|
+ state.activeIfaceSubTab = subTab;
|
|
|
|
|
+ const treeTab = document.getElementById('ifaces-subtab-tree');
|
|
|
|
|
+ const rolesTab = document.getElementById('ifaces-subtab-roles');
|
|
|
|
|
+ const btnTree = document.getElementById('ifaces-subtab-btn-tree');
|
|
|
|
|
+ const btnRoles = document.getElementById('ifaces-subtab-btn-roles');
|
|
|
|
|
+
|
|
|
|
|
+ if (subTab === 'roles') {
|
|
|
|
|
+ treeTab?.classList.add('hidden');
|
|
|
|
|
+ rolesTab?.classList.remove('hidden');
|
|
|
|
|
+ if (btnRoles) {
|
|
|
|
|
+ btnRoles.className = 'px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-brand-500/10 text-brand-400 border border-brand-500/20 transition flex items-center gap-1.5';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (btnTree) {
|
|
|
|
|
+ btnTree.className = 'px-3.5 py-1.5 rounded-lg text-xs font-medium text-slate-400 hover:text-white hover:bg-slate-800 transition flex items-center gap-1.5';
|
|
|
|
|
+ }
|
|
|
|
|
+ loadRoles();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ rolesTab?.classList.add('hidden');
|
|
|
|
|
+ treeTab?.classList.remove('hidden');
|
|
|
|
|
+ if (btnTree) {
|
|
|
|
|
+ btnTree.className = 'px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-brand-500/10 text-brand-400 border border-brand-500/20 transition flex items-center gap-1.5';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (btnRoles) {
|
|
|
|
|
+ btnRoles.className = 'px-3.5 py-1.5 rounded-lg text-xs font-medium text-slate-400 hover:text-white hover:bg-slate-800 transition flex items-center gap-1.5';
|
|
|
|
|
+ }
|
|
|
|
|
+ renderInterfacesList();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadRoles() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/roles`);
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ state.roles = await res.json();
|
|
|
|
|
+ const countEl = document.getElementById('badge-roles-count');
|
|
|
|
|
+ if (countEl) countEl.innerText = (state.roles || []).length;
|
|
|
|
|
+ renderRolesTable();
|
|
|
|
|
+ populateRoleSelects();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load role tags:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function renderRolesTable() {
|
|
|
|
|
+ const tbody = document.getElementById('roles-table-body');
|
|
|
|
|
+ const empty = document.getElementById('roles-empty-state');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+
|
|
|
|
|
+ const roles = state.roles || [];
|
|
|
|
|
+ if (roles.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ empty?.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ tbody.innerHTML = roles.map(r => {
|
|
|
|
|
+ const colorKey = (r.color || 'blue').toLowerCase();
|
|
|
|
|
+ const theme = ROLE_COLORS[colorKey] || ROLE_COLORS.blue;
|
|
|
|
|
+ const ifaces = Array.isArray(r.interfaces) ? r.interfaces : [];
|
|
|
|
|
+ const ifacesChips = ifaces.length > 0
|
|
|
|
|
+ ? ifaces.map(name => `<span onclick="openIfaceModal('${escapeHtml(name)}')" class="cursor-pointer px-2 py-0.5 rounded text-[10px] font-mono bg-slate-800 text-slate-200 border border-slate-700 hover:border-cyan-500 transition mr-1 mb-1 inline-block" title="Click to view/edit ${escapeHtml(name)}">${escapeHtml(name)}</span>`).join('')
|
|
|
|
|
+ : '<span class="text-slate-600 font-sans text-xs italic">Unassigned (0)</span>';
|
|
|
|
|
+
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
|
|
+ <td class="py-3 px-4">
|
|
|
|
|
+ <span class="px-2.5 py-1 rounded text-xs font-semibold ${theme.bg} ${theme.text} border ${theme.border}">
|
|
|
|
|
+ ${escapeHtml(r.name)}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-3 px-4 font-bold text-white font-mono flex items-center gap-2">
|
|
|
|
|
+ <span>${escapeHtml(r.name)}</span>
|
|
|
|
|
+ ${r.builtin ? '<span class="text-[9px] px-1.5 py-0.2 rounded bg-slate-800 text-slate-400 border border-slate-700 uppercase font-sans">Builtin</span>' : ''}
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-3 px-4 font-sans capitalize text-slate-300">
|
|
|
|
|
+ <span class="inline-flex items-center gap-1.5">
|
|
|
|
|
+ <span class="w-2 h-2 rounded-full ${theme.bg.replace('/20', '')}"></span>
|
|
|
|
|
+ <span>${escapeHtml(r.color || 'blue')}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-3 px-4 font-sans text-slate-400 text-xs">${escapeHtml(r.description || '-')}</td>
|
|
|
|
|
+ <td class="py-3 px-4 font-sans">
|
|
|
|
|
+ <div class="flex flex-wrap items-center">
|
|
|
|
|
+ ${ifacesChips}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-3 px-4 text-right space-x-2 font-sans whitespace-nowrap">
|
|
|
|
|
+ <button onclick="openRoleModal('${escapeHtml(r.id)}')"
|
|
|
|
|
+ class="text-xs text-cyan-400 hover:text-cyan-300 font-semibold px-2 py-1 rounded hover:bg-slate-800 transition">
|
|
|
|
|
+ Edit
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="deleteRoleTag('${escapeHtml(r.id)}', '${escapeHtml(r.name)}')"
|
|
|
|
|
+ class="text-xs text-rose-400 hover:text-rose-300 font-semibold px-2 py-1 rounded hover:bg-slate-800 transition">
|
|
|
|
|
+ Delete
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openRoleModal(id = null) {
|
|
|
|
|
+ const modal = document.getElementById('role-modal');
|
|
|
|
|
+ if (!modal) return;
|
|
|
|
|
+
|
|
|
|
|
+ const idInput = document.getElementById('role-modal-id');
|
|
|
|
|
+ const nameInput = document.getElementById('role-modal-name');
|
|
|
|
|
+ const colorSelect = document.getElementById('role-modal-color');
|
|
|
|
|
+ const descInput = document.getElementById('role-modal-desc');
|
|
|
|
|
+ const title = document.getElementById('role-modal-title');
|
|
|
|
|
+
|
|
|
|
|
+ if (id) {
|
|
|
|
|
+ const role = (state.roles || []).find(r => r.id === id);
|
|
|
|
|
+ if (role) {
|
|
|
|
|
+ if (title) title.innerText = `Edit Role Tag: ${role.name}`;
|
|
|
|
|
+ if (idInput) idInput.value = role.id;
|
|
|
|
|
+ if (nameInput) nameInput.value = role.name;
|
|
|
|
|
+ if (colorSelect) colorSelect.value = role.color || 'blue';
|
|
|
|
|
+ if (descInput) descInput.value = role.description || '';
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ if (title) title.innerText = 'Add Role Tag';
|
|
|
|
|
+ if (idInput) idInput.value = '';
|
|
|
|
|
+ if (nameInput) nameInput.value = '';
|
|
|
|
|
+ if (colorSelect) colorSelect.value = 'blue';
|
|
|
|
|
+ if (descInput) descInput.value = '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ updateRoleModalPreview();
|
|
|
|
|
+ modal.classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeRoleModal() {
|
|
|
|
|
+ const modal = document.getElementById('role-modal');
|
|
|
|
|
+ if (modal) modal.classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function updateRoleModalPreview() {
|
|
|
|
|
+ const name = document.getElementById('role-modal-name')?.value.trim() || 'Preview';
|
|
|
|
|
+ const colorKey = (document.getElementById('role-modal-color')?.value || 'blue').toLowerCase();
|
|
|
|
|
+ const theme = ROLE_COLORS[colorKey] || ROLE_COLORS.blue;
|
|
|
|
|
+ const preview = document.getElementById('role-modal-preview');
|
|
|
|
|
+ if (preview) {
|
|
|
|
|
+ preview.innerText = name;
|
|
|
|
|
+ preview.className = `px-2.5 py-1 rounded text-xs font-semibold ${theme.bg} ${theme.text} border ${theme.border}`;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSaveRole(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const id = document.getElementById('role-modal-id')?.value;
|
|
|
|
|
+ const name = document.getElementById('role-modal-name')?.value.trim();
|
|
|
|
|
+ const color = document.getElementById('role-modal-color')?.value;
|
|
|
|
|
+ const desc = document.getElementById('role-modal-desc')?.value.trim();
|
|
|
|
|
+
|
|
|
|
|
+ if (!name) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = { id, name, color, description: desc };
|
|
|
|
|
+ const method = id ? 'PUT' : 'POST';
|
|
|
|
|
+ const url = id ? `${API_BASE}/roles/${encodeURIComponent(id)}` : `${API_BASE}/roles`;
|
|
|
|
|
+
|
|
|
|
|
+ const res = await fetch(url, {
|
|
|
|
|
+ method,
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Role Tag Saved', `Role tag "${name}" saved successfully`, 'success');
|
|
|
|
|
+ closeRoleModal();
|
|
|
|
|
+ await loadRoles();
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || err.message || 'Failed to save role tag', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteRoleTag(id, name) {
|
|
|
|
|
+ if (!confirm(`Are you sure you want to delete role tag "${name}"?\nInterfaces assigned to this role will default to LAN.`)) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/roles/${encodeURIComponent(id)}`, {
|
|
|
|
|
+ method: 'DELETE'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Role Tag Deleted', `Role tag "${name}" removed`, 'success');
|
|
|
|
|
+ await loadRoles();
|
|
|
|
|
+ fetchData(false);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || err.message || 'Failed to delete role tag', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function populateRoleSelects(selectedRole = '') {
|
|
|
|
|
+ const ifaceSelect = document.getElementById('modal-iface-role');
|
|
|
|
|
+ const vlanSelect = document.getElementById('vlan-role');
|
|
|
|
|
+
|
|
|
|
|
+ const roles = state.roles || [];
|
|
|
|
|
+ const optionsHtml = roles.map(r => `<option value="${escapeHtml(r.name)}">${escapeHtml(r.name)}${r.description ? ` (${escapeHtml(r.description)})` : ''}</option>`).join('');
|
|
|
|
|
+
|
|
|
|
|
+ if (ifaceSelect) {
|
|
|
|
|
+ const currentVal = selectedRole || ifaceSelect.value || 'LAN';
|
|
|
|
|
+ ifaceSelect.innerHTML = optionsHtml;
|
|
|
|
|
+ if (currentVal) ifaceSelect.value = currentVal;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (vlanSelect) {
|
|
|
|
|
+ const currentVal = vlanSelect.value || 'LAN';
|
|
|
|
|
+ vlanSelect.innerHTML = optionsHtml;
|
|
|
|
|
+ if (currentVal) vlanSelect.value = currentVal;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// Filter and Render Hierarchical Interfaces List
|
|
// Filter and Render Hierarchical Interfaces List
|
|
|
function filterIfaces(filter) {
|
|
function filterIfaces(filter) {
|
|
|
state.ifaceFilter = filter || 'all';
|
|
state.ifaceFilter = filter || 'all';
|
|
@@ -893,7 +1158,7 @@ function renderInterfacesList() {
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-3 px-4 font-sans">
|
|
<td class="py-3 px-4 font-sans">
|
|
|
<div class="flex items-center gap-1.5">
|
|
<div class="flex items-center gap-1.5">
|
|
|
- <span class="px-2 py-0.5 rounded text-[10px] font-semibold bg-slate-800 text-slate-300 border border-slate-700/60">${parent.role || 'LAN'}</span>
|
|
|
|
|
|
|
+ ${getRoleBadge(parent.role || 'LAN')}
|
|
|
<span class="text-xs text-slate-400 truncate max-w-[120px]">${parent.description || '-'}</span>
|
|
<span class="text-xs text-slate-400 truncate max-w-[120px]">${parent.description || '-'}</span>
|
|
|
</div>
|
|
</div>
|
|
|
</td>
|
|
</td>
|
|
@@ -1024,8 +1289,10 @@ function renderInterfacesList() {
|
|
|
})()}
|
|
})()}
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 px-4 font-sans text-slate-300">
|
|
<td class="py-2.5 px-4 font-sans text-slate-300">
|
|
|
- <span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 mr-1.5">${v.role || 'VLAN'}</span>
|
|
|
|
|
- <span class="text-slate-400 text-xs">${v.description || '-'}</span>
|
|
|
|
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
|
|
+ ${getRoleBadge(v.role || 'LAN')}
|
|
|
|
|
+ <span class="text-slate-400 text-xs">${v.description || '-'}</span>
|
|
|
|
|
+ </div>
|
|
|
</td>
|
|
</td>
|
|
|
<td class="py-2.5 px-4 text-slate-400">${v.mtu}</td>
|
|
<td class="py-2.5 px-4 text-slate-400">${v.mtu}</td>
|
|
|
<td class="py-2.5 px-4 text-[11px]">
|
|
<td class="py-2.5 px-4 text-[11px]">
|
|
@@ -2930,6 +3197,7 @@ function openVlanModal(defaultParent = '') {
|
|
|
document.getElementById('vlan-name').value = '';
|
|
document.getElementById('vlan-name').value = '';
|
|
|
document.getElementById('vlan-ip').value = '';
|
|
document.getElementById('vlan-ip').value = '';
|
|
|
document.getElementById('vlan-desc').value = '';
|
|
document.getElementById('vlan-desc').value = '';
|
|
|
|
|
+ populateRoleSelects('LAN');
|
|
|
autoSuggestVlanName();
|
|
autoSuggestVlanName();
|
|
|
document.getElementById('vlan-modal').classList.remove('hidden');
|
|
document.getElementById('vlan-modal').classList.remove('hidden');
|
|
|
}
|
|
}
|
|
@@ -3026,7 +3294,7 @@ function openIfaceModal(name) {
|
|
|
document.getElementById('modal-iface-name').innerText = iface.name;
|
|
document.getElementById('modal-iface-name').innerText = iface.name;
|
|
|
document.getElementById('modal-iface-badge').innerText = iface.is_vlan ? `VLAN ${iface.vlan_id}` : iface.type;
|
|
document.getElementById('modal-iface-badge').innerText = iface.is_vlan ? `VLAN ${iface.vlan_id}` : iface.type;
|
|
|
document.getElementById('modal-iface-mtu').value = iface.mtu;
|
|
document.getElementById('modal-iface-mtu').value = iface.mtu;
|
|
|
- document.getElementById('modal-iface-role').value = iface.role || 'LAN';
|
|
|
|
|
|
|
+ populateRoleSelects(iface.role || 'LAN');
|
|
|
document.getElementById('modal-iface-desc').value = iface.description || '';
|
|
document.getElementById('modal-iface-desc').value = iface.description || '';
|
|
|
|
|
|
|
|
const nameInput = document.getElementById('modal-iface-new-name');
|
|
const nameInput = document.getElementById('modal-iface-new-name');
|