樹狀結構排版修正&命令邏寫入邏輯優化
This commit is contained in:
parent
09a0f3699a
commit
cd5ab201f5
44
index.html
44
index.html
|
|
@ -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="關閉面板">×</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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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, ""
|
||||||
|
|
|
||||||
368
static/app.js
368
static/app.js
|
|
@ -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, "'");
|
const safeValue = currentValue.replace(/'/g, "'");
|
||||||
|
|
||||||
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, "'");
|
const safeValue = originalValue.replace(/'/g, "'");
|
||||||
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') {
|
||||||
// 任務 A:MAC 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, """);
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -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; } }
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue