From 93eb42226d6fb0a11e6c4b6c049c40f75ae33474 Mon Sep 17 00:00:00 2001 From: swpa Date: Mon, 4 May 2026 18:26:12 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=A8=B9=E7=8B=80=E7=B5=90?= =?UTF-8?q?=E6=A7=8B=E6=8E=92=E7=89=88=E4=BF=AE=E6=AD=A3=20&=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E9=82=8F=E5=AF=AB=E5=85=A5=E9=82=8F=E8=BC=AF=E5=84=AA?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 46 +++++- routers/config.py | 103 +++++++++++- routers/lock.py | 4 +- static/app.js | 374 +++++++++++++++++++++++++++++++++++++------ static/style.css | 91 +++++++++++ system_settings.json | 371 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 933 insertions(+), 56 deletions(-) diff --git a/index.html b/index.html index 6564bba..1b7628a 100644 --- a/index.html +++ b/index.html @@ -250,22 +250,54 @@ diff --git a/routers/config.py b/routers/config.py index 1545c8c..ab06af3 100644 --- a/routers/config.py +++ b/routers/config.py @@ -6,6 +6,7 @@ from typing import List from netmiko import ConnectHandler # 💡 確保從 shared 引入 deep_split_tree from shared import CMTS_DEVICE, cmts_config_lock, parse_cli_to_tree, deep_split_tree, load_settings, save_settings +from collections import defaultdict router = APIRouter() @@ -15,6 +16,15 @@ class ConfigRequest(BaseModel): username: str password: str +# 定義前端傳來的 Diff 資料結構 +class DiffItem(BaseModel): + path: str # 例如: "cable.ds-rf-port.1:9/0.down-channel.0.frequency" + old_val: str + new_val: str + +class GenerateCliRequest(BaseModel): + diffs: List[DiffItem] + @router.post("/cmts-config") async def execute_cmts_config(req: ConfigRequest): # 使用非同步鎖,確保同一時間只有一個寫入任務執行 @@ -27,18 +37,107 @@ async def execute_cmts_config(req: ConfigRequest): commands = [cmd.strip() for cmd in req.script.splitlines() if cmd.strip() and not cmd.strip().startswith('!')] net_connect = ConnectHandler(**device) + # 🌟 1. 手動進入設定模式 (恢復您原本正確的寫法) net_connect.send_command_timing("config") - # 批次送出設定指令 - output = net_connect.send_config_set(commands, cmd_verify=False) + # 🌟 2. 批次送出設定指令 + # 加上 enter_config_mode=False 與 exit_config_mode=False + # 這樣 Netmiko 就不會自作主張去打 config terminal 了 + output = net_connect.send_config_set( + commands, + read_timeout=60, + cmd_verify=False, + enter_config_mode=False, + exit_config_mode=False + ) + # 🌟 3. 手動退出設定模式 net_connect.send_command_timing("exit") net_connect.disconnect() + + # 🌟 新增:檢查設備回傳的文字中是否包含拒絕或錯誤的關鍵字 + lower_output = output.lower() + if "aborted" in lower_output or "error" in lower_output or "invalid" in lower_output: + # 回傳錯誤狀態,前端就會顯示紅色字體且「不會」自動關閉視窗 + return { + "status": "error", + "message": "CMTS 拒絕了設定 (Commit Aborted)", + "detail": output + } return {"status": "success", "data": output} except Exception as e: raise HTTPException(status_code=500, detail=f"CMTS Config Error: {str(e)}") + +@router.post("/cmts-generate-cli") +async def generate_cli(req: GenerateCliRequest): + """ + 將前端的 JSON Diff 轉譯為 Harmonic 單行 CLI 腳本 + 並自動處理 admin-state 的安全生命週期 (先 down 後 up) + """ + # 用來將同一個介面的變更群組化 + # 結構: { "cable ds-rf-port 1:9/0 down-channel 0": [ ("frequency", "350000000"), ... ] } + interface_groups = defaultdict(list) + + # 用來記錄該介面最終的 admin-state 應該是什麼 (預設為不變,若有改動則更新) + interface_admin_states = {} + for diff in req.diffs: + # 將路徑切分:例如 ['cable', 'ds-rf-port', '1:9/0', 'down-channel', '0', 'frequency'] + parts = diff.path.split('::') + + # 由於 Harmonic 的樹狀圖結構與 CLI 結構高度一致 + # 我們可以假設「最後一個元素」是參數名稱,「前面的所有元素」組合起來就是介面路徑 + param_name = parts[-1] + interface_path = " ".join(parts[:-1]) # 例如: "cable ds-rf-port 1:9/0 down-channel 0" + + if param_name == "admin-state": + # 如果使用者直接修改了 admin-state,我們記錄下來,作為最後的狀態 + interface_admin_states[interface_path] = diff.new_val + else: + # 一般參數變更,加入群組 + interface_groups[interface_path].append((param_name, diff.new_val)) + + # 開始組裝最終的 CLI 腳本 + cli_lines = [] + + # 為了視覺清晰,我們可以加上註解 (您的執行 API 已經會自動過濾 ! 開頭的行) + cli_lines.append("! --- Auto-Generated Configuration Script ---") + + for interface, changes in interface_groups.items(): + cli_lines.append(f"! Configuring: {interface}") + + # 1. 安全機制:先強制將介面 admin-state down + cli_lines.append(f"{interface} admin-state down") + + # 2. 寫入所有被修改的參數 + for param, new_val in changes: + if new_val == "": + # 處理清空數值的情況 (通常是加上 no) + cli_lines.append(f"no {interface} {param}") + else: + cli_lines.append(f"{interface} {param} {new_val}") + + # 3. 恢復狀態:如果使用者有特別設定 admin-state 就用使用者的,否則預設恢復為 up + final_state = interface_admin_states.get(interface, "up") + cli_lines.append(f"{interface} admin-state {final_state}") + # 🌟 新增:Harmonic 必須要有 commit 才會生效 + cli_lines.append("commit") + cli_lines.append("!") # 空行分隔 + + # 將獨立修改 admin-state (沒有修改其他參數) 的情況也補上 + for interface, state in interface_admin_states.items(): + if interface not in interface_groups: + cli_lines.append(f"! Configuring state only: {interface}") + cli_lines.append(f"{interface} admin-state {state}") + # 🌟 新增:Harmonic 必須要有 commit 才會生效 + cli_lines.append("commit") + cli_lines.append("!") + + # 將陣列組合成字串回傳 + final_script = "\n".join(cli_lines) + + return {"status": "success", "data": final_script} # ========================================== # 💡 以下為 DS RF Port 動態樹狀查詢 API diff --git a/routers/lock.py b/routers/lock.py index 174fe0b..986cd81 100644 --- a/routers/lock.py +++ b/routers/lock.py @@ -45,10 +45,10 @@ def is_path_locked_by_others(target_path: str, user_id: str) -> tuple[Optional[d if target_path == locked_path: return lock_info, f"此區塊正由 [{lock_info['username']}] 編輯中" - if target_path.startswith(locked_path + "."): + if target_path.startswith(locked_path + "::"): return lock_info, f"父層級 [{locked_path}] 已被 [{lock_info['username']}] 鎖定,無法編輯子區塊" - if locked_path.startswith(target_path + "."): + if locked_path.startswith(target_path + "::"): return lock_info, f"子層級 [{locked_path}] 已被 [{lock_info['username']}] 鎖定,無法鎖定整個父區塊" return None, "" diff --git a/static/app.js b/static/app.js index 22cf44c..2cb24d0 100644 --- a/static/app.js +++ b/static/app.js @@ -7,6 +7,11 @@ let term, fitAddon, ws; let currentMacDomainData = null; let isConnected = false; +// 💡 新增:追蹤當前編輯狀態 +let currentEditPath = null; +let currentEditElementId = null; +let currentEditIsSuccess = false; + window.onload = initTerminal; window.onresize = () => { if (fitAddon) fitAddon.fit(); }; @@ -14,7 +19,7 @@ window.onresize = () => { if (fitAddon) fitAddon.fit(); }; // 💡 全域狀態與鎖定管理 (Lock Management) // ========================================== const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9); -const ACTIVE_HEARTBEATS = {}; // 記錄正在打心跳的計時器: { "path": intervalId } +const ACTIVE_HEARTBEATS = {}; async function sendHeartbeat(path) { try { @@ -26,6 +31,24 @@ async function sendHeartbeat(path) { } catch (error) {} } +// 🎨 輔助函數:解析終端機輸出,僅將錯誤/警告行標紅 +function highlightTerminalOutput(text) { + if (!text) return ''; + + // 將多行字串拆分成陣列,逐行檢查 + const lines = text.split('\n'); + const formattedLines = lines.map(line => { + // 偵測常見的錯誤與警告關鍵字 (不分大小寫) + // 您可以隨時在這裡擴充設備專屬的錯誤代碼,例如 '% Invalid' + if (line.match(/(Aborted|Error|Warning|Failed|% Invalid|% Unknown|Bad command)/i)) { + return `${line}`; + } + return line; // 沒有關鍵字的行,維持預設顏色 (白色) + }); + + return formattedLines.join('\n'); +} + // ========================================== // 💡 資料夾層級的編輯模式邏輯 (Node-Level Lock) // ========================================== @@ -43,20 +66,17 @@ async function startEditFolder(path, elementId) { const result = await response.json(); if (response.status === 409) { - alert(`⚠️ ${result.detail}`); // 後端擋下衝突時顯示警告 + alert(`⚠️ ${result.detail}`); return; } if (result.status === 'success') { - // 啟動心跳 ACTIVE_HEARTBEATS[path] = setInterval(() => sendHeartbeat(path), 15000); - // UI 切換 document.getElementById(`edit-btn-${elementId}`).style.display = 'none'; document.getElementById(`actions-${elementId}`).style.display = 'inline-block'; document.getElementById(`details-${elementId}`).open = true; - // 轉為輸入框 const contentDiv = document.getElementById(`content-${elementId}`); const leafContainers = contentDiv.querySelectorAll('.leaf-container'); @@ -102,40 +122,259 @@ async function cancelEditFolder(path, elementId) { }); } -function previewFolderCLI(path, elementId) { +// 🚀 Phase 3: 收集修改並呼叫後端轉譯 API +async function previewFolderCLI(path, elementId) { + // 💡 記錄當前正在編輯的節點資訊 + currentEditPath = path; + currentEditElementId = elementId; + currentEditIsSuccess = false; // 預設為尚未成功 const contentDiv = document.getElementById(`content-${elementId}`); + if (!contentDiv) return; + const inputs = contentDiv.querySelectorAll('.edit-input'); - - let changes = []; - - // 掃描所有輸入框,找出被修改過的值 + const diffs = []; + inputs.forEach(input => { - const nodePath = input.getAttribute('data-path'); - const newVal = input.value; - const origVal = input.parentElement.getAttribute('data-original'); + const container = input.closest('.leaf-container'); + if (!container) return; + + const originalVal = container.getAttribute('data-original') || ""; + const newVal = input.value.trim(); + const nodePath = container.getAttribute('data-path'); - if (newVal !== origVal) { - changes.push(`- ${nodePath}:\n [舊] ${origVal} ➡️ [新] ${newVal}`); + if (originalVal !== newVal && nodePath) { + diffs.push({ + path: nodePath, + old_val: originalVal, + new_val: newVal + }); } }); - - if (changes.length === 0) { - alert("您沒有修改任何數值!"); + + if (diffs.length === 0) { + alert("沒有偵測到任何修改,無需生成指令!"); return; } + + try { + const response = await fetch('/api/v1/cmts-generate-cli', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ diffs: diffs }) + }); + + const result = await response.json(); + + if (result.status === 'success') { + showCliPreviewModal(result.data); + } else { + alert("CLI 生成失敗: " + result.message); + } + } catch (error) { + console.error("Error generating CLI:", error); + alert("網路連線錯誤,無法生成 CLI"); + } +} + +// 🚀 Phase 3: 顯示側邊預覽區塊 (初始化為「預覽模式」) +function showCliPreviewModal(cliScript) { + const leftPane = document.getElementById('tree-container'); + const resizer = document.getElementById('drag-resizer'); + const rightPane = document.getElementById('side-cli-preview'); - alert(`【Phase 3 預覽】\n鎖定區塊: ${path}\n\n偵測到的修改:\n${changes.join('\n\n')}\n\n即將實作轉譯為 CMTS CLI 的功能!`); + // --- UI 狀態重置為「預覽模式」 --- + document.getElementById('side-pane-title').innerHTML = '⚠️ 即將寫入的指令'; + document.getElementById('side-pane-title').style.color = '#f1c40f'; + + document.getElementById('btn-side-cancel').style.display = 'inline-block'; + document.getElementById('btn-side-confirm').style.display = 'inline-block'; + document.getElementById('btn-side-close').style.display = 'none'; + + document.getElementById('side-cli-textarea').style.display = 'block'; + document.getElementById('side-execution-result').style.display = 'none'; + + // 填入指令 + document.getElementById('side-cli-textarea').value = cliScript; + + // 設定初始 50/50 比例 + leftPane.style.width = 'calc(50% - 11px)'; + rightPane.style.width = 'calc(50% - 11px)'; + + resizer.style.display = 'flex'; + rightPane.style.display = 'block'; + + if (!window.isResizerInitialized) { + initResizer(); + window.isResizerInitialized = true; + } +} + +// ❌ 隱藏側邊預覽區塊 (恢復左側 100% 寬度) +function hideSideCLI() { + document.getElementById('tree-container').style.width = '100%'; + document.getElementById('drag-resizer').style.display = 'none'; + document.getElementById('side-cli-preview').style.display = 'none'; + + // 💡 判斷是否需要更新左側樹狀圖 + if (currentEditIsSuccess && currentEditElementId && currentEditPath) { + applyEditFolder(currentEditPath, currentEditElementId); + } + + // 重置狀態,等待下一次編輯 + currentEditPath = null; + currentEditElementId = null; + currentEditIsSuccess = false; +} + +// 💡 新增:套用修改並解除鎖定 +async function applyEditFolder(path, elementId) { + const contentDiv = document.getElementById(`content-${elementId}`); + if (contentDiv) { + const inputs = contentDiv.querySelectorAll('.edit-input'); + inputs.forEach(input => { + const container = input.closest('.leaf-container'); + if (container) { + const newVal = input.value.trim(); + // 關鍵:將新值寫入 data-original + // 這樣稍後呼叫 cancelEditFolder 時,就會用這個新值來還原畫面 + container.setAttribute('data-original', newVal); + } + }); + } + + // 呼叫現有的 cancelEditFolder 來解除後端鎖定,並將輸入框變回純文字 + await cancelEditFolder(path, elementId); +} + +// 🚀 執行側邊區塊的指令 (切換為「執行模式」) +function executeSideCLI() { + const finalScript = document.getElementById('side-cli-textarea').value; + + // --- UI 狀態切換為「執行結果模式」 --- + document.getElementById('side-pane-title').innerHTML = '⏳ 正在寫入設備...'; + document.getElementById('side-pane-title').style.color = '#3498db'; + + document.getElementById('btn-side-cancel').style.display = 'none'; + document.getElementById('btn-side-confirm').style.display = 'none'; + document.getElementById('btn-side-close').style.display = 'none'; + + document.getElementById('side-cli-textarea').style.display = 'none'; + const resultBox = document.getElementById('side-execution-result'); + resultBox.style.display = 'block'; + + // 💡 移除原本多餘的提示,直接以純白色顯示正在執行的腳本 + resultBox.innerHTML = `${finalScript}`; + + // 呼叫 API + executeGeneratedCLI(finalScript); +} + +// 🖱️ 實作左右拖曳調整寬度引擎 +function initResizer() { + const resizer = document.getElementById('drag-resizer'); + const leftPane = document.getElementById('tree-container'); + const rightPane = document.getElementById('side-cli-preview'); + const container = document.getElementById('split-container'); + + let isResizing = false; + + // 滑鼠按下分隔線:開始拖曳 + resizer.addEventListener('mousedown', (e) => { + isResizing = true; + document.body.style.cursor = 'col-resize'; + document.body.style.userSelect = 'none'; // 防止拖曳時意外反白文字 + }); + + // 滑鼠移動:動態計算寬度 + document.addEventListener('mousemove', (e) => { + if (!isResizing) return; + + const containerRect = container.getBoundingClientRect(); + let newLeftWidth = e.clientX - containerRect.left; + + // 限制左右面板的最小/最大寬度 (20% ~ 80%),防止拉過頭 + const minWidth = containerRect.width * 0.2; + const maxWidth = containerRect.width * 0.8; + if (newLeftWidth < minWidth) newLeftWidth = minWidth; + if (newLeftWidth > maxWidth) newLeftWidth = maxWidth; + + // 轉換為百分比 + const leftPercentage = (newLeftWidth / containerRect.width) * 100; + const rightPercentage = 100 - leftPercentage; + + // 即時更新左右面板寬度 + leftPane.style.width = `calc(${leftPercentage}% - 11px)`; + rightPane.style.width = `calc(${rightPercentage}% - 11px)`; + }); + + // 滑鼠放開:結束拖曳 + document.addEventListener('mouseup', () => { + if (isResizing) { + isResizing = false; + document.body.style.cursor = 'default'; + document.body.style.userSelect = 'auto'; // 恢復文字反白功能 + } + }); +} + +// 🚀 Phase 3: 將腳本發送給設備執行 (輸出至側邊欄) +async function executeGeneratedCLI(script) { + const connInfo = getGlobalConnectionInfo(); + if (!connInfo) return; + + const outputEl = document.getElementById('side-execution-result'); + + try { + const response = await fetch('/api/v1/cmts-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + script: script, + host: connInfo.host, + username: connInfo.user, + password: connInfo.pass + }) + }); + + const result = await response.json(); + + // 執行完畢,顯示「完成並關閉」按鈕 + document.getElementById('btn-side-close').style.display = 'inline-block'; + + if (result.status === 'success') { + currentEditIsSuccess = true; // 💡 標記執行成功 + + document.getElementById('side-pane-title').innerHTML = '✅ 執行成功'; + document.getElementById('side-pane-title').style.color = '#2ecc71'; + + // 💡 成功時,移除內部提示,直接顯示純淨的白色 Log + outputEl.innerHTML = `${result.data}`; + } else { + currentEditIsSuccess = false; // 💡 標記執行失敗 + + document.getElementById('side-pane-title').innerHTML = '❌ 執行失敗'; + document.getElementById('side-pane-title').style.color = '#ff6b6b'; + + // 💡 失敗時,移除內部提示,並透過輔助函數「僅針對錯誤行」進行紅色高亮 + const rawLog = result.detail || result.message || ''; + outputEl.innerHTML = `${highlightTerminalOutput(rawLog)}`; + } + } catch (error) { + document.getElementById('btn-side-close').style.display = 'inline-block'; + document.getElementById('side-pane-title').innerHTML = '❌ 連線錯誤'; + document.getElementById('side-pane-title').style.color = '#ff6b6b'; + + outputEl.innerHTML = `連線或伺服器錯誤: ${error}`; + } } // ========================================== -// 💡 編輯模式 UI 切換邏輯 +// 💡 編輯模式 UI 切換邏輯 (保留舊版單一節點編輯用) // ========================================== function renderEditModeUI(path, currentValue, elementId) { const container = document.getElementById(elementId); if (!container) return; - // 將原本的文字替換為 Input 與操作按鈕 - // 注意:這裡將單引號跳脫,避免 HTML 屬性解析錯誤 const safeValue = currentValue.replace(/'/g, "'"); container.innerHTML = ` @@ -158,7 +397,6 @@ function restoreViewModeUI(path, originalValue, elementId) { const container = document.getElementById(elementId); if (!container) return; - // 恢復成原本的文字與編輯按鈕 const safeValue = originalValue.replace(/'/g, "'"); container.innerHTML = ` ${originalValue} @@ -168,7 +406,6 @@ function restoreViewModeUI(path, originalValue, elementId) { `; } -// Phase 3 的預留函數:預覽 CLI 指令 function previewCLI(path, elementId) { const newValue = document.getElementById(`input-${elementId}`).value; alert(`【Phase 3 預覽】\n路徑: ${path}\n新值: ${newValue}\n\n即將實作轉譯為 CMTS CLI 的功能!`); @@ -208,21 +445,17 @@ function switchConfigTask() { } } -// 💡 合併修復:統一處理「載入任務」的邏輯與防呆 function startConfigTask() { - // 防呆 1:檢查是否已連線 if (!isConnected) { alert("請先點擊畫面上方的「連線至 CMTS」成功後,再執行載入任務!"); return; } - // 先確保對應的表單有顯示出來 switchConfigTask(); const selectedTaskId = document.getElementById('configTask').value; if (selectedTaskId === 'form-bonding-config') { - // 任務 A:MAC Domain 配置精靈 if (currentMacDomainData && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) { return; } @@ -230,8 +463,6 @@ function startConfigTask() { loadMacDomainList(); } else if (selectedTaskId === 'form-full-config') { - // 💡 修復:任務 B:完整設備配置樹狀圖 - // 當使用者點擊「載入任務」時,直接幫他觸發抓取樹狀圖的 API fetchFullConfig(); } } @@ -758,16 +989,25 @@ function buildTree(data, parentPath = "") { let html = '
'; for (const key in data) { const value = data[key]; - const currentPath = parentPath ? `${parentPath}.${key}` : key; + const currentPath = parentPath ? `${parentPath}::${key}` : key; if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) { // 📁 渲染資料夾 (加入編輯與操作按鈕) const elementId = `folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; html += ` -
- - 📁 ${key} + +
+ + + + + + + + + + ${key} `; + let html = `
`; for (const key in data) { const value = data[key]; - // 💡 組合絕對路徑,例如 "cable.mac-domain" - const currentPath = parentPath ? `${parentPath}.${key}` : key; + const currentPath = parentPath ? `${parentPath}::${key}` : key; const isChecked = hiddenKeys.includes(currentPath) ? "checked" : ""; if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) { html += ` -
- - - 📁 ${key} +
+ + + + + + + ${key} - ${buildRealFilterTree(value, currentPath, hiddenKeys)} + + +
+ ${buildRealFilterTree(value, currentPath, hiddenKeys)} +
`; } else { + const safeValue = (value === null ? '' : value).toString().replace(/"/g, """); html += `
- - 📄 ${key}: ${value === null ? '' : value} + + 📄 ${key}: + ${safeValue}
`; } @@ -926,6 +1177,39 @@ function buildRealFilterTree(data, parentPath, hiddenKeys) { return html; } +// 🔽 當點擊「父節點」時:同步勾選/取消所有子節點 +function toggleChildCheckboxes(parentCheckbox) { + const details = parentCheckbox.closest('details'); + if (details) { + // 找到該資料夾內所有的子 Checkbox + const childCheckboxes = details.querySelectorAll('.filter-children-container .filter-checkbox'); + childCheckboxes.forEach(cb => { + cb.checked = parentCheckbox.checked; + }); + } + // 往上檢查是否需要更新更上層的父節點 + updateParentCheckboxState(parentCheckbox); +} + +// 🔼 當點擊「子節點」時:往上檢查父節點是否該被勾選 +function updateParentCheckboxState(element) { + // 找到包住這個元素的上一層 details + const parentDetails = element.closest('.filter-children-container')?.closest('details'); + if (!parentDetails) return; + + const parentCheckbox = parentDetails.querySelector('summary .filter-checkbox'); + const childCheckboxes = parentDetails.querySelectorAll('.filter-children-container .filter-checkbox'); + + if (parentCheckbox && childCheckboxes.length > 0) { + // 如果所有的子節點都被勾選了,父節點就自動打勾;只要有一個沒勾,父節點就取消打勾 + const allChecked = Array.from(childCheckboxes).every(cb => cb.checked); + parentCheckbox.checked = allChecked; + + // 遞迴往上檢查,確保多層級的資料夾也能正確連動 + updateParentCheckboxState(parentCheckbox); + } +} + async function saveSystemSettings() { const checkboxes = document.querySelectorAll('.filter-checkbox:checked'); const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value); @@ -962,4 +1246,4 @@ function closeModal(event) { if (event && event.target !== event.currentTarget && event.target.className !== 'modal-close') return; document.getElementById('outputModal').classList.remove('active'); document.body.style.overflow = ''; -} \ No newline at end of file +} diff --git a/static/style.css b/static/style.css index dd1f0d6..15de630 100644 --- a/static/style.css +++ b/static/style.css @@ -54,4 +54,95 @@ button:hover { background-color: #3498db; } .modal-body { flex-grow: 1; padding: 20px; overflow: auto; background-color: #1e1e1e; } .readonly-terminal { margin: 0; color: #e0e0e0; font-family: 'Courier New', Courier, monospace; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; } +/* 樹狀圖節點的容器樣式 */ +.tree-node-header { + display: flex; + align-items: center; + cursor: pointer; + padding: 6px 8px; + user-select: none; + color: #2c3e50; /* 保持原本的深色文字 */ + transition: background-color 0.2s ease; + border-radius: 4px; +} + +/* 💡 輕量化懸停效果:改用極淺的灰藍色 */ +.tree-node-header:hover { + background-color: #eef2f5; /* 非常柔和的背景色 */ + /* 這裡不需要再改變文字顏色了,深色字在淺色背景上非常清晰 */ +} + +/* 1. 旋轉小箭頭 (Chevron) */ +.tree-chevron { + width: 16px; + height: 16px; + margin-right: 6px; + /* 內建灰色箭頭 SVG */ + background-image: url("data:image/svg+xml;utf8,"); + background-size: contain; + background-repeat: no-repeat; + transition: transform 0.2s ease-in-out; /* 💡 旋轉動畫核心 */ +} + +/* 展開時:箭頭向下轉 90 度 */ +.tree-node-header.is-open .tree-chevron { + transform: rotate(90deg); +} + +/* 2. 資料夾圖示 (預設為關閉狀態) */ +.tree-folder-icon { + width: 18px; + height: 18px; + margin-right: 8px; + /* 內建金色關閉資料夾 SVG */ + background-image: url("data:image/svg+xml;utf8,"); + background-size: contain; + background-repeat: no-repeat; +} + +/* 展開時:切換為開啟的資料夾 */ +.tree-node-header.is-open .tree-folder-icon { + /* 內建金色開啟資料夾 SVG */ + background-image: url("data:image/svg+xml;utf8,"); +} + +/* 隱藏原生 Checkbox,改為自訂的過濾器樣式 */ +.filter-checkbox { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + border: 2px solid #bdc3c7; + border-radius: 4px; + background-color: #fff; + cursor: pointer; + position: relative; + display: inline-block; + vertical-align: middle; + margin-right: 8px; + transition: all 0.2s ease; +} + +/* 懸停時提示顏色 */ +.filter-checkbox:hover { + border-color: #e74c3c; +} + +/* 勾選時背景變紅,並顯示 ✖ */ +.filter-checkbox:checked { + background-color: #e74c3c; + border-color: #e74c3c; +} + +.filter-checkbox:checked::after { + content: '✖'; + color: white; + font-size: 11px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-weight: bold; +} + @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } diff --git a/system_settings.json b/system_settings.json index abef151..a6d6221 100644 --- a/system_settings.json +++ b/system_settings.json @@ -4,15 +4,386 @@ "ssh", "hostname", "logging", + "logging::evt", + "logging::evt::1024", + "logging::evt::1024::description", + "logging::evt::1024::name", + "logging::evt::1024::priority", + "logging::evt::1024::active", + "logging::evt::1024::destination", + "logging::evt::1026", + "logging::evt::1026::description", + "logging::evt::1026::name", + "logging::evt::1026::priority", + "logging::evt::1026::active", + "logging::evt::1026::destination", + "logging::evt::1027", + "logging::evt::1027::description", + "logging::evt::1027::name", + "logging::evt::1027::priority", + "logging::evt::1027::active", + "logging::evt::1027::destination", + "logging::evt::1028", + "logging::evt::1028::description", + "logging::evt::1028::name", + "logging::evt::1028::priority", + "logging::evt::1028::active", + "logging::evt::1028::destination", + "logging::evt::1029", + "logging::evt::1029::description", + "logging::evt::1029::name", + "logging::evt::1029::priority", + "logging::evt::1029::active", + "logging::evt::1029::destination", + "logging::evt::1030", + "logging::evt::1030::description", + "logging::evt::1030::name", + "logging::evt::1030::priority", + "logging::evt::1030::active", + "logging::evt::1030::destination", + "logging::evt::1031", + "logging::evt::1031::description", + "logging::evt::1031::name", + "logging::evt::1031::priority", + "logging::evt::1031::active", + "logging::evt::1031::destination", + "logging::evt::1032", + "logging::evt::1032::description", + "logging::evt::1032::name", + "logging::evt::1032::priority", + "logging::evt::1032::active", + "logging::evt::1032::destination", + "logging::evt::1033", + "logging::evt::1033::description", + "logging::evt::1033::name", + "logging::evt::1033::priority", + "logging::evt::1033::active", + "logging::evt::1033::destination", + "logging::evt::1034", + "logging::evt::1034::description", + "logging::evt::1034::name", + "logging::evt::1034::priority", + "logging::evt::1034::active", + "logging::evt::1034::destination", + "logging::evt::1035", + "logging::evt::1035::description", + "logging::evt::1035::name", + "logging::evt::1035::priority", + "logging::evt::1035::active", + "logging::evt::1035::destination", + "logging::evt::1043", + "logging::evt::1043::description", + "logging::evt::1043::name", + "logging::evt::1043::priority", + "logging::evt::1043::active", + "logging::evt::1043::destination", + "logging::evt::1044", + "logging::evt::1044::description", + "logging::evt::1044::name", + "logging::evt::1044::priority", + "logging::evt::1044::active", + "logging::evt::1044::destination", + "logging::evt::1045", + "logging::evt::1045::description", + "logging::evt::1045::name", + "logging::evt::1045::priority", + "logging::evt::1045::active", + "logging::evt::1045::destination", + "logging::evt::1046", + "logging::evt::1046::description", + "logging::evt::1046::name", + "logging::evt::1046::priority", + "logging::evt::1046::active", + "logging::evt::1046::destination", + "logging::evt::1047", + "logging::evt::1047::description", + "logging::evt::1047::name", + "logging::evt::1047::priority", + "logging::evt::1047::active", + "logging::evt::1047::destination", + "logging::evt::1060", + "logging::evt::1060::description", + "logging::evt::1060::name", + "logging::evt::1060::priority", + "logging::evt::1060::active", + "logging::evt::1060::destination", + "logging::evt::1061", + "logging::evt::1061::description", + "logging::evt::1061::name", + "logging::evt::1061::priority", + "logging::evt::1061::active", + "logging::evt::1061::destination", + "logging::evt::1062", + "logging::evt::1062::description", + "logging::evt::1062::name", + "logging::evt::1062::priority", + "logging::evt::1062::active", + "logging::evt::1062::destination", + "logging::evt::1063", + "logging::evt::1063::description", + "logging::evt::1063::name", + "logging::evt::1063::priority", + "logging::evt::1063::active", + "logging::evt::1063::destination", + "logging::evt::1064", + "logging::evt::1064::description", + "logging::evt::1064::name", + "logging::evt::1064::priority", + "logging::evt::1064::active", + "logging::evt::1064::destination", + "logging::evt::1065", + "logging::evt::1065::description", + "logging::evt::1065::name", + "logging::evt::1065::priority", + "logging::evt::1065::active", + "logging::evt::1065::destination", + "logging::evt::1066", + "logging::evt::1066::description", + "logging::evt::1066::name", + "logging::evt::1066::priority", + "logging::evt::1066::active", + "logging::evt::1066::destination", + "logging::evt::1067", + "logging::evt::1067::description", + "logging::evt::1067::name", + "logging::evt::1067::priority", + "logging::evt::1067::active", + "logging::evt::1067::destination", + "logging::evt::1068", + "logging::evt::1068::description", + "logging::evt::1068::name", + "logging::evt::1068::priority", + "logging::evt::1068::active", + "logging::evt::1068::destination", + "logging::evt::1069", + "logging::evt::1069::description", + "logging::evt::1069::name", + "logging::evt::1069::priority", + "logging::evt::1069::active", + "logging::evt::1069::destination", + "logging::evt::1070", + "logging::evt::1070::description", + "logging::evt::1070::name", + "logging::evt::1070::priority", + "logging::evt::1070::active", + "logging::evt::1070::destination", + "logging::evt::1071", + "logging::evt::1071::description", + "logging::evt::1071::name", + "logging::evt::1071::priority", + "logging::evt::1071::active", + "logging::evt::1071::destination", + "logging::evt::1072", + "logging::evt::1072::description", + "logging::evt::1072::name", + "logging::evt::1072::priority", + "logging::evt::1072::active", + "logging::evt::1072::destination", + "logging::evt::1073", + "logging::evt::1073::description", + "logging::evt::1073::name", + "logging::evt::1073::priority", + "logging::evt::1073::active", + "logging::evt::1073::destination", + "logging::evt::1075", + "logging::evt::1075::description", + "logging::evt::1075::name", + "logging::evt::1075::priority", + "logging::evt::1075::active", + "logging::evt::1075::destination", + "logging::evt::1076", + "logging::evt::1076::description", + "logging::evt::1076::name", + "logging::evt::1076::priority", + "logging::evt::1076::active", + "logging::evt::1076::destination", + "logging::evt::1077", + "logging::evt::1077::description", + "logging::evt::1077::name", + "logging::evt::1077::priority", + "logging::evt::1077::active", + "logging::evt::1077::destination", + "logging::evt::2002", + "logging::evt::2002::description", + "logging::evt::2002::name", + "logging::evt::2002::priority", + "logging::evt::2002::active", + "logging::evt::2002::destination", + "logging::evt::2003", + "logging::evt::2003::description", + "logging::evt::2003::name", + "logging::evt::2003::priority", + "logging::evt::2003::active", + "logging::evt::2003::destination", + "logging::evt::2005", + "logging::evt::2005::description", + "logging::evt::2005::name", + "logging::evt::2005::priority", + "logging::evt::2005::active", + "logging::evt::2005::destination", + "logging::evt::2006", + "logging::evt::2006::description", + "logging::evt::2006::name", + "logging::evt::2006::priority", + "logging::evt::2006::active", + "logging::evt::2006::destination", + "logging::evt::2020", + "logging::evt::2020::description", + "logging::evt::2020::name", + "logging::evt::2020::priority", + "logging::evt::2020::active", + "logging::evt::2020::destination", + "logging::evt::2024", + "logging::evt::2024::description", + "logging::evt::2024::name", + "logging::evt::2024::priority", + "logging::evt::2024::active", + "logging::evt::2024::destination", + "logging::evt::2025", + "logging::evt::2025::description", + "logging::evt::2025::name", + "logging::evt::2025::priority", + "logging::evt::2025::active", + "logging::evt::2025::destination", + "logging::evt::2026", + "logging::evt::2026::description", + "logging::evt::2026::name", + "logging::evt::2026::priority", + "logging::evt::2026::active", + "logging::evt::2026::destination", + "logging::evt::2028", + "logging::evt::2028::description", + "logging::evt::2028::name", + "logging::evt::2028::priority", + "logging::evt::2028::active", + "logging::evt::2028::destination", + "logging::evt::2030", + "logging::evt::2030::description", + "logging::evt::2030::name", + "logging::evt::2030::priority", + "logging::evt::2030::active", + "logging::evt::2030::destination", + "logging::evt::2034", + "logging::evt::2034::description", + "logging::evt::2034::name", + "logging::evt::2034::priority", + "logging::evt::2034::active", + "logging::evt::2034::destination", + "logging::evt::2046", + "logging::evt::2046::description", + "logging::evt::2046::name", + "logging::evt::2046::priority", + "logging::evt::2046::active", + "logging::evt::2046::destination", + "logging::evt::2050", + "logging::evt::2050::description", + "logging::evt::2050::name", + "logging::evt::2050::priority", + "logging::evt::2050::active", + "logging::evt::2050::destination", + "logging::evt::2051", + "logging::evt::2051::description", + "logging::evt::2051::name", + "logging::evt::2051::priority", + "logging::evt::2051::active", + "logging::evt::2051::destination", + "logging::evt::2052", + "logging::evt::2052::description", + "logging::evt::2052::name", + "logging::evt::2052::priority", + "logging::evt::2052::active", + "logging::evt::2052::destination", + "logging::evt::2053", + "logging::evt::2053::description", + "logging::evt::2053::name", + "logging::evt::2053::priority", + "logging::evt::2053::active", + "logging::evt::2053::destination", + "logging::evt::2054", + "logging::evt::2054::description", + "logging::evt::2054::name", + "logging::evt::2054::priority", + "logging::evt::2054::active", + "logging::evt::2054::destination", + "logging::evt::2055", + "logging::evt::2055::description", + "logging::evt::2055::name", + "logging::evt::2055::priority", + "logging::evt::2055::active", + "logging::evt::2055::destination", + "logging::evt::2056", + "logging::evt::2056::description", + "logging::evt::2056::name", + "logging::evt::2056::priority", + "logging::evt::2056::active", + "logging::evt::2056::destination", + "logging::evt::2057", + "logging::evt::2057::description", + "logging::evt::2057::name", + "logging::evt::2057::priority", + "logging::evt::2057::active", + "logging::evt::2057::destination", + "logging::evt::2061", + "logging::evt::2061::description", + "logging::evt::2061::name", + "logging::evt::2061::priority", + "logging::evt::2061::active", + "logging::evt::2061::destination", + "logging::evt::2062", + "logging::evt::2062::description", + "logging::evt::2062::name", + "logging::evt::2062::priority", + "logging::evt::2062::active", + "logging::evt::2062::destination", + "logging::evt::2064", + "logging::evt::2064::description", + "logging::evt::2064::name", + "logging::evt::2064::priority", + "logging::evt::2064::active", + "logging::evt::2064::destination", + "logging::evt::2065", + "logging::evt::2065::description", + "logging::evt::2065::name", + "logging::evt::2065::priority", + "logging::evt::2065::active", + "logging::evt::2065::destination", "no", "ipdr", "snmp-server", "aaa", "radius-server", "privilege", + "privilege::0", + "privilege::0::password", + "privilege::1", + "privilege::1::password", + "privilege::15", + "privilege::15::password", "cli", "packetcable", "system", + "system::proto-throttle", + "system::proto-throttle::total", + "system::proto-throttle::total::rate-pps", + "system::proto-throttle::total::max-burst-pkts", + "system::proto-throttle::other", + "system::proto-throttle::other::rate-pps", + "system::proto-throttle::other::max-burst-pkts", + "system::proto-throttle::arp", + "system::proto-throttle::arp::rate-pps", + "system::proto-throttle::arp::max-burst-pkts", + "system::proto-throttle::dhcp", + "system::proto-throttle::dhcp::rate-pps", + "system::proto-throttle::dhcp::max-burst-pkts", + "system::proto-throttle::dhcpv6", + "system::proto-throttle::dhcpv6::rate-pps", + "system::proto-throttle::dhcpv6::max-burst-pkts", + "system::proto-throttle::nd", + "system::proto-throttle::nd::rate-pps", + "system::proto-throttle::nd::max-burst-pkts", + "system::proto-throttle::rip", + "system::proto-throttle::rip::rate-pps", + "system::proto-throttle::rip::max-burst-pkts", "network", "clock" ]