樹狀結構排版修正&命令邏寫入邏輯優化

This commit is contained in:
swpa 2026-05-04 18:26:12 +08:00
parent 09a0f3699a
commit cd5ab201f5
6 changed files with 933 additions and 56 deletions

View File

@ -250,20 +250,52 @@
<!-- 表單 4完整設備配置樹狀圖 --> <!-- 表單 4完整設備配置樹狀圖 -->
<div id="form-full-config" class="task-form" style="display: none;"> <div id="form-full-config" class="task-form" style="display: none;">
<!-- 💡 將標題與載入提示放在同一行 (使用 flexbox) -->
<h3 style="display: flex; align-items: center; margin-bottom: 15px;"> <h3 style="display: flex; align-items: center; margin-bottom: 15px;">
🌳 完整設備配置樹狀圖 🌳 完整設備配置樹狀圖
<!-- 💡 隱藏的載入提示,放在標題右邊 -->
<span id="loading-message" style="display: none; color: #f39c12; font-size: 14px; margin-left: 15px; font-weight: normal;"> <span id="loading-message" style="display: none; color: #f39c12; font-size: 14px; margin-left: 15px; font-weight: normal;">
⏳ 正在從設備抓取完整配置,可能需要 30~60 秒,請耐心稍候... ⏳ 正在從設備抓取完整配置,可能需要 30~60 秒,請耐心稍候...
</span> </span>
</h3> </h3>
<!-- 💡 已經刪除了原本的綠色按鈕與說明文字 --> <!-- 🌟 左右分屏容器 -->
<div id="split-container" style="display: flex; align-items: flex-start; width: 100%; position: relative;">
<!-- 樹狀圖渲染區塊 --> <!-- 左側:樹狀圖 (預設滿版,顯示預覽時會被 JS 改為 50%) -->
<div id="tree-container" style="background-color: #ffffff; padding: 15px; border-radius: 5px; border: 1px solid #e0e0e0; min-height: 100px;"> <div id="tree-container" style="width: 100%; background-color: #ffffff; padding: 15px; border-radius: 5px; border: 1px solid #e0e0e0; min-height: 100px; box-sizing: border-box; overflow-x: auto;">
<span style="color: #7f8c8d;">尚未載入資料。請點擊上方「載入任務」按鈕開始。</span> <span style="color: #7f8c8d;">尚未載入資料。請點擊上方「載入任務」按鈕開始。</span>
</div>
<!-- 🖱️ 拖曳分隔線 (預設隱藏) -->
<div id="drag-resizer" style="display: none; width: 12px; cursor: col-resize; margin: 0 5px; flex-shrink: 0; align-items: center; justify-content: center; position: sticky; top: 20px; height: 400px; z-index: 10;" title="左右拖曳調整寬度">
<div style="width: 4px; height: 40px; background-color: #bdc3c7; border-radius: 2px;"></div>
</div>
<!-- 右側CLI 預覽與執行結果 (預設隱藏,顯示時佔 50%) -->
<div id="side-cli-preview" style="width: 50%; position: sticky; top: 20px; background: #2c3e50; padding: 12px 15px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: none; border: 1px solid #34495e; box-sizing: border-box; flex-shrink: 0;">
<!-- 頂部標題列 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h4 id="side-pane-title" style="color: #f1c40f; margin: 0; font-size: 15px;">⚠️ 即將寫入的指令</h4>
<div style="display: flex; align-items: center; gap: 10px;">
<!-- 預覽模式按鈕 -->
<button id="btn-side-cancel" onclick="hideSideCLI()" style="background-color: #7f8c8d; padding: 5px 12px; font-size: 13px;">取消</button>
<button id="btn-side-confirm" onclick="executeSideCLI()" style="background-color: #27ae60; padding: 5px 12px; font-size: 13px;">🚀 確認寫入</button>
<!-- 執行結果模式按鈕 (預設隱藏) -->
<button id="btn-side-close" onclick="hideSideCLI()" style="background-color: #3498db; padding: 5px 12px; font-size: 13px; display: none;">✅ 完成並關閉</button>
<div style="width: 1px; height: 20px; background-color: #7f8c8d; margin: 0 5px;"></div>
<span onclick="hideSideCLI()" style="color: #bdc3c7; font-size: 26px; cursor: pointer; line-height: 1;" title="關閉面板">&times;</span>
</div>
</div>
<!-- 模式一:指令預覽框 (可編輯) -->
<textarea id="side-cli-textarea" style="width: 100%; height: 400px; background: #1e1e1e; color: #ecf0f1; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 14px; line-height: 1.5; padding: 12px 15px; border: 1px solid #7f8c8d; border-radius: 4px; box-sizing: border-box; white-space: pre; overflow-wrap: normal; overflow-x: scroll; margin-bottom: 0; display: block;"></textarea>
<!-- 模式二:執行結果框 (唯讀,升級字體與行高) -->
<div id="side-execution-result" style="width: 100%; height: 400px; background: #1e1e1e; color: #ecf0f1; font-family: 'Consolas', 'Monaco', 'Courier New', monospace; font-size: 14px; line-height: 1.5; padding: 12px 15px; border: 1px solid #7f8c8d; border-radius: 4px; box-sizing: border-box; overflow-y: auto; margin-bottom: 0; display: none; white-space: pre-wrap; word-wrap: break-word;"></div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,6 +6,7 @@ from typing import List
from netmiko import ConnectHandler from netmiko import ConnectHandler
# 💡 確保從 shared 引入 deep_split_tree # 💡 確保從 shared 引入 deep_split_tree
from shared import CMTS_DEVICE, cmts_config_lock, parse_cli_to_tree, deep_split_tree, load_settings, save_settings 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() router = APIRouter()
@ -15,6 +16,15 @@ class ConfigRequest(BaseModel):
username: str username: str
password: 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") @router.post("/cmts-config")
async def execute_cmts_config(req: ConfigRequest): 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('!')] commands = [cmd.strip() for cmd in req.script.splitlines() if cmd.strip() and not cmd.strip().startswith('!')]
net_connect = ConnectHandler(**device) net_connect = ConnectHandler(**device)
# 🌟 1. 手動進入設定模式 (恢復您原本正確的寫法)
net_connect.send_command_timing("config") net_connect.send_command_timing("config")
# 批次送出設定指令 # 🌟 2. 批次送出設定指令
output = net_connect.send_config_set(commands, cmd_verify=False) # 加上 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.send_command_timing("exit")
net_connect.disconnect() 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} return {"status": "success", "data": output}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=f"CMTS Config Error: {str(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 # 💡 以下為 DS RF Port 動態樹狀查詢 API

