Add device workspace tabs

This commit is contained in:
benya
2026-06-04 20:15:38 +03:00
parent 6a65031593
commit 50bcac4c8d
4 changed files with 166 additions and 59 deletions
+38
View File
@@ -2,6 +2,7 @@ const state = {
username: "",
devices: [],
selectedDeviceId: null,
deviceTab: "overview",
filter: "all",
commandFilter: "all",
commands: [],
@@ -33,6 +34,9 @@ const els = {
emptyState: document.querySelector("#emptyState"),
deviceView: document.querySelector("#deviceView"),
backToFleetBtn: document.querySelector("#backToFleetBtn"),
quickDiagnosticBtn: document.querySelector("#quickDiagnosticBtn"),
openLuciBtn: document.querySelector("#openLuciBtn"),
remoteAccessPanel: document.querySelector("#remoteAccessPanel"),
deviceName: document.querySelector("#deviceName"),
deviceMeta: document.querySelector("#deviceMeta"),
deviceBadge: document.querySelector("#deviceBadge"),
@@ -381,6 +385,7 @@ function renderDeviceDetail(device) {
els.fleetView.classList.add("is-hidden");
els.deviceView.classList.remove("is-hidden");
renderDeviceTab();
const displayName = deviceDisplayName(device);
els.pageTitle.textContent = displayName;
els.deviceName.textContent = displayName;
@@ -573,6 +578,7 @@ async function loadDevices() {
async function selectDevice(id) {
state.selectedDeviceId = id;
state.deviceTab = "overview";
renderDevices();
renderDeviceDetail(currentDevice());
await Promise.all([loadCommands(), loadAudit(), loadMetricsHistory(), loadAlerts(), loadRemoteSessions()]);
@@ -585,6 +591,31 @@ function showFleet() {
renderDeviceDetail(null);
}
function selectDeviceTab(tab) {
state.deviceTab = tab;
renderDeviceTab();
}
function renderDeviceTab() {
for (const button of document.querySelectorAll(".device-tab")) {
button.classList.toggle("is-active", button.dataset.deviceTabTarget === state.deviceTab);
}
for (const section of document.querySelectorAll("[data-device-tab]")) {
section.classList.toggle("is-hidden", section.dataset.deviceTab !== state.deviceTab);
}
}
function openLuciOrRemoteAccess() {
const activeSession = state.remoteSessions.find((session) => session.status === "active" && session.luci_port);
if (activeSession) {
window.open(`/luci/${encodeURIComponent(activeSession.device_id)}/${encodeURIComponent(activeSession.id)}/`, "_blank", "noopener");
return;
}
selectDeviceTab("operations");
els.remoteAccessPanel.scrollIntoView({ behavior: "smooth", block: "start" });
setStatus("Создайте удаленную сессию, затем откройте LuCI");
}
async function loadCommands() {
if (!state.selectedDeviceId) return;
const data = await api(`/api/devices/${encodeURIComponent(state.selectedDeviceId)}/commands?limit=50`);
@@ -1014,6 +1045,7 @@ async function runAlertDiagnostics(alert) {
}
function scrollToCommands() {
selectDeviceTab("expert");
document.querySelector("#commandsPanel")?.scrollIntoView({ behavior: "smooth", block: "start" });
}
@@ -1043,6 +1075,8 @@ els.logoutBtn.addEventListener("click", () => logout().catch((error) => setStatu
els.refreshBtn.addEventListener("click", () => loadDevices().catch((error) => setStatus(error.message)));
els.backToFleetBtn.addEventListener("click", showFleet);
els.quickDiagnosticBtn.addEventListener("click", () => selectDeviceTab("operations"));
els.openLuciBtn.addEventListener("click", openLuciOrRemoteAccess);
els.reloadCommandsBtn.addEventListener("click", () => loadCommands().catch((error) => setStatus(error.message)));
els.reloadAuditBtn.addEventListener("click", () => loadAudit().catch((error) => setStatus(error.message)));
els.reloadAlertsBtn.addEventListener("click", () => loadAlerts().catch((error) => setStatus(error.message)));
@@ -1080,6 +1114,10 @@ for (const button of document.querySelectorAll(".diagnostic-btn")) {
});
}
for (const button of document.querySelectorAll(".device-tab")) {
button.addEventListener("click", () => selectDeviceTab(button.dataset.deviceTabTarget));
}
for (const button of document.querySelectorAll(".filter")) {
button.addEventListener("click", () => {
state.filter = button.dataset.filter;
+43 -31
View File
@@ -145,22 +145,34 @@
<div class="device-header">
<div>
<button id="backToFleetBtn" class="back-button" type="button">← Объекты</button>
<p class="eyebrow">Device</p>
<p class="eyebrow">Объект</p>
<h2 id="deviceName"></h2>
<p id="deviceMeta"></p>
</div>
<span id="deviceBadge" class="badge"></span>
<div class="device-header-actions">
<span id="deviceBadge" class="badge"></span>
<button id="quickDiagnosticBtn" type="button">Диагностика</button>
<button id="openLuciBtn" class="primary" type="button">Открыть LuCI</button>
</div>
</div>
<section class="panel">
<nav class="device-tabs" aria-label="Разделы объекта">
<button class="device-tab is-active" data-device-tab-target="overview" type="button">Обзор</button>
<button class="device-tab" data-device-tab-target="clients" type="button">Клиенты и сеть</button>
<button class="device-tab" data-device-tab-target="operations" type="button">Операции</button>
<button class="device-tab" data-device-tab-target="information" type="button">Сведения</button>
<button class="device-tab" data-device-tab-target="expert" type="button">Эксперт</button>
</nav>
<section class="panel overview-status-panel" data-device-tab="overview">
<div class="section-title">
<h2>Состояние роутера</h2>
<span class="summary-pill">кратко</span>
<h2>Состояние объекта</h2>
<span class="summary-pill">обновляется автоматически</span>
</div>
<div id="healthSummary" class="health-summary"></div>
</section>
<section class="panel">
<section class="panel" data-device-tab="information">
<div class="section-title">
<h2>Группа и теги</h2>
<button id="saveFleetBtn" type="button">Сохранить</button>
@@ -177,46 +189,46 @@
</div>
</section>
<div class="metrics-grid">
<div class="metrics-grid" data-device-tab="overview">
<div class="metric">
<span>Last seen</span>
<span>Последний контакт</span>
<strong id="lastSeen">-</strong>
</div>
<div class="metric">
<span>Load average</span>
<span>Нагрузка</span>
<strong id="loadAvg">-</strong>
</div>
<div class="metric">
<span>Uptime</span>
<span>Время работы</span>
<strong id="uptime">-</strong>
</div>
<div class="metric">
<span>Default route</span>
<div class="metric secondary-metric">
<span>Маршрут по умолчанию</span>
<strong id="defaultRoute">-</strong>
</div>
<div class="metric">
<span>WAN IP</span>
<span>WAN-адрес</span>
<strong id="wanIp">-</strong>
</div>
<div class="metric">
<span>Memory</span>
<span>Память</span>
<strong id="memoryUsage">-</strong>
</div>
<div class="metric">
<span>Disk</span>
<div class="metric secondary-metric">
<span>Хранилище</span>
<strong id="diskUsage">-</strong>
</div>
<div class="metric">
<span>Clients</span>
<span>Клиенты</span>
<strong id="clientCounts">-</strong>
</div>
<div class="metric">
<span>Connectivity</span>
<div class="metric secondary-metric">
<span>Проверки связи</span>
<strong id="connectivityStatus">-</strong>
</div>
</div>
<div class="two-column">
<div class="two-column" data-device-tab="expert">
<section class="panel">
<div class="section-title">
<h2>Inventory</h2>
@@ -243,7 +255,7 @@
</section>
</div>
<section class="panel">
<section class="panel" data-device-tab="operations">
<div class="section-title">
<h2>Быстрая диагностика</h2>
<span class="summary-pill">без SSH</span>
@@ -257,7 +269,7 @@
</div>
</section>
<div class="two-column">
<div class="two-column" data-device-tab="clients">
<section class="panel">
<div class="section-title">
<h2>Leases and Wi-Fi</h2>
@@ -273,7 +285,7 @@
</section>
</div>
<section class="panel">
<section class="panel" data-device-tab="overview">
<div class="section-title">
<h2>Проблемы</h2>
<div class="section-actions">
@@ -284,7 +296,7 @@
<div id="alertList" class="table-list"></div>
</section>
<section class="panel">
<section class="panel" data-device-tab="overview">
<div class="section-title">
<h2>История мониторинга</h2>
<button id="reloadMetricsHistoryBtn" type="button">Обновить</button>
@@ -292,7 +304,7 @@
<div id="metricsHistory" class="table-list"></div>
</section>
<section class="panel">
<section class="panel" data-device-tab="expert">
<div class="section-title">
<h2>UCI configuration</h2>
</div>
@@ -336,7 +348,7 @@
</div>
</section>
<section class="panel">
<section class="panel" data-device-tab="expert">
<div class="section-title">
<h2>Packages</h2>
</div>
@@ -353,7 +365,7 @@
</div>
</section>
<section class="panel">
<section id="remoteAccessPanel" class="panel" data-device-tab="operations">
<div class="section-title">
<h2>Remote access</h2>
<div class="section-actions">
@@ -402,7 +414,7 @@
<div id="remoteSessionList" class="table-list remote-session-list"></div>
</section>
<section id="commandsPanel" class="panel">
<section class="panel" data-device-tab="operations">
<div class="section-title">
<h2>UCI presets</h2>
</div>
@@ -448,7 +460,7 @@
</div>
</section>
<section class="panel">
<section id="commandsPanel" class="panel" data-device-tab="expert">
<div class="section-title">
<h2>История команд</h2>
<div class="section-actions">
@@ -468,7 +480,7 @@
<div id="commandList" class="table-list"></div>
</section>
<section id="commandDetailPanel" class="panel is-hidden">
<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>
@@ -477,7 +489,7 @@
<pre id="commandDetailOutput" class="command-detail-output"></pre>
</section>
<section class="panel">
<section class="panel" data-device-tab="expert">
<div class="section-title">
<h2>Audit</h2>
<div class="section-actions">
+68 -11
View File
@@ -181,6 +181,48 @@ select {
margin-bottom: 18px;
}
.device-header {
min-height: 82px;
margin-bottom: 0;
}
.device-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.device-tabs {
display: flex;
gap: 4px;
overflow-x: auto;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
margin: 0 -28px 18px;
padding: 0 28px;
}
.device-tab {
min-height: 46px;
border: 0;
border-bottom: 3px solid transparent;
border-radius: 0;
background: transparent;
color: var(--muted);
font-weight: 700;
white-space: nowrap;
}
.device-tab:hover,
.device-tab.is-active {
border-color: var(--accent);
color: var(--text);
}
.device-tab.is-active {
color: var(--accent);
}
.eyebrow {
margin: 0 0 4px;
color: var(--muted);
@@ -490,18 +532,18 @@ select {
}
.health-item.ok {
border-color: #abefc6;
background: #f6fef9;
border-color: #3b663e;
background: #203329;
}
.health-item.warn {
border-color: #fedf89;
background: #fffbeb;
border-color: #715d31;
background: #342e22;
}
.health-item.bad {
border-color: #fecdca;
background: #fffbfa;
border-color: #713d3d;
background: #382525;
}
.diagnostic-grid {
@@ -792,11 +834,15 @@ select {
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-bottom: 14px;
}
.secondary-metric {
display: none;
}
.metric {
min-width: 0;
border: 1px solid var(--line);
@@ -838,8 +884,8 @@ select {
}
.alert-row.warning {
border-color: #fedf89;
background: #fffbeb;
border-color: #715d31;
background: #342e22;
}
.alert-row {
@@ -847,8 +893,8 @@ select {
}
.alert-row.critical {
border-color: #fecdca;
background: #fffbfa;
border-color: #713d3d;
background: #382525;
}
.alert-row.warning span {
@@ -1046,6 +1092,17 @@ select {
grid-column: 1 / -1;
}
.device-header {
align-items: flex-start;
flex-direction: column;
padding: 14px 0;
}
.device-header-actions {
width: 100%;
flex-wrap: wrap;
}
.row,
.row.audit,
.command-form,