Simplify command history UI
This commit is contained in:
+71
-25
@@ -6,6 +6,7 @@ const state = {
|
||||
filter: "all",
|
||||
clientFilter: "all",
|
||||
commandFilter: "all",
|
||||
commandStatusFilter: "",
|
||||
commands: [],
|
||||
alerts: [],
|
||||
remoteSessions: [],
|
||||
@@ -131,6 +132,7 @@ const els = {
|
||||
reloadCommandsBtn: document.querySelector("#reloadCommandsBtn"),
|
||||
reloadAuditBtn: document.querySelector("#reloadAuditBtn"),
|
||||
commandSummary: document.querySelector("#commandSummary"),
|
||||
commandStatusFilter: document.querySelector("#commandStatusFilter"),
|
||||
auditSummary: document.querySelector("#auditSummary"),
|
||||
commandList: document.querySelector("#commandList"),
|
||||
commandDetailPanel: document.querySelector("#commandDetailPanel"),
|
||||
@@ -236,18 +238,57 @@ function formatShortDate(value) {
|
||||
|
||||
function statusLabel(status) {
|
||||
return {
|
||||
requested: "Requested",
|
||||
queued: "Queued",
|
||||
active: "Active",
|
||||
closed: "Closed",
|
||||
claimed: "Running",
|
||||
completed: "Done",
|
||||
failed: "Failed",
|
||||
cancelled: "Cancelled",
|
||||
expired: "Expired",
|
||||
requested: "Запрошено",
|
||||
queued: "В очереди",
|
||||
active: "Активно",
|
||||
closed: "Закрыто",
|
||||
claimed: "Выполняется",
|
||||
completed: "Готово",
|
||||
failed: "Ошибка",
|
||||
cancelled: "Отменено",
|
||||
expired: "Истекло",
|
||||
}[status] || status || "-";
|
||||
}
|
||||
|
||||
function commandTypeLabel(type) {
|
||||
return {
|
||||
ping: "Проверка связи",
|
||||
traceroute: "Маршрут до узла",
|
||||
route_show: "Показ маршрутов",
|
||||
interfaces_show: "Показ интерфейсов",
|
||||
reboot: "Перезагрузка",
|
||||
service_restart: "Перезапуск сервиса",
|
||||
pkg_list_installed: "Список пакетов",
|
||||
pkg_update: "Обновление индекса пакетов",
|
||||
pkg_list_upgradable: "Доступные обновления пакетов",
|
||||
pkg_install: "Установка пакета",
|
||||
pkg_remove: "Удаление пакета",
|
||||
opkg_list_installed: "Список пакетов",
|
||||
opkg_update: "Обновление индекса пакетов",
|
||||
opkg_list_upgradable: "Доступные обновления пакетов",
|
||||
opkg_install: "Установка пакета",
|
||||
opkg_remove: "Удаление пакета",
|
||||
uci_show: "Просмотр UCI",
|
||||
uci_backup: "Резервная копия UCI",
|
||||
uci_preview: "Проверка изменения настроек",
|
||||
uci_set: "Подготовка изменения настроек",
|
||||
uci_commit: "Применение настроек",
|
||||
uci_commit_confirmed: "Безопасное применение настроек",
|
||||
uci_revert: "Отмена staged-настроек",
|
||||
uci_restore: "Восстановление настроек",
|
||||
remote_ssh_reverse: "Открытие удаленного доступа",
|
||||
remote_ssh_close: "Закрытие удаленного доступа",
|
||||
}[type] || type || "-";
|
||||
}
|
||||
|
||||
function commandFilterMatches(command) {
|
||||
if (state.commandStatusFilter && command.status !== state.commandStatusFilter) return false;
|
||||
if (state.commandFilter === "running") return command.status === "queued" || command.status === "claimed";
|
||||
if (state.commandFilter === "failed") return ["failed", "expired", "cancelled"].includes(command.status);
|
||||
if (state.commandFilter === "completed") return command.status === "completed";
|
||||
return true;
|
||||
}
|
||||
|
||||
function remoteStatusLabel(status) {
|
||||
return {
|
||||
requested: "Запрашивается",
|
||||
@@ -759,33 +800,33 @@ async function loadCommands() {
|
||||
function renderCommands(commands) {
|
||||
els.commandList.innerHTML = "";
|
||||
renderCommandSummary(commands);
|
||||
const filtered = state.commandFilter === "all" ? commands : commands.filter((command) => command.status === state.commandFilter);
|
||||
const filtered = commands.filter(commandFilterMatches);
|
||||
if (filtered.length === 0) {
|
||||
els.commandList.textContent = "No commands";
|
||||
els.commandList.innerHTML = '<div class="inline-empty">Команд по выбранному фильтру нет</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (const command of filtered) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "row";
|
||||
row.className = "command-row";
|
||||
const canCancel = command.status === "queued" || command.status === "claimed";
|
||||
const output = command.output || JSON.stringify(command.args || {});
|
||||
const finishedAt = command.completed_at || command.cancelled_at || command.expired_at;
|
||||
row.innerHTML = `
|
||||
<div>
|
||||
<strong>${escapeHtml(command.type)}</strong><br>
|
||||
<small>${escapeHtml(command.id)}</small>
|
||||
<div class="command-main">
|
||||
<strong>${escapeHtml(commandTypeLabel(command.type))}</strong>
|
||||
<small>${escapeHtml(command.type)} · ${escapeHtml(command.id)}</small>
|
||||
</div>
|
||||
<span class="status ${escapeHtml(command.status)}">${escapeHtml(statusLabel(command.status))}</span>
|
||||
<span>try ${command.attempt_count || 0}/${command.max_attempts || 3}</span>
|
||||
<span>${command.attempt_count || 0}/${command.max_attempts || 3} попытка</span>
|
||||
<span class="lifecycle-time">${escapeHtml(formatShortDate(finishedAt || command.claimed_at || command.created_at))}</span>
|
||||
<details class="command-output">
|
||||
<summary>${escapeHtml(outputSummary(output))}</summary>
|
||||
<pre>${escapeHtml(output)}</pre>
|
||||
</details>
|
||||
<div class="row-actions">
|
||||
<button type="button" data-action="detail">Open</button>
|
||||
<button type="button" data-action="cancel" ${canCancel ? "" : "disabled"}>Cancel</button>
|
||||
<button type="button" data-action="detail">Детали</button>
|
||||
<button type="button" data-action="cancel" ${canCancel ? "" : "disabled"}>Отменить</button>
|
||||
</div>
|
||||
`;
|
||||
row.querySelector('[data-action="detail"]').addEventListener("click", () => {
|
||||
@@ -805,7 +846,7 @@ function renderCommandSummary(commands) {
|
||||
const total = commands.length;
|
||||
const active = (counts.queued || 0) + (counts.claimed || 0);
|
||||
const failed = (counts.failed || 0) + (counts.expired || 0);
|
||||
els.commandSummary.textContent = `${total} total / ${active} active / ${failed} attention`;
|
||||
els.commandSummary.textContent = `${total} всего / ${active} выполняются / ${failed} ошибки`;
|
||||
}
|
||||
|
||||
function renderCommandDetail(command) {
|
||||
@@ -816,13 +857,13 @@ function renderCommandDetail(command) {
|
||||
const output = command.output || JSON.stringify(command.args || {}, null, 2);
|
||||
els.commandDetailPanel.classList.remove("is-hidden");
|
||||
els.commandDetailMeta.innerHTML = `
|
||||
<span>${escapeHtml(command.type)}</span>
|
||||
<span>${escapeHtml(commandTypeLabel(command.type))}</span>
|
||||
<span>${escapeHtml(statusLabel(command.status))}</span>
|
||||
<span>try ${command.attempt_count || 0}/${command.max_attempts || 3}</span>
|
||||
<span>created ${escapeHtml(formatShortDate(command.created_at))}</span>
|
||||
<span>expires ${escapeHtml(formatShortDate(command.expires_at))}</span>
|
||||
<span>claimed ${escapeHtml(formatShortDate(command.claimed_at))}</span>
|
||||
<span>finished ${escapeHtml(formatShortDate(command.completed_at || command.cancelled_at || command.expired_at))}</span>
|
||||
<span>${command.attempt_count || 0}/${command.max_attempts || 3} попытка</span>
|
||||
<span>создана ${escapeHtml(formatShortDate(command.created_at))}</span>
|
||||
<span>истекает ${escapeHtml(formatShortDate(command.expires_at))}</span>
|
||||
<span>забрана ${escapeHtml(formatShortDate(command.claimed_at))}</span>
|
||||
<span>завершена ${escapeHtml(formatShortDate(command.completed_at || command.cancelled_at || command.expired_at))}</span>
|
||||
<span>${escapeHtml(command.id)}</span>
|
||||
`;
|
||||
els.commandDetailOutput.innerHTML = highlightOutput(output);
|
||||
@@ -1423,6 +1464,11 @@ for (const button of document.querySelectorAll(".command-filter")) {
|
||||
});
|
||||
}
|
||||
|
||||
els.commandStatusFilter.addEventListener("change", () => {
|
||||
state.commandStatusFilter = els.commandStatusFilter.value;
|
||||
renderCommands(state.commands);
|
||||
});
|
||||
|
||||
els.commandType.addEventListener("change", () => {
|
||||
els.commandTarget.disabled = ["pkg_list_installed", "route_show", "interfaces_show", "reboot"].includes(els.commandType.value);
|
||||
});
|
||||
|
||||
+17
-11
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>OpenWrt RMM</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=6">
|
||||
<link rel="stylesheet" href="/styles.css?v=7">
|
||||
</head>
|
||||
<body>
|
||||
<main id="loginView" class="login-view">
|
||||
@@ -608,21 +608,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters command-filters">
|
||||
<button class="command-filter is-active" data-command-filter="all" type="button">All</button>
|
||||
<button class="command-filter" data-command-filter="queued" type="button">Queued</button>
|
||||
<button class="command-filter" data-command-filter="claimed" type="button">Running</button>
|
||||
<button class="command-filter" data-command-filter="completed" type="button">Done</button>
|
||||
<button class="command-filter" data-command-filter="failed" type="button">Failed</button>
|
||||
<button class="command-filter" data-command-filter="expired" type="button">Expired</button>
|
||||
<button class="command-filter" data-command-filter="cancelled" type="button">Cancelled</button>
|
||||
<button class="command-filter is-active" data-command-filter="all" type="button">Все</button>
|
||||
<button class="command-filter" data-command-filter="running" type="button">Выполняются</button>
|
||||
<button class="command-filter" data-command-filter="failed" type="button">Ошибки</button>
|
||||
<button class="command-filter" data-command-filter="completed" type="button">Готовые</button>
|
||||
<select id="commandStatusFilter" aria-label="Точный статус команды">
|
||||
<option value="">Любой статус</option>
|
||||
<option value="queued">В очереди</option>
|
||||
<option value="claimed">Выполняется</option>
|
||||
<option value="completed">Готово</option>
|
||||
<option value="failed">Ошибка</option>
|
||||
<option value="expired">Истекла</option>
|
||||
<option value="cancelled">Отменена</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="commandList" class="table-list"></div>
|
||||
</section>
|
||||
|
||||
<section id="commandDetailPanel" class="panel is-hidden" data-device-tab="expert">
|
||||
<div class="section-title">
|
||||
<h2>Command detail</h2>
|
||||
<button id="copyCommandOutputBtn" type="button">Copy output</button>
|
||||
<h2>Детали команды</h2>
|
||||
<button id="copyCommandOutputBtn" type="button">Копировать вывод</button>
|
||||
</div>
|
||||
<div id="commandDetailMeta" class="detail-meta"></div>
|
||||
<pre id="commandDetailOutput" class="command-detail-output"></pre>
|
||||
@@ -642,6 +648,6 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/app.js?v=13" defer></script>
|
||||
<script src="/app.js?v=14" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+37
-1
@@ -1032,7 +1032,15 @@ select {
|
||||
}
|
||||
|
||||
.command-filters {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.command-filters select {
|
||||
width: auto;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.filter.is-active,
|
||||
@@ -1412,6 +1420,33 @@ select {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.command-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1.2fr) 120px 100px 90px minmax(220px, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.command-main {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.command-main strong,
|
||||
.command-main small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.command-main small {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.lifecycle-time {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
@@ -1582,6 +1617,7 @@ select {
|
||||
}
|
||||
|
||||
.row,
|
||||
.command-row,
|
||||
.row.audit,
|
||||
.command-form,
|
||||
.fleet-form,
|
||||
|
||||
Reference in New Issue
Block a user