feat: создать современный интерфейс с боковым меню

- Полностью переработан дизайн интерфейса
- Добавлено боковое меню слева с контролами
- Область логов перемещена вправо
- Добавлены иконки Font Awesome
- Современный CSS с переменными и анимациями
- Адаптивный дизайн для мобильных устройств
- Улучшенная навигация по контейнерам
- Современные кнопки и элементы управления
- Поддержка темной и светлой темы
- Индикаторы статуса контейнеров

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов 2025-08-16 12:34:44 +03:00
parent a1572d470c
commit 43f19d32e1

View File

@ -3,33 +3,400 @@
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>LogBoard+ — compose logs</title>
<title>LogBoard+ — Modern Log Viewer</title>
<meta name="x-token" content="__TOKEN__"/>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
/* THEME TOKENS */
:root{
--bg:#0e0f13; --panel:#151821; --muted:#8b94a8; --accent:#7aa2f7; --ok:#9ece6a; --warn:#e0af68; --err:#f7768e; --fg:#e5e9f0;
--border:#2a2f3a; --tab:#1b2030; --tab-active:#22283a; --chip:#2b3142; --link:#9ab8ff;
--sidebar-width: 280px; --header-height: 60px;
}
:root[data-theme="light"]{
--bg:#f7f9fc; --panel:#ffffff; --muted:#667085; --accent:#3b82f6; --ok:#15803d; --warn:#b45309; --err:#b91c1c; --fg:#0f172a;
--border:#e5e7eb; --tab:#eef2ff; --tab-active:#dbeafe; --chip:#eef2f7; --link:#1d4ed8;
}
*{box-sizing:border-box} html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--fg);font:13px/1.45 ui-monospace,Menlo,Consolas,monospace}
*{box-sizing:border-box}
html,body{height:100%; margin: 0; padding: 0;}
body{background:var(--bg);color:var(--fg);font:13px/1.45 ui-monospace,Menlo,Consolas,monospace; overflow: hidden;}
a{color:var(--link)}
header{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--panel);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:10}
header h1{font-size:14px;margin:0;color:var(--muted)}
.controls{margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.controls label{display:flex;align-items:center;gap:6px;color:var(--muted);font-size:12px}
select,button,input[type="text"]{
background:var(--chip);color:var(--fg);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:12px}
button{cursor:pointer} button.primary{background:var(--accent);color:#0b0d12;border:none}
#tabs{display:flex;gap:6px;padding:8px 10px;background:var(--tab);border-bottom:1px solid var(--border);overflow:auto}
.tab{border:1px solid var(--border);background:var(--chip);color:var(--fg);padding:6px 10px;border-radius:999px;cursor:pointer;white-space:nowrap;font-size:12px}
.tab.active{background:var(--tab-active);border-color:var(--accent);color:var(--accent)}
main{height:calc(100% - 110px);display:grid;grid-template-columns:1fr;grid-auto-rows:1fr;gap:8px;padding:8px}
/* Modern Layout */
.app-container {
display: flex;
height: 100vh;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: var(--sidebar-width);
background: var(--panel);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 20px;
border-bottom: 1px solid var(--border);
background: var(--panel);
}
.sidebar-header h1 {
font-size: 18px;
margin: 0 0 8px 0;
color: var(--accent);
font-weight: 600;
}
.sidebar-header .subtitle {
font-size: 12px;
color: var(--muted);
margin: 0;
}
/* Sidebar Controls */
.sidebar-controls {
padding: 16px;
border-bottom: 1px solid var(--border);
}
.control-group {
margin-bottom: 16px;
}
.control-group:last-child {
margin-bottom: 0;
}
.control-group label {
display: block;
font-size: 11px;
color: var(--muted);
margin-bottom: 6px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.control-group select,
.control-group input[type="text"] {
width: 100%;
background: var(--chip);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
transition: border-color 0.2s ease;
}
.control-group select:focus,
.control-group input[type="text"]:focus {
outline: none;
border-color: var(--accent);
}
/* Checkbox Groups */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.checkbox-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--muted);
}
.checkbox-item input[type="checkbox"] {
margin: 0;
}
/* Buttons */
.btn {
background: var(--chip);
color: var(--fg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 6px;
}
.btn:hover {
background: var(--tab-active);
border-color: var(--accent);
}
.btn-primary {
background: var(--accent);
color: #0b0d12;
border-color: var(--accent);
font-weight: 500;
}
.btn-primary:hover {
background: #6b8fd8;
}
.btn-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
/* Main Content */
.main-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Header */
.header {
height: var(--header-height);
background: var(--panel);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
gap: 16px;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: var(--fg);
margin: 0;
}
.header-badge {
background: var(--chip);
color: var(--muted);
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
border: 1px solid var(--border);
}
.header-controls {
margin-left: auto;
display: flex;
align-items: center;
gap: 12px;
}
/* Theme Toggle */
.theme-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
}
.theme-toggle input {
appearance: none;
width: 40px;
height: 20px;
border-radius: 999px;
position: relative;
background: var(--chip);
border: 1px solid var(--border);
cursor: pointer;
transition: background 0.2s ease;
}
.theme-toggle input::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--fg);
transition: transform 0.2s ease;
}
.theme-toggle input:checked::after {
transform: translateX(20px);
}
.theme-toggle input:checked {
background: var(--accent);
}
/* Container List */
.container-list {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.container-item {
background: var(--chip);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
}
.container-item:hover {
background: var(--tab-active);
border-color: var(--accent);
}
.container-item.active {
background: var(--tab-active);
border-color: var(--accent);
color: var(--accent);
}
.container-item.active::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--accent);
border-radius: 0 2px 2px 0;
}
.container-name {
font-size: 13px;
font-weight: 500;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 8px;
}
.container-service {
font-size: 11px;
color: var(--muted);
margin-bottom: 4px;
}
.container-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 10px;
}
.status-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ok);
}
.status-indicator.running { background: var(--ok); }
.status-indicator.stopped { background: var(--err); }
.status-indicator.paused { background: var(--warn); }
/* Log Area */
.log-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.log-header {
padding: 12px 20px;
background: var(--panel);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 12px;
}
.log-title {
font-size: 14px;
font-weight: 500;
color: var(--fg);
margin: 0;
}
.log-controls {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
}
.log-content {
flex: 1;
overflow: auto;
padding: 16px;
background: var(--bg);
}
.log {
white-space: pre-wrap;
word-break: break-word;
font-size: 12px;
line-height: 1.5;
margin: 0;
tab-size: 2;
}
/* Responsive */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: absolute;
z-index: 1000;
height: 100%;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
width: 100%;
}
.mobile-toggle {
display: block;
}
}
.mobile-toggle {
display: none;
background: none;
border: none;
color: var(--fg);
font-size: 18px;
cursor: pointer;
padding: 8px;
}
/* Legacy styles for compatibility */
#tabs{display:none}
main{display:none}
.grid-1{grid-template-columns:1fr}
.grid-2{grid-template-columns:1fr 1fr}
.grid-3{grid-template-columns:1fr 1fr 1fr}
@ -69,52 +436,140 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</style>
</head>
<body>
<header>
<h1>LogBoard+</h1>
<span id="projectBadge" class="badge">project: <em>all</em></span>
<div class="controls">
<label>Projects:
<div class="app-container">
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h1><i class="fas fa-terminal"></i> LogBoard+</h1>
<p class="subtitle">Modern Log Viewer</p>
</div>
<div class="sidebar-controls">
<div class="control-group">
<label>Project</label>
<select id="projectSelect">
<option value="all">All Projects</option>
</select>
</label>
<span class="filterlvl">
<label><input type="checkbox" id="lvlDebug" checked>DEBUG</label>
<label><input type="checkbox" id="lvlInfo" checked>INFO</label>
<label><input type="checkbox" id="lvlWarn" checked>WARN</label>
<label><input type="checkbox" id="lvlErr" checked>ERROR</label>
</span>
<label>Tail:
</div>
<div class="control-group">
<label>Log Levels</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="lvlDebug" checked>
<label for="lvlDebug">DEBUG</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="lvlInfo" checked>
<label for="lvlInfo">INFO</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="lvlWarn" checked>
<label for="lvlWarn">WARN</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="lvlErr" checked>
<label for="lvlErr">ERROR</label>
</div>
</div>
</div>
<div class="control-group">
<label>Tail Lines</label>
<select id="tail">
<option value="200">200</option>
<option value="500" selected>500</option>
<option value="1000">1000</option>
<option value="0">all</option>
<option value="0">All</option>
</select>
</label>
<label><input id="autoscroll" type="checkbox" checked/> auto</label>
<label><input id="wrap" type="checkbox" checked/> wrap</label>
<label><input id="pause" type="checkbox"/> pause</label>
<label><input id="aggregate" type="checkbox"/> aggregate</label>
<label class="theme-toggle" title="Dark / Light">
<span>theme</span>
<input id="themeSwitch" type="checkbox" />
</label>
<input id="filter" type="text" placeholder="filter (regex)…"/>
<button id="snapshot">snapshot</button>
<button id="groupBtn">group</button>
<button id="clear">clear</button>
<button id="refresh">refresh</button>
<span id="wsstate" class="badge">ws: off</span>
<span id="layoutBadge" class="badge">view: tabs</span>
</div>
</header>
<div id="tabs"></div>
<div id="idFilters" style="padding:6px 10px; display:flex; gap:6px; flex-wrap:wrap;"></div>
<main id="grid" class="grid-1"></main>
<div class="control-group">
<label>Options</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="autoscroll" checked>
<label for="autoscroll">Auto-scroll</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="wrap" checked>
<label for="wrap">Wrap text</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="pause">
<label for="pause">Pause</label>
</div>
</div>
</div>
<div class="control-group">
<label>Filter</label>
<input id="filter" type="text" placeholder="Filter logs (regex)…"/>
</div>
<div class="control-group">
<label>Actions</label>
<div class="btn-group">
<button id="refresh" class="btn"><i class="fas fa-sync-alt"></i> Refresh</button>
<button id="clear" class="btn"><i class="fas fa-trash"></i> Clear</button>
<button id="snapshot" class="btn"><i class="fas fa-download"></i> Snapshot</button>
</div>
</div>
</div>
<!-- Container List -->
<div class="container-list" id="containerList">
<div class="container-item placeholder">
<div class="container-name">
<i class="fas fa-info-circle"></i>
Loading containers...
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Header -->
<div class="header">
<button class="mobile-toggle" id="mobileToggle">
<i class="fas fa-bars"></i>
</button>
<h2 class="header-title">Logs</h2>
<span class="header-badge" id="projectBadge">All Projects</span>
<div class="header-controls">
<div class="theme-toggle">
<span>Theme</span>
<input id="themeSwitch" type="checkbox" />
</div>
<span id="wsstate">ws: off</span>
</div>
</div>
<!-- Log Area -->
<div class="log-area">
<div class="log-header">
<h3 class="log-title" id="logTitle">Select a container to view logs</h3>
<div class="log-controls">
<span class="counter">DEBUG: <span class="cdbg">0</span></span>
<span class="counter">INFO: <span class="cinfo">0</span></span>
<span class="counter">WARN: <span class="cwarn">0</span></span>
<span class="counter">ERROR: <span class="cerr">0</span></span>
</div>
</div>
<div class="log-content">
<pre class="log" id="logContent">No container selected</pre>
</div>
</div>
</div>
</div>
<!-- Legacy elements for compatibility -->
<div id="tabs" style="display: none;"></div>
<div id="idFilters" style="display: none;"></div>
<main id="grid" style="display: none;"></main>
<button id="copyFab" class="copy-fab" type="button">копировать</button>
<footer>© LogBoard+</footer>
<footer style="display: none;">© LogBoard+</footer>
<script>
const state = {
@ -128,6 +583,7 @@ const state = {
};
const els = {
// Legacy elements
tabs: document.getElementById('tabs'),
grid: document.getElementById('grid'),
tail: document.getElementById('tail'),
@ -150,6 +606,12 @@ const els = {
themeSwitch: document.getElementById('themeSwitch'),
copyFab: document.getElementById('copyFab'),
groupBtn: document.getElementById('groupBtn'),
// New modern elements
containerList: document.getElementById('containerList'),
logTitle: document.getElementById('logTitle'),
logContent: document.getElementById('logContent'),
mobileToggle: document.getElementById('mobileToggle'),
};
// ----- Theme toggle -----
@ -284,6 +746,7 @@ function panelTemplate(svc){
}
function buildTabs(){
// Legacy tabs (hidden)
els.tabs.innerHTML='';
state.services.forEach(svc=>{
const b = document.createElement('button');
@ -293,6 +756,38 @@ function buildTabs(){
b.onclick = ()=> switchToSingle(svc);
els.tabs.appendChild(b);
});
// Modern container list
els.containerList.innerHTML = '';
state.services.forEach(svc => {
const item = document.createElement('div');
item.className = 'container-item';
if (state.current && svc.id === state.current.id) {
item.classList.add('active');
}
item.setAttribute('data-cid', svc.id);
const statusClass = svc.status === 'running' ? 'running' :
svc.status === 'stopped' ? 'stopped' : 'paused';
item.innerHTML = `
<div class="container-name">
<i class="fas fa-cube"></i>
${escapeHtml(svc.name)}
</div>
<div class="container-service">
${escapeHtml(svc.service || svc.name)}
${svc.project ? ` • ${escapeHtml(svc.project)}` : ''}
</div>
<div class="container-status">
<span class="status-indicator ${statusClass}"></span>
${escapeHtml(svc.status)}
</div>
`;
item.onclick = () => switchToSingle(svc);
els.containerList.appendChild(item);
});
}
function setLayout(cls){
@ -425,6 +920,15 @@ function openWs(svc, panel){
}
obj.logEl.insertAdjacentHTML('beforeend', html);
if (els.autoscroll.checked) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
// Update modern interface
if (state.current && state.current.id === id) {
els.logContent.innerHTML = obj.logEl.innerHTML;
const logContent = document.querySelector('.log-content');
if (els.autoscroll.checked) {
logContent.scrollTop = logContent.scrollHeight;
}
}
}
}
@ -450,6 +954,7 @@ function ensurePanel(svc){
}
function switchToSingle(svc){
// Legacy functionality
setLayout('tabs');
els.grid.innerHTML='';
const panel = ensurePanel(svc);
@ -459,6 +964,19 @@ function switchToSingle(svc){
state.current = svc;
buildTabs();
for (const p of [...els.grid.children]) if (p!==panel) p.remove();
// Modern interface updates
els.logTitle.textContent = `${svc.name} (${svc.service || svc.name})`;
els.logContent.textContent = 'Connecting...';
// Update active state in container list
document.querySelectorAll('.container-item').forEach(item => {
item.classList.remove('active');
});
const activeItem = document.querySelector(`.container-item[data-cid="${svc.id}"]`);
if (activeItem) {
activeItem.classList.add('active');
}
}
function openMulti(ids){
@ -585,7 +1103,18 @@ els.groupBtn.onclick = ()=>{
els.clearBtn.onclick = ()=> Object.values(state.open).forEach(o=> o.logEl.textContent='');
els.refreshBtn.onclick = fetchServices;
els.projectSelect.onchange = fetchServices;
els.snapshotBtn.onclick = ()=>{ if (state.current) sendSnapshot(state.current.id); };
// Mobile menu toggle
els.mobileToggle.onclick = () => {
document.querySelector('.sidebar').classList.toggle('open');
};
els.snapshotBtn.onclick = ()=>{
if (state.current) {
sendSnapshot(state.current.id);
} else {
alert('No container selected');
}
};
els.tail.onchange = ()=> {
Object.keys(state.open).forEach(id=>{
const svc = state.services.find(s=> s.id===id);
@ -596,7 +1125,10 @@ els.tail.onchange = ()=> {
closeWs(id); openWs(svc, panel);
});
};
els.wrapToggle.onchange = ()=> document.querySelectorAll('.log').forEach(el=> el.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre');
els.wrapToggle.onchange = ()=> {
document.querySelectorAll('.log').forEach(el=> el.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre');
els.logContent.style.whiteSpace = els.wrapToggle.checked ? 'pre-wrap' : 'pre';
};
els.filter.oninput = ()=> { state.filter = els.filter.value.trim(); };
els.lvlDebug.onchange = ()=> state.levels.debug = els.lvlDebug.checked;
els.lvlInfo.onchange = ()=> state.levels.info = els.lvlInfo.checked;