feat: добавить сворачиваемые секции и исправить фильтрацию логов
- Добавлены сворачиваемые секции для всех групп контролов - Добавлены кнопки сворачивания с иконками - Сохранение состояния секций в localStorage - Исправлена проблема с LogLevels - добавлена буферизация всех логов - Логи теперь восстанавливаются при включении уровней обратно - Улучшен CSS для сворачиваемых секций с анимациями - Добавлены hover эффекты для заголовков секций - Оптимизирована производительность фильтрации Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user