View File

@ -45,10 +45,10 @@ def is_path_locked_by_others(target_path: str, user_id: str) -> tuple[Optional[d
if target_path == locked_path: if target_path == locked_path:
return lock_info, f"此區塊正由 [{lock_info['username']}] 編輯中" 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']}] 鎖定,無法編輯子區塊" 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 lock_info, f"子層級 [{locked_path}] 已被 [{lock_info['username']}] 鎖定,無法鎖定整個父區塊"
return None, "" return None, ""

View File

@ -7,6 +7,11 @@ let term, fitAddon, ws;
let currentMacDomainData = null; let currentMacDomainData = null;
let isConnected = false; let isConnected = false;
// 💡 新增:追蹤當前編輯狀態
let currentEditPath = null;
let currentEditElementId = null;
let currentEditIsSuccess = false;
window.onload = initTerminal; window.onload = initTerminal;
window.onresize = () => { if (fitAddon) fitAddon.fit(); }; window.onresize = () => { if (fitAddon) fitAddon.fit(); };
@ -14,7 +19,7 @@ window.onresize = () => { if (fitAddon) fitAddon.fit(); };
// 💡 全域狀態與鎖定管理 (Lock Management) // 💡 全域狀態與鎖定管理 (Lock Management)
// ========================================== // ==========================================
const SESSION_USER_ID = crypto.randomUUID ? crypto.randomUUID() : 'user-' + Math.random().toString(36).substr(2, 9); 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) { async function sendHeartbeat(path) {
try { try {
@ -26,6 +31,24 @@ async function sendHeartbeat(path) {
} catch (error) {} } 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 `<span style="color: #ff6b6b; font-weight: bold;">${line}</span>`;
}
return line; // 沒有關鍵字的行,維持預設顏色 (白色)
});
return formattedLines.join('\n');
}
// ========================================== // ==========================================
// 💡 資料夾層級的編輯模式邏輯 (Node-Level Lock) // 💡 資料夾層級的編輯模式邏輯 (Node-Level Lock)
// ========================================== // ==========================================
@ -43,20 +66,17 @@ async function startEditFolder(path, elementId) {
const result = await response.json(); const result = await response.json();
if (response.status === 409) { if (response.status === 409) {
alert(`⚠️ ${result.detail}`); // 後端擋下衝突時顯示警告 alert(`⚠️ ${result.detail}`);
return; return;
} }
if (result.status === 'success') { if (result.status === 'success') {
// 啟動心跳
ACTIVE_HEARTBEATS[path] = setInterval(() => sendHeartbeat(path), 15000); ACTIVE_HEARTBEATS[path] = setInterval(() => sendHeartbeat(path), 15000);
// UI 切換
document.getElementById(`edit-btn-${elementId}`).style.display = 'none'; document.getElementById(`edit-btn-${elementId}`).style.display = 'none';
document.getElementById(`actions-${elementId}`).style.display = 'inline-block'; document.getElementById(`actions-${elementId}`).style.display = 'inline-block';
document.getElementById(`details-${elementId}`).open = true; document.getElementById(`details-${elementId}`).open = true;
// 轉為輸入框
const contentDiv = document.getElementById(`content-${elementId}`); const contentDiv = document.getElementById(`content-${elementId}`);
const leafContainers = contentDiv.querySelectorAll('.leaf-container'); 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}`); const contentDiv = document.getElementById(`content-${elementId}`);
if (!contentDiv) return;
const inputs = contentDiv.querySelectorAll('.edit-input'); const inputs = contentDiv.querySelectorAll('.edit-input');
const diffs = [];
let changes = [];
// 掃描所有輸入框,找出被修改過的值
inputs.forEach(input => { inputs.forEach(input => {
const nodePath = input.getAttribute('data-path'); const container = input.closest('.leaf-container');
const newVal = input.value; if (!container) return;
const origVal = input.parentElement.getAttribute('data-original');
if (newVal !== origVal) { const originalVal = container.getAttribute('data-original') || "";
changes.push(`- ${nodePath}:\n [舊] ${origVal} ➡️ [新] ${newVal}`); const newVal = input.value.trim();
const nodePath = container.getAttribute('data-path');
if (originalVal !== newVal && nodePath) {
diffs.push({
path: nodePath,
old_val: originalVal,
new_val: newVal
});
} }
}); });
if (changes.length === 0) { if (diffs.length === 0) {
alert("您沒有修改任何數值!"); alert("沒有偵測到任何修改,無需生成指令");
return; return;
} }
alert(`【Phase 3 預覽】\n鎖定區塊: ${path}\n\n偵測到的修改:\n${changes.join('\n\n')}\n\n即將實作轉譯為 CMTS CLI 的功能!`); 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');
// --- 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 = `<span style="color: #ecf0f1;">${finalScript}</span>`;
// 呼叫 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 = `<span style="color: #ecf0f1;">${result.data}</span>`;
} 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 = `<span style="color: #ecf0f1;">${highlightTerminalOutput(rawLog)}</span>`;
}
} 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 = `<span style="color: #ff6b6b; font-weight: bold;">連線或伺服器錯誤: ${error}</span>`;
}
} }
// ========================================== // ==========================================
// 💡 編輯模式 UI 切換邏輯 // 💡 編輯模式 UI 切換邏輯 (保留舊版單一節點編輯用)
// ========================================== // ==========================================
function renderEditModeUI(path, currentValue, elementId) { function renderEditModeUI(path, currentValue, elementId) {
const container = document.getElementById(elementId); const container = document.getElementById(elementId);
if (!container) return; if (!container) return;
// 將原本的文字替換為 Input 與操作按鈕
// 注意:這裡將單引號跳脫,避免 HTML 屬性解析錯誤
const safeValue = currentValue.replace(/'/g, "&#39;"); const safeValue = currentValue.replace(/'/g, "&#39;");
container.innerHTML = ` container.innerHTML = `
@ -158,7 +397,6 @@ function restoreViewModeUI(path, originalValue, elementId) {
const container = document.getElementById(elementId); const container = document.getElementById(elementId);
if (!container) return; if (!container) return;
// 恢復成原本的文字與編輯按鈕
const safeValue = originalValue.replace(/'/g, "&#39;"); const safeValue = originalValue.replace(/'/g, "&#39;");
container.innerHTML = ` container.innerHTML = `
<span style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${originalValue}</span> <span style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${originalValue}</span>
@ -168,7 +406,6 @@ function restoreViewModeUI(path, originalValue, elementId) {
`; `;
} }
// Phase 3 的預留函數:預覽 CLI 指令
function previewCLI(path, elementId) { function previewCLI(path, elementId) {
const newValue = document.getElementById(`input-${elementId}`).value; const newValue = document.getElementById(`input-${elementId}`).value;
alert(`【Phase 3 預覽】\n路徑: ${path}\n新值: ${newValue}\n\n即將實作轉譯為 CMTS CLI 的功能!`); alert(`【Phase 3 預覽】\n路徑: ${path}\n新值: ${newValue}\n\n即將實作轉譯為 CMTS CLI 的功能!`);
@ -208,21 +445,17 @@ function switchConfigTask() {
} }
} }
// 💡 合併修復:統一處理「載入任務」的邏輯與防呆
function startConfigTask() { function startConfigTask() {
// 防呆 1檢查是否已連線
if (!isConnected) { if (!isConnected) {
alert("請先點擊畫面上方的「連線至 CMTS」成功後再執行載入任務"); alert("請先點擊畫面上方的「連線至 CMTS」成功後再執行載入任務");
return; return;
} }
// 先確保對應的表單有顯示出來
switchConfigTask(); switchConfigTask();
const selectedTaskId = document.getElementById('configTask').value; const selectedTaskId = document.getElementById('configTask').value;
if (selectedTaskId === 'form-bonding-config') { if (selectedTaskId === 'form-bonding-config') {
// 任務 AMAC Domain 配置精靈
if (currentMacDomainData && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) { if (currentMacDomainData && !confirm("重新載入將會清除您目前未套用的設定,確定要繼續嗎?")) {
return; return;
} }
@ -230,8 +463,6 @@ function startConfigTask() {
loadMacDomainList(); loadMacDomainList();
} else if (selectedTaskId === 'form-full-config') { } else if (selectedTaskId === 'form-full-config') {
// 💡 修復:任務 B完整設備配置樹狀圖
// 當使用者點擊「載入任務」時,直接幫他觸發抓取樹狀圖的 API
fetchFullConfig(); fetchFullConfig();
} }
} }
@ -758,16 +989,25 @@ function buildTree(data, parentPath = "") {
let html = '<div style="margin-left: 20px;">'; let html = '<div style="margin-left: 20px;">';
for (const key in data) { for (const key in data) {
const value = data[key]; 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) { if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) {
// 📁 渲染資料夾 (加入編輯與操作按鈕) // 📁 渲染資料夾 (加入編輯與操作按鈕)
const elementId = `folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`; const elementId = `folder-${currentPath.replace(/[^a-zA-Z0-9]/g, '-')}`;
html += ` html += `
<details id="details-${elementId}"> <!-- 💡 加上 ontoggle 事件當展開/收合時自動切換 is-open class -->
<summary style="cursor: pointer; font-weight: bold; color: #2c3e50; margin-top: 5px; padding: 2px 0; display: flex; align-items: center;"> <details id="details-${elementId}" ontoggle="this.querySelector('summary').classList.toggle('is-open', this.open)">
<span style="margin-right: 5px;">📁</span> ${key}
<!-- 💡 套用 tree-node-header 樣式並加入 list-style: none 隱藏原生黑色小三角形 -->
<summary class="tree-node-header" style="font-weight: bold; color: #2c3e50; margin-top: 5px; list-style: none;">
<!-- 💡 替換為純 CSS 的旋轉箭頭與動態資料夾圖示 -->
<span class="tree-chevron"></span>
<span class="tree-folder-icon"></span>
<!-- 確保資料夾名稱佔據剩餘空間讓後面的按鈕靠右或保持間距 -->
<span style="flex-grow: 1;">${key}</span>
<!-- 資料夾專屬編輯按鈕 --> <!-- 資料夾專屬編輯按鈕 -->
<span id="edit-btn-${elementId}" onclick="event.stopPropagation(); event.preventDefault(); startEditFolder('${currentPath}', '${elementId}')" <span id="edit-btn-${elementId}" onclick="event.stopPropagation(); event.preventDefault(); startEditFolder('${currentPath}', '${elementId}')"
@ -896,28 +1136,39 @@ async function loadRealConfigForFilters() {
function buildRealFilterTree(data, parentPath, hiddenKeys) { function buildRealFilterTree(data, parentPath, hiddenKeys) {
if (typeof data !== 'object' || data === null) return ''; if (typeof data !== 'object' || data === null) return '';
let html = `<div style="margin-left: ${parentPath ? '25px' : '0'};">`; let html = `<div style="margin-left: ${parentPath ? '20px' : '0'};">`;
for (const key in data) { for (const key in data) {
const value = data[key]; 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" : ""; const isChecked = hiddenKeys.includes(currentPath) ? "checked" : "";
if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) { if (typeof value === 'object' && value !== null && Object.keys(value).length > 0) {
html += ` html += `
<details> <details ontoggle="this.querySelector('summary').classList.toggle('is-open', this.open)">
<summary style="cursor: pointer; font-weight: bold; color: #2c3e50; margin-top: 5px; padding: 4px 0; display: flex; align-items: center; list-style: none;"> <summary class="tree-node-header" style="font-weight: bold; color: #2c3e50; margin-top: 5px; list-style: none;">
<input type="checkbox" value="${currentPath}" class="filter-checkbox" style="margin-right: 8px; cursor: pointer; width: 16px; height: 16px; accent-color: #e74c3c;" ${isChecked}> <!-- 💡 加上 onclick 阻擋事件冒泡避免點擊 Checkbox 時觸發資料夾展開/收合 -->
<span style="margin-right: 5px;">📁</span> ${key} <input type="checkbox" value="${currentPath}" class="filter-checkbox" ${isChecked}
onclick="event.stopPropagation();" onchange="toggleChildCheckboxes(this)">
<span class="tree-chevron"></span>
<span class="tree-folder-icon"></span>
<span style="flex-grow: 1;">${key}</span>
</summary> </summary>
${buildRealFilterTree(value, currentPath, hiddenKeys)}
<!-- 💡 加入特定的 class 方便後續 JS 抓取子節點 -->
<div class="filter-children-container">
${buildRealFilterTree(value, currentPath, hiddenKeys)}
</div>
</details> </details>
`; `;
} else { } else {
const safeValue = (value === null ? '' : value).toString().replace(/"/g, "&quot;");
html += ` html += `
<div style="padding: 4px 0; color: #34495e; margin-left: 20px; border-left: 1px solid #ecf0f1; padding-left: 10px; display: flex; align-items: center;"> <div style="padding: 4px 0; color: #34495e; margin-left: 20px; border-left: 1px solid #ecf0f1; padding-left: 10px; display: flex; align-items: center;">
<input type="checkbox" value="${currentPath}" class="filter-checkbox" style="margin-right: 8px; cursor: pointer; width: 16px; height: 16px; accent-color: #e74c3c;" ${isChecked}> <input type="checkbox" value="${currentPath}" class="filter-checkbox" ${isChecked}
<span style="margin-right: 5px;">📄</span> <b>${key}</b>: <span style="color: #16a085; margin-left: 5px;">${value === null ? '' : value}</span> onchange="updateParentCheckboxState(this)">
<span style="margin-right: 5px;">📄</span> <b>${key}</b>:
<span style="color: #16a085; margin-left: 5px; font-family: Courier New, monospace;">${safeValue}</span>
</div> </div>
`; `;
} }
@ -926,6 +1177,39 @@ function buildRealFilterTree(data, parentPath, hiddenKeys) {
return html; 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() { async function saveSystemSettings() {
const checkboxes = document.querySelectorAll('.filter-checkbox:checked'); const checkboxes = document.querySelectorAll('.filter-checkbox:checked');
const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value); const selectedHiddenKeys = Array.from(checkboxes).map(cb => cb.value);

View File

@ -54,4 +54,95 @@ button:hover { background-color: #3498db; }
.modal-body { flex-grow: 1; padding: 20px; overflow: auto; background-color: #1e1e1e; } .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; } .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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%2395a5a6'><path d='M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z'/></svg>");
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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f1c40f'><path d='M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z'/></svg>");
background-size: contain;
background-repeat: no-repeat;
}
/* 展開時:切換為開啟的資料夾 */
.tree-node-header.is-open .tree-folder-icon {
/* 內建金色開啟資料夾 SVG */
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f1c40f'><path d='M19 8H8.99C8.04 8 7.19 8.59 6.81 9.46L2.81 18.55C2.62 18.98 2.94 19.5 3.41 19.5H16.99C17.94 19.5 18.79 18.91 19.17 18.04L23.17 8.95C23.36 8.52 23.04 8 22.57 8H19zM4 4c-1.1 0-2 .9-2 2v10.59l3.09-7.04C5.56 8.59 6.51 8 7.46 8H20V6c0-1.1-.9-2-2-2h-8l-2-2H4z'/></svg>");
}
/* 隱藏原生 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; } } @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

View File

@ -4,15 +4,386 @@
"ssh", "ssh",
"hostname", "hostname",
"logging", "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", "no",
"ipdr", "ipdr",
"snmp-server", "snmp-server",
"aaa", "aaa",
"radius-server", "radius-server",
"privilege", "privilege",
"privilege::0",
"privilege::0::password",
"privilege::1",
"privilege::1::password",
"privilege::15",
"privilege::15::password",
"cli", "cli",
"packetcable", "packetcable",
"system", "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", "network",
"clock" "clock"
] ]