|
@@ -6957,7 +6957,8 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
|
|
|
|
|
const [
|
|
const [
|
|
|
sum, bgpInst, bgpPeers, bgpNets, bgpRoutes,
|
|
sum, bgpInst, bgpPeers, bgpNets, bgpRoutes,
|
|
|
- ospfInsts, ospfAreas, ospfRanges, ospfTemplates, ospfLiveIfaces, ospfStaticNbrs, ospfNbrs, ospfLsa, filters
|
|
|
|
|
|
|
+ ospfInsts, ospfAreas, ospfRanges, ospfTemplates, ospfLiveIfaces, ospfStaticNbrs, ospfNbrs, ospfLsa, filters,
|
|
|
|
|
+ selectRules, commLists, asLists
|
|
|
] = await Promise.all([
|
|
] = await Promise.all([
|
|
|
fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
|
|
fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/instance`),
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/instance`),
|
|
@@ -6972,7 +6973,10 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/static-neighbors`),
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/static-neighbors`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/neighbors`),
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/neighbors`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/lsa`),
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/lsa`),
|
|
|
- fetchSafeJson(`${API_BASE}/routing/filters`)
|
|
|
|
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/filters`),
|
|
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/filters/select-rules`),
|
|
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/filters/community-lists`),
|
|
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/filters/as-lists`)
|
|
|
]);
|
|
]);
|
|
|
|
|
|
|
|
if (sum) state.dynSummary = sum;
|
|
if (sum) state.dynSummary = sum;
|
|
@@ -6989,6 +6993,9 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
if (ospfNbrs) state.ospfNeighbors = ospfNbrs;
|
|
if (ospfNbrs) state.ospfNeighbors = ospfNbrs;
|
|
|
if (ospfLsa) state.ospfLSA = ospfLsa;
|
|
if (ospfLsa) state.ospfLSA = ospfLsa;
|
|
|
if (filters) state.routingFilters = filters;
|
|
if (filters) state.routingFilters = filters;
|
|
|
|
|
+ if (selectRules) state.routingSelectRules = selectRules;
|
|
|
|
|
+ if (commLists) state.routingCommunityLists = commLists;
|
|
|
|
|
+ if (asLists) state.routingASLists = asLists;
|
|
|
|
|
|
|
|
try { renderBgpStatusPill(); } catch(e) { console.error('renderBgpStatusPill error:', e); }
|
|
try { renderBgpStatusPill(); } catch(e) { console.error('renderBgpStatusPill error:', e); }
|
|
|
try { renderBgpPeers(); } catch(e) { console.error('renderBgpPeers error:', e); }
|
|
try { renderBgpPeers(); } catch(e) { console.error('renderBgpPeers error:', e); }
|
|
@@ -9621,24 +9628,147 @@ function populateBridgePortCandidateSelect(selectId, currentVal = '') {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// -------------------------------------------------------------
|
|
// -------------------------------------------------------------
|
|
|
-// Routing Filters Management (/routing filter)
|
|
|
|
|
|
|
+// RouterOS v7 Routing Filters Management (/routing filter)
|
|
|
// -------------------------------------------------------------
|
|
// -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
+if (!state.activeRoutingFilterTab) state.activeRoutingFilterTab = 'rules';
|
|
|
|
|
+if (!state.selectedFilterRow) state.selectedFilterRow = {};
|
|
|
|
|
+if (!state.rfSort) state.rfSort = {};
|
|
|
|
|
+
|
|
|
|
|
+function switchRoutingFilterTab(tab) {
|
|
|
|
|
+ state.activeRoutingFilterTab = tab;
|
|
|
|
|
+ const tabs = ['rules', 'select-rules', 'community-lists', 'as-lists'];
|
|
|
|
|
+
|
|
|
|
|
+ tabs.forEach(t => {
|
|
|
|
|
+ const btn = document.getElementById(`tab-rf-${t}`);
|
|
|
|
|
+ const content = document.getElementById(`tab-content-rf-${t}`);
|
|
|
|
|
+ if (btn) {
|
|
|
|
|
+ if (t === tab) {
|
|
|
|
|
+ btn.className = 'px-3 py-1 text-xs font-semibold rounded bg-emerald-600/20 text-emerald-300 border border-emerald-500/30 transition shadow-sm';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ btn.className = 'px-3 py-1 text-xs font-semibold rounded text-slate-400 hover:text-white hover:bg-slate-800 transition';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (content) {
|
|
|
|
|
+ if (t === tab) content.classList.remove('hidden');
|
|
|
|
|
+ else content.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const chainContainer = document.getElementById('rf-chain-filter-container');
|
|
|
|
|
+ if (chainContainer) {
|
|
|
|
|
+ if (tab === 'rules' || tab === 'select-rules') chainContainer.classList.remove('hidden');
|
|
|
|
|
+ else chainContainer.classList.add('hidden');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ renderRoutingFilters();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function selectFilterRow(tab, id) {
|
|
|
|
|
+ if (state.selectedFilterRow[tab] === id) {
|
|
|
|
|
+ delete state.selectedFilterRow[tab];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.selectedFilterRow[tab] = id;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tab === 'rules') renderRoutingFilterRules();
|
|
|
|
|
+ else if (tab === 'select-rules') renderRoutingFilterSelectRules();
|
|
|
|
|
+ else if (tab === 'community-lists') renderRoutingFilterCommunityLists();
|
|
|
|
|
+ else if (tab === 'as-lists') renderRoutingFilterASLists();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openRoutingFilterNewModal() {
|
|
|
|
|
+ const tab = state.activeRoutingFilterTab || 'rules';
|
|
|
|
|
+ if (tab === 'rules') openAddRoutingFilterModal();
|
|
|
|
|
+ else if (tab === 'select-rules') openAddSelectRuleModal();
|
|
|
|
|
+ else if (tab === 'community-lists') openAddCommunityListModal();
|
|
|
|
|
+ else if (tab === 'as-lists') openAddASListModal();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function toggleSelectedRoutingFilter(disabled) {
|
|
|
|
|
+ const tab = state.activeRoutingFilterTab || 'rules';
|
|
|
|
|
+ const id = state.selectedFilterRow[tab];
|
|
|
|
|
+ if (!id) {
|
|
|
|
|
+ showToast('Select Row', 'Please click on a row to select it first', 'info');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tab === 'rules') toggleRoutingFilterState(id, disabled);
|
|
|
|
|
+ else if (tab === 'select-rules') toggleSelectRuleState(id, disabled);
|
|
|
|
|
+ else if (tab === 'community-lists') toggleCommunityListState(id, disabled);
|
|
|
|
|
+ else if (tab === 'as-lists') toggleASListState(id, disabled);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function removeSelectedRoutingFilter() {
|
|
|
|
|
+ const tab = state.activeRoutingFilterTab || 'rules';
|
|
|
|
|
+ const id = state.selectedFilterRow[tab];
|
|
|
|
|
+ if (!id) {
|
|
|
|
|
+ showToast('Select Row', 'Please click on a row to select it first', 'info');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (tab === 'rules') deleteRoutingFilter(id);
|
|
|
|
|
+ else if (tab === 'select-rules') deleteSelectRule(id);
|
|
|
|
|
+ else if (tab === 'community-lists') deleteCommunityList(id);
|
|
|
|
|
+ else if (tab === 'as-lists') deleteASList(id);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function searchRoutingFilters(q) {
|
|
|
|
|
+ state.rfSearchQuery = (q || '').trim().toLowerCase();
|
|
|
|
|
+ renderRoutingFilters();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function sortFilterTable(tab, col) {
|
|
|
|
|
+ if (!state.rfSort) state.rfSort = {};
|
|
|
|
|
+ if (!state.rfSort[tab]) state.rfSort[tab] = { col: col, asc: true };
|
|
|
|
|
+ if (state.rfSort[tab].col === col) {
|
|
|
|
|
+ state.rfSort[tab].asc = !state.rfSort[tab].asc;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.rfSort[tab].col = col;
|
|
|
|
|
+ state.rfSort[tab].asc = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ const asc = state.rfSort[tab].asc;
|
|
|
|
|
+
|
|
|
|
|
+ let list = [];
|
|
|
|
|
+ if (tab === 'rules') list = state.routingFilters || [];
|
|
|
|
|
+ else if (tab === 'select-rules') list = state.routingSelectRules || [];
|
|
|
|
|
+ else if (tab === 'community-lists') list = state.routingCommunityLists || [];
|
|
|
|
|
+ else if (tab === 'as-lists') list = state.routingASLists || [];
|
|
|
|
|
+
|
|
|
|
|
+ list.sort((a, b) => {
|
|
|
|
|
+ let valA = a[col] ?? '';
|
|
|
|
|
+ let valB = b[col] ?? '';
|
|
|
|
|
+ if (col === 'flags') {
|
|
|
|
|
+ valA = a.disabled ? 'X' : 'A';
|
|
|
|
|
+ valB = b.disabled ? 'X' : 'A';
|
|
|
|
|
+ }
|
|
|
|
|
+ if (typeof valA === 'number' && typeof valB === 'number') {
|
|
|
|
|
+ return asc ? valA - valB : valB - valA;
|
|
|
|
|
+ }
|
|
|
|
|
+ return asc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA));
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const allCols = ['flags', 'rule_order', 'chain', 'rule', 'name', 'communities', 'range', 'regexp', 'comment'];
|
|
|
|
|
+ allCols.forEach(c => {
|
|
|
|
|
+ const el = document.getElementById(`sort-icon-${tab}-${c}`);
|
|
|
|
|
+ if (el) el.innerText = (c === col) ? (asc ? '▲' : '▼') : '';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (tab === 'rules') renderRoutingFilterRules();
|
|
|
|
|
+ else if (tab === 'select-rules') renderRoutingFilterSelectRules();
|
|
|
|
|
+ else if (tab === 'community-lists') renderRoutingFilterCommunityLists();
|
|
|
|
|
+ else if (tab === 'as-lists') renderRoutingFilterASLists();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function renderRoutingFilters() {
|
|
function renderRoutingFilters() {
|
|
|
- const tbody = document.getElementById('routing-filters-table-body');
|
|
|
|
|
- if (!tbody) return;
|
|
|
|
|
- const empty = document.getElementById('routing-filters-empty');
|
|
|
|
|
- const countBadge = document.getElementById('routing-filters-count-badge');
|
|
|
|
|
const navBadge = document.getElementById('badge-filters-count');
|
|
const navBadge = document.getElementById('badge-filters-count');
|
|
|
- const chainSelect = document.getElementById('routing-filter-chain-select');
|
|
|
|
|
-
|
|
|
|
|
const allRules = state.routingFilters || [];
|
|
const allRules = state.routingFilters || [];
|
|
|
if (navBadge) navBadge.innerText = allRules.length;
|
|
if (navBadge) navBadge.innerText = allRules.length;
|
|
|
|
|
|
|
|
- // Populate chain filter select options dynamically
|
|
|
|
|
|
|
+ // Populate chain filter select options and datalist
|
|
|
|
|
+ const chainSelect = document.getElementById('routing-filter-chain-select');
|
|
|
|
|
+ const chainDatalist = document.getElementById('dl-routing-chains');
|
|
|
|
|
+ const uniqueChains = Array.from(new Set(allRules.map(r => r.chain).filter(Boolean))).sort();
|
|
|
|
|
+
|
|
|
if (chainSelect) {
|
|
if (chainSelect) {
|
|
|
const currentVal = state.filterChainSelected || 'all';
|
|
const currentVal = state.filterChainSelected || 'all';
|
|
|
- const uniqueChains = Array.from(new Set(allRules.map(r => r.chain).filter(Boolean))).sort();
|
|
|
|
|
chainSelect.innerHTML = `<option value="all">All Chains (${allRules.length})</option>` +
|
|
chainSelect.innerHTML = `<option value="all">All Chains (${allRules.length})</option>` +
|
|
|
uniqueChains.map(ch => {
|
|
uniqueChains.map(ch => {
|
|
|
const count = allRules.filter(r => r.chain === ch).length;
|
|
const count = allRules.filter(r => r.chain === ch).length;
|
|
@@ -9647,105 +9777,168 @@ function renderRoutingFilters() {
|
|
|
chainSelect.value = currentVal;
|
|
chainSelect.value = currentVal;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Filter rules by selected chain
|
|
|
|
|
- const filteredRules = (state.filterChainSelected && state.filterChainSelected !== 'all')
|
|
|
|
|
- ? allRules.filter(r => r.chain === state.filterChainSelected)
|
|
|
|
|
- : allRules.slice();
|
|
|
|
|
|
|
+ if (chainDatalist) {
|
|
|
|
|
+ chainDatalist.innerHTML = uniqueChains.map(ch => `<option value="${escapeHtml(ch)}">`).join('');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- // Sort by chain then rule_order
|
|
|
|
|
- filteredRules.sort((a, b) => {
|
|
|
|
|
- if (a.chain !== b.chain) return (a.chain || '').localeCompare(b.chain || '');
|
|
|
|
|
- return (a.rule_order || 0) - (b.rule_order || 0);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ renderRoutingFilterRules();
|
|
|
|
|
+ renderRoutingFilterSelectRules();
|
|
|
|
|
+ renderRoutingFilterCommunityLists();
|
|
|
|
|
+ renderRoutingFilterASLists();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function filterRoutingFiltersByChain(chain) {
|
|
|
|
|
+ state.filterChainSelected = chain;
|
|
|
|
|
+ renderRoutingFilters();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// TAB 1: RULES (/routing filter rule)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function renderRoutingFilterRules() {
|
|
|
|
|
+ const tbody = document.getElementById('routing-filter-rules-tbody');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ const empty = document.getElementById('rf-rules-empty');
|
|
|
|
|
+ const statusCount = document.getElementById('rf-status-count');
|
|
|
|
|
+
|
|
|
|
|
+ let list = (state.routingFilters || []).slice();
|
|
|
|
|
+
|
|
|
|
|
+ if (state.filterChainSelected && state.filterChainSelected !== 'all') {
|
|
|
|
|
+ list = list.filter(r => r.chain === state.filterChainSelected);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const q = state.rfSearchQuery;
|
|
|
|
|
+ if (q) {
|
|
|
|
|
+ list = list.filter(r =>
|
|
|
|
|
+ (r.chain && r.chain.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (r.rule && r.rule.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (r.prefix && r.prefix.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (r.comment && r.comment.toLowerCase().includes(q))
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!state.rfSort?.['rules']) {
|
|
|
|
|
+ list.sort((a, b) => {
|
|
|
|
|
+ if (a.chain !== b.chain) return (a.chain || '').localeCompare(b.chain || '');
|
|
|
|
|
+ return (a.rule_order || 0) - (b.rule_order || 0);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (countBadge) countBadge.innerText = `${filteredRules.length} Rules`;
|
|
|
|
|
|
|
+ if (statusCount && (state.activeRoutingFilterTab === 'rules' || !state.activeRoutingFilterTab)) {
|
|
|
|
|
+ statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (filteredRules.length === 0) {
|
|
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
tbody.innerHTML = '';
|
|
tbody.innerHTML = '';
|
|
|
if (empty) empty.classList.remove('hidden');
|
|
if (empty) empty.classList.remove('hidden');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (empty) empty.classList.add('hidden');
|
|
if (empty) empty.classList.add('hidden');
|
|
|
|
|
|
|
|
- tbody.innerHTML = filteredRules.map(r => {
|
|
|
|
|
|
|
+ tbody.innerHTML = list.map(r => {
|
|
|
|
|
+ const isSelected = state.selectedFilterRow?.['rules'] === r.id;
|
|
|
const flagA = !r.disabled ? '<span class="text-cyan-400 font-bold" title="Active">A</span>' : '';
|
|
const flagA = !r.disabled ? '<span class="text-cyan-400 font-bold" title="Active">A</span>' : '';
|
|
|
- const flagX = r.disabled ? '<span class="text-rose-400 font-bold ml-1" title="Disabled">X</span>' : '';
|
|
|
|
|
-
|
|
|
|
|
- let actionBadge = '';
|
|
|
|
|
- if (r.action === 'accept') {
|
|
|
|
|
- actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">accept</span>';
|
|
|
|
|
- } else if (r.action === 'discard') {
|
|
|
|
|
- actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">discard</span>';
|
|
|
|
|
- } else if (r.action === 'reject') {
|
|
|
|
|
- actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-red-950 text-red-300 border border-red-800/40">reject</span>';
|
|
|
|
|
- } else {
|
|
|
|
|
- actionBadge = `<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-blue-500/10 text-blue-400 border border-blue-500/20">${escapeHtml(r.action || 'accept')}</span>`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const flagX = r.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
|
|
|
|
|
+ const flagC = r.comment ? '<span class="text-amber-400 font-bold ml-1" title="Comment">C</span>' : '';
|
|
|
|
|
|
|
|
- // Prefix match
|
|
|
|
|
- let matchPfx = '';
|
|
|
|
|
- if (r.prefix) {
|
|
|
|
|
- matchPfx = `<span class="font-bold text-white">${escapeHtml(r.prefix)}</span>`;
|
|
|
|
|
- if (r.prefix_len) {
|
|
|
|
|
- matchPfx += ` <span class="text-slate-400 text-[11px] font-normal">[${escapeHtml(r.prefix_len)}]</span>`;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ let ruleDisplay = '';
|
|
|
|
|
+ if (r.rule && r.rule.trim() !== '') {
|
|
|
|
|
+ ruleDisplay = `<span class="text-emerald-400 font-mono font-medium">${escapeHtml(r.rule)}</span>`;
|
|
|
} else {
|
|
} else {
|
|
|
- matchPfx = '<span class="text-slate-500 italic">any prefix</span>';
|
|
|
|
|
|
|
+ let conds = [];
|
|
|
|
|
+ if (r.prefix) conds.push(`dst in ${escapeHtml(r.prefix)}`);
|
|
|
|
|
+ if (r.match_protocol && r.match_protocol !== 'any') conds.push(`protocol ${escapeHtml(r.match_protocol)}`);
|
|
|
|
|
+ let act = r.action || 'accept';
|
|
|
|
|
+ ruleDisplay = `<span class="text-slate-300 font-mono">if (${conds.length ? conds.join(' && ') : 'true'}) { ${escapeHtml(act)}; }</span>`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Other matches
|
|
|
|
|
- const matchExtra = [];
|
|
|
|
|
- if (r.match_protocol && r.match_protocol !== 'any') {
|
|
|
|
|
- matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 border border-slate-700">proto:${escapeHtml(r.match_protocol)}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- if (r.match_as_path) {
|
|
|
|
|
- matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40 font-mono">as-path:${escapeHtml(r.match_as_path)}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- if (r.match_community) {
|
|
|
|
|
- matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40 font-mono">comm:${escapeHtml(r.match_community)}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- const matchExtraHtml = matchExtra.length > 0 ? matchExtra.join(' ') : '<span class="text-slate-600">-</span>';
|
|
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr onclick="selectFilterRow('rules', '${r.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
|
|
|
|
|
+ <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}${flagC}</td>
|
|
|
|
|
+ <td class="py-2 px-2 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
|
|
|
|
|
+ <td class="py-2 px-3">
|
|
|
|
|
+ <span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-800 text-cyan-300 border border-slate-700 font-semibold">${escapeHtml(r.chain)}</span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ <td class="py-2 px-3 break-all max-w-xl">${ruleDisplay}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
|
|
|
|
|
+ <button onclick="moveRoutingFilterOrder('${r.id}', -1)" title="Move Up" class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700">▲</button>
|
|
|
|
|
+ <button onclick="moveRoutingFilterOrder('${r.id}', 1)" title="Move Down" class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700">▼</button>
|
|
|
|
|
+ <button onclick="toggleRoutingFilterState('${r.id}', ${!r.disabled})" class="px-2 py-0.5 rounded text-[10px] ${r.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
|
|
|
|
|
+ ${r.disabled ? 'Enable' : 'Disable'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="openEditRoutingFilterModal('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
|
|
|
|
|
+ Edit
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="deleteRoutingFilter('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
|
|
+ ✕
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // Set modifications
|
|
|
|
|
- const setBadges = [];
|
|
|
|
|
- if (r.set_local_pref > 0) {
|
|
|
|
|
- setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-cyan-950 text-cyan-300 border border-cyan-800/40">loc-pref: ${r.set_local_pref}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- if (r.set_metric > 0) {
|
|
|
|
|
- setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-amber-950 text-amber-300 border border-amber-800/40">med: ${r.set_metric}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- if (r.set_as_prepend > 0) {
|
|
|
|
|
- setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40">prepend: +${r.set_as_prepend}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- if (r.set_community) {
|
|
|
|
|
- setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40">comm: ${escapeHtml(r.set_community)}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- if (r.set_next_hop) {
|
|
|
|
|
- setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-blue-950 text-blue-300 border border-blue-800/40">nh: ${escapeHtml(r.set_next_hop)}</span>`);
|
|
|
|
|
- }
|
|
|
|
|
- const setHtml = setBadges.length > 0 ? setBadges.join(' ') : '<span class="text-slate-600">-</span>';
|
|
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// TAB 2: SELECT RULES (/routing filter select-rule)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function renderRoutingFilterSelectRules() {
|
|
|
|
|
+ const tbody = document.getElementById('routing-filter-select-rules-tbody');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ const empty = document.getElementById('rf-select-rules-empty');
|
|
|
|
|
+ const statusCount = document.getElementById('rf-status-count');
|
|
|
|
|
+
|
|
|
|
|
+ let list = (state.routingSelectRules || []).slice();
|
|
|
|
|
+
|
|
|
|
|
+ if (state.filterChainSelected && state.filterChainSelected !== 'all') {
|
|
|
|
|
+ list = list.filter(r => r.chain === state.filterChainSelected);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const q = state.rfSearchQuery;
|
|
|
|
|
+ if (q) {
|
|
|
|
|
+ list = list.filter(r =>
|
|
|
|
|
+ (r.chain && r.chain.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (r.rule && r.rule.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (r.comment && r.comment.toLowerCase().includes(q))
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (statusCount && state.activeRoutingFilterTab === 'select-rules') {
|
|
|
|
|
+ statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+ if (empty) empty.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (empty) empty.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ tbody.innerHTML = list.map(r => {
|
|
|
|
|
+ const isSelected = state.selectedFilterRow?.['select-rules'] === r.id;
|
|
|
|
|
+ const flagA = !r.disabled ? '<span class="text-cyan-400 font-bold">A</span>' : '';
|
|
|
|
|
+ const flagX = r.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '';
|
|
|
|
|
|
|
|
return `
|
|
return `
|
|
|
- <tr class="hover:bg-slate-800/40 transition">
|
|
|
|
|
- <td class="py-2.5 px-3 text-center tracking-wider">${flagA}${flagX}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
|
|
|
|
|
- <td class="py-2.5 px-3">
|
|
|
|
|
|
|
+ <tr onclick="selectFilterRow('select-rules', '${r.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
|
|
|
|
|
+ <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}</td>
|
|
|
|
|
+ <td class="py-2 px-2 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
|
|
|
|
|
+ <td class="py-2 px-3">
|
|
|
<span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-800 text-cyan-300 border border-slate-700 font-semibold">${escapeHtml(r.chain)}</span>
|
|
<span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-800 text-cyan-300 border border-slate-700 font-semibold">${escapeHtml(r.chain)}</span>
|
|
|
</td>
|
|
</td>
|
|
|
- <td class="py-2.5 px-3">${actionBadge}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 font-mono">${matchPfx}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 space-x-1">${matchExtraHtml}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 space-x-1 font-mono">${setHtml}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
|
|
|
|
|
- <td class="py-2.5 px-3 text-right space-x-1.5 whitespace-nowrap font-sans">
|
|
|
|
|
- <button onclick="toggleRoutingFilterState('${r.id}', ${!r.disabled})" class="px-2 py-1 rounded text-[11px] ${r.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
|
|
|
|
|
|
|
+ <td class="py-2 px-3 break-all max-w-xl font-mono text-emerald-400">${escapeHtml(r.rule || '')}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
|
|
|
|
|
+ <button onclick="toggleSelectRuleState('${r.id}', ${!r.disabled})" class="px-2 py-0.5 rounded text-[10px] ${r.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
|
|
|
${r.disabled ? 'Enable' : 'Disable'}
|
|
${r.disabled ? 'Enable' : 'Disable'}
|
|
|
</button>
|
|
</button>
|
|
|
- <button onclick="openEditRoutingFilterModal('${r.id}')" class="px-2 py-1 rounded text-[11px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
|
|
|
|
|
|
|
+ <button onclick="openEditSelectRuleModal('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
|
|
|
Edit
|
|
Edit
|
|
|
</button>
|
|
</button>
|
|
|
- <button onclick="deleteRoutingFilter('${r.id}')" class="px-2 py-1 rounded text-[11px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
|
|
- Delete
|
|
|
|
|
|
|
+ <button onclick="deleteSelectRule('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
|
|
+ ✕
|
|
|
</button>
|
|
</button>
|
|
|
</td>
|
|
</td>
|
|
|
</tr>
|
|
</tr>
|
|
@@ -9753,11 +9946,224 @@ function renderRoutingFilters() {
|
|
|
}).join('');
|
|
}).join('');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function filterRoutingFiltersByChain(chain) {
|
|
|
|
|
- state.filterChainSelected = chain;
|
|
|
|
|
- renderRoutingFilters();
|
|
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// TAB 3: COMMUNITY LISTS (/routing filter community-list)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function renderRoutingFilterCommunityLists() {
|
|
|
|
|
+ const tbody = document.getElementById('routing-filter-comm-lists-tbody');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ const empty = document.getElementById('rf-comm-lists-empty');
|
|
|
|
|
+ const statusCount = document.getElementById('rf-status-count');
|
|
|
|
|
+
|
|
|
|
|
+ let list = (state.routingCommunityLists || []).slice();
|
|
|
|
|
+
|
|
|
|
|
+ const q = state.rfSearchQuery;
|
|
|
|
|
+ if (q) {
|
|
|
|
|
+ list = list.filter(cl =>
|
|
|
|
|
+ (cl.name && cl.name.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (cl.comment && cl.comment.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (cl.communities && cl.communities.some(c => c.toLowerCase().includes(q)))
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (statusCount && state.activeRoutingFilterTab === 'community-lists') {
|
|
|
|
|
+ statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+ if (empty) empty.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (empty) empty.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ tbody.innerHTML = list.map(cl => {
|
|
|
|
|
+ const isSelected = state.selectedFilterRow?.['community-lists'] === cl.id;
|
|
|
|
|
+ const flagA = !cl.disabled ? '<span class="text-cyan-400 font-bold">A</span>' : '';
|
|
|
|
|
+ const flagX = cl.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '';
|
|
|
|
|
+
|
|
|
|
|
+ const commBadges = (cl.communities || []).map(c =>
|
|
|
|
|
+ `<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40 font-mono">${escapeHtml(c)}</span>`
|
|
|
|
|
+ ).join(' ');
|
|
|
|
|
+
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr onclick="selectFilterRow('community-lists', '${cl.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
|
|
|
|
|
+ <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}</td>
|
|
|
|
|
+ <td class="py-2 px-3 font-mono font-bold text-white">${escapeHtml(cl.name)}</td>
|
|
|
|
|
+ <td class="py-2 px-3 space-x-1">${commBadges || '<span class="text-slate-600">-</span>'}</td>
|
|
|
|
|
+ <td class="py-2 px-3 font-mono text-slate-400">${escapeHtml(cl.regexp || '-')}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(cl.comment || '')}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
|
|
|
|
|
+ <button onclick="toggleCommunityListState('${cl.id}', ${!cl.disabled})" class="px-2 py-0.5 rounded text-[10px] ${cl.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
|
|
|
|
|
+ ${cl.disabled ? 'Enable' : 'Disable'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="openEditCommunityListModal('${cl.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
|
|
|
|
|
+ Edit
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="deleteCommunityList('${cl.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
|
|
+ ✕
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// TAB 4: AS LISTS (/routing filter num-list)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function renderRoutingFilterASLists() {
|
|
|
|
|
+ const tbody = document.getElementById('routing-filter-as-lists-tbody');
|
|
|
|
|
+ if (!tbody) return;
|
|
|
|
|
+ const empty = document.getElementById('rf-as-lists-empty');
|
|
|
|
|
+ const statusCount = document.getElementById('rf-status-count');
|
|
|
|
|
+
|
|
|
|
|
+ let list = (state.routingASLists || []).slice();
|
|
|
|
|
+
|
|
|
|
|
+ const q = state.rfSearchQuery;
|
|
|
|
|
+ if (q) {
|
|
|
|
|
+ list = list.filter(al =>
|
|
|
|
|
+ (al.name && al.name.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (al.range && al.range.toLowerCase().includes(q)) ||
|
|
|
|
|
+ (al.comment && al.comment.toLowerCase().includes(q))
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (statusCount && state.activeRoutingFilterTab === 'as-lists') {
|
|
|
|
|
+ statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (list.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+ if (empty) empty.classList.remove('hidden');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (empty) empty.classList.add('hidden');
|
|
|
|
|
+
|
|
|
|
|
+ tbody.innerHTML = list.map(al => {
|
|
|
|
|
+ const isSelected = state.selectedFilterRow?.['as-lists'] === al.id;
|
|
|
|
|
+ const flagA = !al.disabled ? '<span class="text-cyan-400 font-bold">A</span>' : '';
|
|
|
|
|
+ const flagX = al.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '';
|
|
|
|
|
+
|
|
|
|
|
+ return `
|
|
|
|
|
+ <tr onclick="selectFilterRow('as-lists', '${al.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
|
|
|
|
|
+ <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}</td>
|
|
|
|
|
+ <td class="py-2 px-3 font-mono font-bold text-white">${escapeHtml(al.name)}</td>
|
|
|
|
|
+ <td class="py-2 px-3 font-mono text-purple-300">${escapeHtml(al.range)}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(al.comment || '')}</td>
|
|
|
|
|
+ <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
|
|
|
|
|
+ <button onclick="toggleASListState('${al.id}', ${!al.disabled})" class="px-2 py-0.5 rounded text-[10px] ${al.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
|
|
|
|
|
+ ${al.disabled ? 'Enable' : 'Disable'}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="openEditASListModal('${al.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
|
|
|
|
|
+ Edit
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button onclick="deleteASList('${al.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
|
|
+ ✕
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ `;
|
|
|
|
|
+ }).join('');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// SNIPPETS & WIZARD HELPERS
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function insertRuleSnippet(type) {
|
|
|
|
|
+ const textarea = document.getElementById('modal-rf-rule');
|
|
|
|
|
+ if (!textarea) return;
|
|
|
|
|
+ let snippet = '';
|
|
|
|
|
+ switch (type) {
|
|
|
|
|
+ case 'accept-default':
|
|
|
|
|
+ snippet = 'if (dst in 0.0.0.0/0) { accept; }';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'drop-rfc1918':
|
|
|
|
|
+ snippet = 'if (dst in 10.0.0.0/8 || dst in 172.16.0.0/12 || dst in 192.168.0.0/16) { reject; }';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'len-24':
|
|
|
|
|
+ snippet = 'if (dst-len > 24) { reject; }';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'local-pref-150':
|
|
|
|
|
+ snippet = 'if (dst in 10.0.0.0/8) { set bgp-local-pref 150; accept; }';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'as-prepend-2':
|
|
|
|
|
+ snippet = 'set bgp-path-prepend 2; accept;';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'set-comm':
|
|
|
|
|
+ snippet = 'set bgp-communities.add 65000:100; accept;';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'match-comm':
|
|
|
|
|
+ snippet = 'if (bgp-communities includes 65000:100) { accept; }';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'reject-all':
|
|
|
|
|
+ snippet = 'reject;';
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!snippet) return;
|
|
|
|
|
+ if (!textarea.value.trim()) {
|
|
|
|
|
+ textarea.value = snippet;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ textarea.value = textarea.value.trim() + '\n' + snippet;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function generateScriptFromWizard() {
|
|
|
|
|
+ const pfx = (document.getElementById('modal-rf-prefix')?.value || '').trim();
|
|
|
|
|
+ const pfxLen = (document.getElementById('modal-rf-prefix-len')?.value || '').trim();
|
|
|
|
|
+ const proto = document.getElementById('modal-rf-protocol')?.value || 'any';
|
|
|
|
|
+ const asp = (document.getElementById('modal-rf-as-path')?.value || '').trim();
|
|
|
|
|
+ const comm = (document.getElementById('modal-rf-community')?.value || '').trim();
|
|
|
|
|
+ const act = document.getElementById('modal-rf-action')?.value || 'accept';
|
|
|
|
|
+ const lp = parseInt(document.getElementById('modal-rf-set-local-pref')?.value, 10) || 0;
|
|
|
|
|
+ const prep = parseInt(document.getElementById('modal-rf-set-as-prepend')?.value, 10) || 0;
|
|
|
|
|
+
|
|
|
|
|
+ const conds = [];
|
|
|
|
|
+ if (pfx) {
|
|
|
|
|
+ if (pfxLen) {
|
|
|
|
|
+ if (pfxLen.includes('-')) {
|
|
|
|
|
+ const parts = pfxLen.split('-');
|
|
|
|
|
+ conds.push(`dst in ${pfx}{${parts[0].trim()},${parts[1].trim()}}`);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ conds.push(`dst in ${pfx} && dst-len >= ${pfxLen}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ conds.push(`dst in ${pfx}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (proto && proto !== 'any') {
|
|
|
|
|
+ conds.push(`protocol ${proto}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (asp) {
|
|
|
|
|
+ conds.push(`bgp-as-path "${asp}"`);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (comm) {
|
|
|
|
|
+ conds.push(`bgp-communities includes ${comm}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const actions = [];
|
|
|
|
|
+ if (lp > 0) actions.push(`set bgp-local-pref ${lp};`);
|
|
|
|
|
+ if (prep > 0) actions.push(`set bgp-path-prepend ${prep};`);
|
|
|
|
|
+ actions.push(`${act};`);
|
|
|
|
|
+
|
|
|
|
|
+ let code = '';
|
|
|
|
|
+ if (conds.length > 0) {
|
|
|
|
|
+ code = `if (${conds.join(' && ')}) {\n ${actions.join(' ')}\n}`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ code = actions.join(' ');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const textarea = document.getElementById('modal-rf-rule');
|
|
|
|
|
+ if (textarea) textarea.value = code;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// MODAL CRUD: RULES (/routing filter rule)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
function openAddRoutingFilterModal(defaultChain = '') {
|
|
function openAddRoutingFilterModal(defaultChain = '') {
|
|
|
const form = document.getElementById('routing-filter-form');
|
|
const form = document.getElementById('routing-filter-form');
|
|
|
if (form) form.reset();
|
|
if (form) form.reset();
|
|
@@ -9769,10 +10175,8 @@ function openAddRoutingFilterModal(defaultChain = '') {
|
|
|
if (chainEl) chainEl.value = defaultChain || (state.filterChainSelected && state.filterChainSelected !== 'all' ? state.filterChainSelected : 'bgp-in');
|
|
if (chainEl) chainEl.value = defaultChain || (state.filterChainSelected && state.filterChainSelected !== 'all' ? state.filterChainSelected : 'bgp-in');
|
|
|
const orderEl = document.getElementById('modal-rf-order');
|
|
const orderEl = document.getElementById('modal-rf-order');
|
|
|
if (orderEl) orderEl.value = ((state.routingFilters || []).length + 1) * 10;
|
|
if (orderEl) orderEl.value = ((state.routingFilters || []).length + 1) * 10;
|
|
|
- const actEl = document.getElementById('modal-rf-action');
|
|
|
|
|
- if (actEl) actEl.value = 'accept';
|
|
|
|
|
- const protoEl = document.getElementById('modal-rf-protocol');
|
|
|
|
|
- if (protoEl) protoEl.value = 'any';
|
|
|
|
|
|
|
+ const ruleEl = document.getElementById('modal-rf-rule');
|
|
|
|
|
+ if (ruleEl) ruleEl.value = 'if (dst in 10.0.0.0/8) { set bgp-local-pref 150; accept; }';
|
|
|
const disEl = document.getElementById('modal-rf-disabled');
|
|
const disEl = document.getElementById('modal-rf-disabled');
|
|
|
if (disEl) disEl.checked = false;
|
|
if (disEl) disEl.checked = false;
|
|
|
const modal = document.getElementById('modal-routing-filter');
|
|
const modal = document.getElementById('modal-routing-filter');
|
|
@@ -9790,28 +10194,18 @@ function openEditRoutingFilterModal(id) {
|
|
|
if (chainEl) chainEl.value = r.chain || '';
|
|
if (chainEl) chainEl.value = r.chain || '';
|
|
|
const orderEl = document.getElementById('modal-rf-order');
|
|
const orderEl = document.getElementById('modal-rf-order');
|
|
|
if (orderEl) orderEl.value = r.rule_order || 10;
|
|
if (orderEl) orderEl.value = r.rule_order || 10;
|
|
|
- const actEl = document.getElementById('modal-rf-action');
|
|
|
|
|
- if (actEl) actEl.value = r.action || 'accept';
|
|
|
|
|
- const pfxEl = document.getElementById('modal-rf-prefix');
|
|
|
|
|
- if (pfxEl) pfxEl.value = r.prefix || '';
|
|
|
|
|
- const pfxLenEl = document.getElementById('modal-rf-prefix-len');
|
|
|
|
|
- if (pfxLenEl) pfxLenEl.value = r.prefix_len || '';
|
|
|
|
|
- const protoEl = document.getElementById('modal-rf-protocol');
|
|
|
|
|
- if (protoEl) protoEl.value = r.match_protocol || 'any';
|
|
|
|
|
- const aspEl = document.getElementById('modal-rf-as-path');
|
|
|
|
|
- if (aspEl) aspEl.value = r.match_as_path || '';
|
|
|
|
|
- const commEl = document.getElementById('modal-rf-community');
|
|
|
|
|
- if (commEl) commEl.value = r.match_community || '';
|
|
|
|
|
- const lpEl = document.getElementById('modal-rf-set-local-pref');
|
|
|
|
|
- if (lpEl) lpEl.value = r.set_local_pref || '';
|
|
|
|
|
- const metEl = document.getElementById('modal-rf-set-metric');
|
|
|
|
|
- if (metEl) metEl.value = r.set_metric || '';
|
|
|
|
|
- const prepEl = document.getElementById('modal-rf-set-as-prepend');
|
|
|
|
|
- if (prepEl) prepEl.value = r.set_as_prepend || '';
|
|
|
|
|
- const setCommEl = document.getElementById('modal-rf-set-community');
|
|
|
|
|
- if (setCommEl) setCommEl.value = r.set_community || '';
|
|
|
|
|
- const nhEl = document.getElementById('modal-rf-set-next-hop');
|
|
|
|
|
- if (nhEl) nhEl.value = r.set_next_hop || '';
|
|
|
|
|
|
|
+ const ruleEl = document.getElementById('modal-rf-rule');
|
|
|
|
|
+ if (ruleEl) {
|
|
|
|
|
+ if (r.rule) {
|
|
|
|
|
+ ruleEl.value = r.rule;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ let conds = [];
|
|
|
|
|
+ if (r.prefix) conds.push(`dst in ${r.prefix}`);
|
|
|
|
|
+ if (r.match_protocol && r.match_protocol !== 'any') conds.push(`protocol ${r.match_protocol}`);
|
|
|
|
|
+ let act = r.action || 'accept';
|
|
|
|
|
+ ruleEl.value = `if (${conds.length ? conds.join(' && ') : 'true'}) { ${act}; }`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
const comEl = document.getElementById('modal-rf-comment');
|
|
const comEl = document.getElementById('modal-rf-comment');
|
|
|
if (comEl) comEl.value = r.comment || '';
|
|
if (comEl) comEl.value = r.comment || '';
|
|
|
const disEl = document.getElementById('modal-rf-disabled');
|
|
const disEl = document.getElementById('modal-rf-disabled');
|
|
@@ -9826,22 +10220,12 @@ function closeRoutingFilterModal() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function submitRoutingFilter(e) {
|
|
async function submitRoutingFilter(e) {
|
|
|
- e.preventDefault();
|
|
|
|
|
|
|
+ if (e) e.preventDefault();
|
|
|
const payload = {
|
|
const payload = {
|
|
|
id: document.getElementById('modal-rf-id').value,
|
|
id: document.getElementById('modal-rf-id').value,
|
|
|
chain: document.getElementById('modal-rf-chain').value.trim(),
|
|
chain: document.getElementById('modal-rf-chain').value.trim(),
|
|
|
rule_order: parseInt(document.getElementById('modal-rf-order').value, 10) || 10,
|
|
rule_order: parseInt(document.getElementById('modal-rf-order').value, 10) || 10,
|
|
|
- action: document.getElementById('modal-rf-action').value,
|
|
|
|
|
- prefix: document.getElementById('modal-rf-prefix').value.trim(),
|
|
|
|
|
- prefix_len: document.getElementById('modal-rf-prefix-len').value.trim(),
|
|
|
|
|
- match_protocol: document.getElementById('modal-rf-protocol').value,
|
|
|
|
|
- match_as_path: document.getElementById('modal-rf-as-path').value.trim(),
|
|
|
|
|
- match_community: document.getElementById('modal-rf-community').value.trim(),
|
|
|
|
|
- set_local_pref: parseInt(document.getElementById('modal-rf-set-local-pref').value, 10) || 0,
|
|
|
|
|
- set_metric: parseInt(document.getElementById('modal-rf-set-metric').value, 10) || 0,
|
|
|
|
|
- set_as_prepend: parseInt(document.getElementById('modal-rf-set-as-prepend').value, 10) || 0,
|
|
|
|
|
- set_community: document.getElementById('modal-rf-set-community').value.trim(),
|
|
|
|
|
- set_next_hop: document.getElementById('modal-rf-set-next-hop').value.trim(),
|
|
|
|
|
|
|
+ rule: document.getElementById('modal-rf-rule').value.trim(),
|
|
|
comment: document.getElementById('modal-rf-comment').value.trim(),
|
|
comment: document.getElementById('modal-rf-comment').value.trim(),
|
|
|
disabled: document.getElementById('modal-rf-disabled').checked
|
|
disabled: document.getElementById('modal-rf-disabled').checked
|
|
|
};
|
|
};
|
|
@@ -9865,6 +10249,66 @@ async function submitRoutingFilter(e) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+async function applyRoutingFilterForm() {
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id: document.getElementById('modal-rf-id').value,
|
|
|
|
|
+ chain: document.getElementById('modal-rf-chain').value.trim(),
|
|
|
|
|
+ rule_order: parseInt(document.getElementById('modal-rf-order').value, 10) || 10,
|
|
|
|
|
+ rule: document.getElementById('modal-rf-rule').value.trim(),
|
|
|
|
|
+ comment: document.getElementById('modal-rf-comment').value.trim(),
|
|
|
|
|
+ disabled: document.getElementById('modal-rf-disabled').checked
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ const data = await res.json();
|
|
|
|
|
+ if (data.id) document.getElementById('modal-rf-id').value = data.id;
|
|
|
|
|
+ showToast('Applied', 'Routing filter configuration applied', 'success');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to apply filter rule', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function moveRoutingFilterOrder(id, delta) {
|
|
|
|
|
+ const list = state.routingFilters || [];
|
|
|
|
|
+ const idx = list.findIndex(r => r.id === id);
|
|
|
|
|
+ if (idx < 0) return;
|
|
|
|
|
+ const targetIdx = idx + delta;
|
|
|
|
|
+ if (targetIdx < 0 || targetIdx >= list.length) return;
|
|
|
|
|
+
|
|
|
|
|
+ const currentOrder = list[idx].rule_order || 10;
|
|
|
|
|
+ const targetOrder = list[targetIdx].rule_order || 20;
|
|
|
|
|
+
|
|
|
|
|
+ list[idx].rule_order = targetOrder;
|
|
|
|
|
+ list[targetIdx].rule_order = currentOrder;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await fetch(`${API_BASE}/routing/filters`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(list[idx])
|
|
|
|
|
+ });
|
|
|
|
|
+ await fetch(`${API_BASE}/routing/filters`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(list[targetIdx])
|
|
|
|
|
+ });
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('Reorder error:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function deleteRoutingFilter(id) {
|
|
async function deleteRoutingFilter(id) {
|
|
|
if (!confirm('Are you sure you want to delete this routing filter rule?')) return;
|
|
if (!confirm('Are you sure you want to delete this routing filter rule?')) return;
|
|
|
try {
|
|
try {
|
|
@@ -9900,6 +10344,297 @@ async function toggleRoutingFilterState(id, disabled) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// MODAL CRUD: SELECT RULES (/routing filter select-rule)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function openAddSelectRuleModal() {
|
|
|
|
|
+ document.getElementById('modal-rf-sr-id').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-title').innerText = 'Add Routing Select Rule';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-chain').value = state.filterChainSelected && state.filterChainSelected !== 'all' ? state.filterChainSelected : 'bgp-out';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-order').value = ((state.routingSelectRules || []).length + 1) * 10;
|
|
|
|
|
+ document.getElementById('modal-rf-sr-rule').value = 'if (protocol bgp && dst in 10.0.0.0/8) { accept; }';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-comment').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-disabled').checked = false;
|
|
|
|
|
+ document.getElementById('modal-rf-select-rule').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditSelectRuleModal(id) {
|
|
|
|
|
+ const r = (state.routingSelectRules || []).find(item => item.id === id);
|
|
|
|
|
+ if (!r) return;
|
|
|
|
|
+ document.getElementById('modal-rf-sr-id').value = r.id;
|
|
|
|
|
+ document.getElementById('modal-rf-sr-title').innerText = 'Edit Routing Select Rule';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-chain').value = r.chain || '';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-order').value = r.rule_order || 10;
|
|
|
|
|
+ document.getElementById('modal-rf-sr-rule').value = r.rule || '';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-comment').value = r.comment || '';
|
|
|
|
|
+ document.getElementById('modal-rf-sr-disabled').checked = !!r.disabled;
|
|
|
|
|
+ document.getElementById('modal-rf-select-rule').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeSelectRuleModal() {
|
|
|
|
|
+ document.getElementById('modal-rf-select-rule').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitSelectRule(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id: document.getElementById('modal-rf-sr-id').value,
|
|
|
|
|
+ chain: document.getElementById('modal-rf-sr-chain').value.trim(),
|
|
|
|
|
+ rule_order: parseInt(document.getElementById('modal-rf-sr-order').value, 10) || 10,
|
|
|
|
|
+ rule: document.getElementById('modal-rf-sr-rule').value.trim(),
|
|
|
|
|
+ comment: document.getElementById('modal-rf-sr-comment').value.trim(),
|
|
|
|
|
+ disabled: document.getElementById('modal-rf-sr-disabled').checked
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/select-rules`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ closeSelectRuleModal();
|
|
|
|
|
+ showToast('Success', 'Select rule saved', 'success');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to save select rule', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteSelectRule(id) {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this select rule?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/select-rules/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'Select rule removed', 'info');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to delete select rule', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleSelectRuleState(id, disabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/select-rules/${encodeURIComponent(id)}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Status Updated', `Select rule ${disabled ? 'disabled' : 'enabled'}`, 'info');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to update select rule state', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// MODAL CRUD: COMMUNITY LISTS (/routing filter community-list)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function openAddCommunityListModal() {
|
|
|
|
|
+ document.getElementById('modal-rf-cl-id').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-title').innerText = 'Add BGP Community List';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-name').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-communities').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-regexp').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-comment').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-disabled').checked = false;
|
|
|
|
|
+ document.getElementById('modal-rf-community-list').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditCommunityListModal(id) {
|
|
|
|
|
+ const cl = (state.routingCommunityLists || []).find(item => item.id === id);
|
|
|
|
|
+ if (!cl) return;
|
|
|
|
|
+ document.getElementById('modal-rf-cl-id').value = cl.id;
|
|
|
|
|
+ document.getElementById('modal-rf-cl-title').innerText = 'Edit BGP Community List';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-name').value = cl.name || '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-communities').value = (cl.communities || []).join(', ');
|
|
|
|
|
+ document.getElementById('modal-rf-cl-regexp').value = cl.regexp || '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-comment').value = cl.comment || '';
|
|
|
|
|
+ document.getElementById('modal-rf-cl-disabled').checked = !!cl.disabled;
|
|
|
|
|
+ document.getElementById('modal-rf-community-list').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeCommunityListModal() {
|
|
|
|
|
+ document.getElementById('modal-rf-community-list').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitCommunityList(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const rawComms = document.getElementById('modal-rf-cl-communities').value;
|
|
|
|
|
+ const comms = rawComms.split(',').map(c => c.trim()).filter(Boolean);
|
|
|
|
|
+
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id: document.getElementById('modal-rf-cl-id').value,
|
|
|
|
|
+ name: document.getElementById('modal-rf-cl-name').value.trim(),
|
|
|
|
|
+ communities: comms,
|
|
|
|
|
+ regexp: document.getElementById('modal-rf-cl-regexp').value.trim(),
|
|
|
|
|
+ comment: document.getElementById('modal-rf-cl-comment').value.trim(),
|
|
|
|
|
+ disabled: document.getElementById('modal-rf-cl-disabled').checked
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/community-lists`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ closeCommunityListModal();
|
|
|
|
|
+ showToast('Success', 'Community list saved', 'success');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to save community list', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteCommunityList(id) {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this community list?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/community-lists/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'Community list removed', 'info');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to delete community list', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleCommunityListState(id, disabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/community-lists/${encodeURIComponent(id)}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Status Updated', `Community list ${disabled ? 'disabled' : 'enabled'}`, 'info');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to update community list state', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+// MODAL CRUD: AS LISTS (/routing filter num-list)
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+function openAddASListModal() {
|
|
|
|
|
+ document.getElementById('modal-rf-al-id').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-title').innerText = 'Add AS Number List';
|
|
|
|
|
+ document.getElementById('modal-rf-al-name').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-range').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-comment').value = '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-disabled').checked = false;
|
|
|
|
|
+ document.getElementById('modal-rf-as-list').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function openEditASListModal(id) {
|
|
|
|
|
+ const al = (state.routingASLists || []).find(item => item.id === id);
|
|
|
|
|
+ if (!al) return;
|
|
|
|
|
+ document.getElementById('modal-rf-al-id').value = al.id;
|
|
|
|
|
+ document.getElementById('modal-rf-al-title').innerText = 'Edit AS Number List';
|
|
|
|
|
+ document.getElementById('modal-rf-al-name').value = al.name || '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-range').value = al.range || '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-comment').value = al.comment || '';
|
|
|
|
|
+ document.getElementById('modal-rf-al-disabled').checked = !!al.disabled;
|
|
|
|
|
+ document.getElementById('modal-rf-as-list').classList.remove('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function closeASListModal() {
|
|
|
|
|
+ document.getElementById('modal-rf-as-list').classList.add('hidden');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function submitASList(e) {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ id: document.getElementById('modal-rf-al-id').value,
|
|
|
|
|
+ name: document.getElementById('modal-rf-al-name').value.trim(),
|
|
|
|
|
+ range: document.getElementById('modal-rf-al-range').value.trim(),
|
|
|
|
|
+ comment: document.getElementById('modal-rf-al-comment').value.trim(),
|
|
|
|
|
+ disabled: document.getElementById('modal-rf-al-disabled').checked
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/as-lists`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify(payload)
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ closeASListModal();
|
|
|
|
|
+ showToast('Success', 'AS number list saved', 'success');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to save AS list', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteASList(id) {
|
|
|
|
|
+ if (!confirm('Are you sure you want to delete this AS list?')) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/as-lists/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Deleted', 'AS list removed', 'info');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to delete AS list', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleASListState(id, disabled) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/as-lists/${encodeURIComponent(id)}/state`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.ok) {
|
|
|
|
|
+ showToast('Status Updated', `AS list ${disabled ? 'disabled' : 'enabled'}`, 'info');
|
|
|
|
|
+ loadDynamicRouting();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const err = await res.json();
|
|
|
|
|
+ showToast('Error', err.error || 'Failed to update AS list state', 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ==========================================
|
|
// ==========================================
|
|
|
// TROUBLESHOOT & DIAGNOSTIC TOOLS (/tool)
|
|
// TROUBLESHOOT & DIAGNOSTIC TOOLS (/tool)
|
|
|
// ==========================================
|
|
// ==========================================
|