Add device workspace tabs
This commit is contained in:
+38
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user