feat: добавить сворачиваемые секции и исправить фильтрацию логов

- Добавлены сворачиваемые секции для всех групп контролов
- Добавлены кнопки сворачивания с иконками
- Сохранение состояния секций в localStorage
- Исправлена проблема с LogLevels - добавлена буферизация всех логов
- Логи теперь восстанавливаются при включении уровней обратно
- Улучшен CSS для сворачиваемых секций с анимациями
- Добавлены hover эффекты для заголовков секций
- Оптимизирована производительность фильтрации

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
Сергей Антропов
2025-08-16 12:52:58 +03:00
parent 7c7c92f798
commit 6dfc2d0104

View File

@@ -73,6 +73,77 @@ a{color:var(--link)}
margin-bottom: 0;
}
/* Collapsible sections */
.control-group.collapsible {
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.control-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--chip);
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
}
.control-header:hover {
background: var(--tab-active);
}
.control-header label {
display: block;
font-size: 11px;
color: var(--muted);
margin: 0;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.collapse-btn {
background: none;
border: none;
color: var(--muted);
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.collapse-btn:hover {
background: var(--border);
color: var(--fg);
}
.collapse-btn i {
font-size: 12px;
transition: transform 0.2s ease;
}
.control-group.collapsed .collapse-btn i {
transform: rotate(-90deg);
}
.control-content {
padding: 16px;
transition: all 0.3s ease;
overflow: hidden;
}
.control-group.collapsed .control-content {
padding: 0 16px;
max-height: 0;
opacity: 0;
}
.control-group label {
display: block;
font-size: 11px;
@@ -445,74 +516,116 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
</div>
<div class="sidebar-controls">
<div class="control-group">
<label>Project</label>
<select id="projectSelect">
<option value="all">All Projects</option>
</select>
<div class="control-group collapsible" data-section="project">
<div class="control-header">
<label>Project</label>
<button class="collapse-btn" data-target="project">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="control-content" id="project-content">
<select id="projectSelect">
<option value="all">All Projects</option>
</select>
</div>
</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 class="control-group collapsible" data-section="levels">
<div class="control-header">
<label>Log Levels</label>
<button class="collapse-btn" data-target="levels">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="control-content" id="levels-content">
<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>
<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>
</select>
<div class="control-group collapsible" data-section="tail">
<div class="control-header">
<label>Tail Lines</label>
<button class="collapse-btn" data-target="tail">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="control-content" id="tail-content">
<select id="tail">
<option value="200">200</option>
<option value="500" selected>500</option>
<option value="1000">1000</option>
<option value="0">All</option>
</select>
</div>
</div>
<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 class="control-group collapsible" data-section="options">
<div class="control-header">
<label>Options</label>
<button class="collapse-btn" data-target="options">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="control-content" id="options-content">
<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>
<div class="control-group">
<label>Filter</label>
<input id="filter" type="text" placeholder="Filter logs (regex)…"/>
<div class="control-group collapsible" data-section="filter">
<div class="control-header">
<label>Filter</label>
<button class="collapse-btn" data-target="filter">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="control-content" id="filter-content">
<input id="filter" type="text" placeholder="Filter logs (regex)…"/>
</div>
</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 class="control-group collapsible" data-section="actions">
<div class="control-header">
<label>Actions</label>
<button class="collapse-btn" data-target="actions">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="control-content" id="actions-content">
<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>
</div>
@@ -634,11 +747,11 @@ function refreshAllLogs() {
const obj = state.open[id];
if (!obj || !obj.logEl) return;
// Получаем все строки логов
const lines = obj.logEl.innerHTML.split('\n');
// Получаем все строки логов из буфера
const allLines = obj.allLogs || [];
const filteredLines = [];
lines.forEach(line => {
allLines.forEach(line => {
if (line.trim() === '') return;
// Проверяем уровень логирования
@@ -987,16 +1100,30 @@ function openWs(svc, panel){
if (cls==='ok') counters.info++;
if (cls==='warn') counters.warn++;
if (cls==='err') counters.err++;
if (!allowedByLevel(cls)) return;
if (!applyFilter(line)) return;
const html = `<span class="line ${cls}">${ansiToHtml(line)}</span>\n`;
const obj = state.open[id];
if (!obj) return;
// Сохраняем все логи в буфере
if (!obj.allLogs) obj.allLogs = [];
obj.allLogs.push(html);
// Ограничиваем размер буфера
if (obj.allLogs.length > 10000) {
obj.allLogs = obj.allLogs.slice(-5000);
}
// Проверяем фильтры
if (!allowedByLevel(cls)) return;
if (!applyFilter(line)) return;
if (els.pause.checked){
obj.pausedBuffer.push(html);
if (obj.pausedBuffer.length>5000) obj.pausedBuffer.shift();
return;
}
obj.logEl.insertAdjacentHTML('beforeend', html);
if (els.autoscroll.checked) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
@@ -1188,6 +1315,7 @@ if (els.groupBtn && els.groupBtn.onclick !== null) {
els.clearBtn.onclick = ()=> {
Object.values(state.open).forEach(o => {
if (o.logEl) o.logEl.textContent = '';
if (o.allLogs) o.allLogs = []; // Очищаем буфер логов
});
// Очищаем современный интерфейс
if (els.logContent) {
@@ -1209,6 +1337,45 @@ els.projectSelect.onchange = fetchServices;
els.mobileToggle.onclick = () => {
document.querySelector('.sidebar').classList.toggle('open');
};
// Collapsible sections
document.addEventListener('DOMContentLoaded', () => {
// Обработчики для сворачивания секций
document.querySelectorAll('.control-header').forEach(header => {
header.addEventListener('click', (e) => {
if (e.target.closest('.collapse-btn')) return; // Не сворачиваем при клике на кнопку
const group = header.closest('.control-group');
group.classList.toggle('collapsed');
// Сохраняем состояние в localStorage
const section = group.dataset.section;
localStorage.setItem(`lb_collapsed_${section}`, group.classList.contains('collapsed'));
});
});
// Обработчики для кнопок сворачивания
document.querySelectorAll('.collapse-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const group = btn.closest('.control-group');
group.classList.toggle('collapsed');
// Сохраняем состояние в localStorage
const section = group.dataset.section;
localStorage.setItem(`lb_collapsed_${section}`, group.classList.contains('collapsed'));
});
});
// Восстанавливаем состояние секций из localStorage
document.querySelectorAll('.control-group.collapsible').forEach(group => {
const section = group.dataset.section;
const isCollapsed = localStorage.getItem(`lb_collapsed_${section}`) === 'true';
if (isCollapsed) {
group.classList.add('collapsed');
}
});
});
els.snapshotBtn.onclick = ()=>{
if (state.current) {
sendSnapshot(state.current.id);