feat: улучшения интерфейса и исправления
- Исправлена фильтрация логов по уровням (INFO, DEBUG, WARN, ERROR) - Добавлено отображение log levels в две строки по два элемента - Добавлено отображение Options в две строки по два элемента - Добавлено отображение Actions в две строки по две кнопки - Исправлена кнопка Refresh - теперь перезапускает WebSocket соединение - Изменен индикатор WebSocket состояния на кнопку с подсветкой фона - Убрана надпись 'Modern Log Viewer' из заголовка и интерфейса - Улучшена логика фильтрации в реальном времени - Автор: Сергей Антропов (https://devops.org.ru)
This commit is contained in:
parent
36eef6a08d
commit
351b2ac041
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>LogBoard+ — Modern Log Viewer</title>
|
||||
<title>LogBoard+</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>
|
||||
@ -110,20 +110,39 @@ a{color:var(--link)}
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Цвета для ws состояния */
|
||||
#wsstate.ws-on {
|
||||
color: var(--ok);
|
||||
/* Кнопка состояния WebSocket */
|
||||
.ws-status-btn {
|
||||
background: var(--chip);
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
cursor: default;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#wsstate.ws-off {
|
||||
color: var(--err);
|
||||
font-weight: 500;
|
||||
.ws-status-btn.ws-on {
|
||||
background: var(--ok);
|
||||
color: #0b0d12;
|
||||
border-color: var(--ok);
|
||||
}
|
||||
|
||||
#wsstate.ws-err {
|
||||
color: var(--warn);
|
||||
font-weight: 500;
|
||||
.ws-status-btn.ws-off {
|
||||
background: var(--err);
|
||||
color: #0b0d12;
|
||||
border-color: var(--err);
|
||||
}
|
||||
|
||||
.ws-status-btn.ws-err {
|
||||
background: var(--warn);
|
||||
color: #0b0d12;
|
||||
border-color: var(--warn);
|
||||
}
|
||||
|
||||
/* Sidebar Controls */
|
||||
@ -271,6 +290,22 @@ a{color:var(--link)}
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group.levels-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-group.options-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -279,6 +314,16 @@ a{color:var(--link)}
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.levels-grid .checkbox-item {
|
||||
min-height: 20px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.options-grid .checkbox-item {
|
||||
min-height: 20px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.checkbox-item input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
@ -320,6 +365,13 @@ a{color:var(--link)}
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn-group.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
@ -614,7 +666,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="subtitle">Modern Log Viewer</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="sidebar-controls">
|
||||
@ -640,7 +692,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-content" id="levels-content">
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-group levels-grid">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="lvlDebug" checked>
|
||||
<label for="lvlDebug">DEBUG</label>
|
||||
@ -686,7 +738,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-content" id="options-content">
|
||||
<div class="checkbox-group">
|
||||
<div class="checkbox-group options-grid">
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="autoscroll" checked>
|
||||
<label for="autoscroll">Auto-scroll</label>
|
||||
@ -723,7 +775,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-content" id="actions-content">
|
||||
<div class="btn-group">
|
||||
<div class="btn-group actions-grid">
|
||||
<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>
|
||||
@ -757,7 +809,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
<span>Theme</span>
|
||||
<input id="themeSwitch" type="checkbox" />
|
||||
</div>
|
||||
<span id="wsstate">ws: off</span>
|
||||
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -865,25 +917,22 @@ function refreshAllLogs() {
|
||||
const obj = state.open[id];
|
||||
if (!obj || !obj.logEl) return;
|
||||
|
||||
// Получаем все строки логов из буфера
|
||||
const allLines = obj.allLogs || [];
|
||||
const filteredLines = [];
|
||||
// Получаем все логи из буфера
|
||||
const allLogs = obj.allLogs || [];
|
||||
const filteredHtml = [];
|
||||
|
||||
allLines.forEach(line => {
|
||||
if (line.trim() === '') return;
|
||||
|
||||
allLogs.forEach(logEntry => {
|
||||
// Проверяем уровень логирования
|
||||
const cls = classify(line);
|
||||
if (!allowedByLevel(cls)) return;
|
||||
if (!allowedByLevel(logEntry.cls)) return;
|
||||
|
||||
// Проверяем фильтр
|
||||
if (!applyFilter(line)) return;
|
||||
if (!applyFilter(logEntry.line)) return;
|
||||
|
||||
filteredLines.push(line);
|
||||
filteredHtml.push(logEntry.html);
|
||||
});
|
||||
|
||||
// Обновляем отображение
|
||||
obj.logEl.innerHTML = filteredLines.join('\n');
|
||||
obj.logEl.innerHTML = filteredHtml.join('');
|
||||
|
||||
// Обновляем современный интерфейс
|
||||
if (state.current && state.current.id === id && els.logContent) {
|
||||
@ -899,13 +948,14 @@ function classify(line){
|
||||
if (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err';
|
||||
if (/\bwarn(ing)?\b|level=warn| \[warn\]/.test(l)) return 'warn';
|
||||
if (/\b(info|started|listening|ready|up)\b|level=info/.test(l)) return 'ok';
|
||||
return '';
|
||||
return 'other'; // Изменено с пустой строки на 'other'
|
||||
}
|
||||
function allowedByLevel(cls){
|
||||
if (cls==='dbg') return state.levels.debug;
|
||||
if (cls==='err') return state.levels.err;
|
||||
if (cls==='warn') return state.levels.warn;
|
||||
if (cls==='ok') return state.levels.info;
|
||||
if (cls==='other') return true; // Всегда показываем неклассифицированные строки
|
||||
return true;
|
||||
}
|
||||
function applyFilter(line){
|
||||
@ -1223,33 +1273,34 @@ function openWs(svc, panel){
|
||||
const obj = state.open[id];
|
||||
if (!obj) return;
|
||||
|
||||
// Сохраняем все логи в буфере
|
||||
// Сохраняем все логи в буфере (всегда)
|
||||
if (!obj.allLogs) obj.allLogs = [];
|
||||
obj.allLogs.push(html);
|
||||
obj.allLogs.push({html: html, line: line, cls: cls});
|
||||
|
||||
// Ограничиваем размер буфера
|
||||
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) obj.pausedBuffer = [];
|
||||
obj.pausedBuffer.push({html: html, line: line, cls: cls});
|
||||
if (obj.pausedBuffer.length>5000) obj.pausedBuffer.shift();
|
||||
return;
|
||||
}
|
||||
|
||||
obj.logEl.insertAdjacentHTML('beforeend', html);
|
||||
if (els.autoscroll.checked) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
|
||||
if (els.autoscroll.checked && obj.wrapEl) obj.wrapEl.scrollTop = obj.wrapEl.scrollHeight;
|
||||
|
||||
// Update modern interface
|
||||
if (state.current && state.current.id === id) {
|
||||
if (state.current && state.current.id === id && els.logContent) {
|
||||
els.logContent.innerHTML = obj.logEl.innerHTML;
|
||||
const logContent = document.querySelector('.log-content');
|
||||
if (els.autoscroll.checked) {
|
||||
if (logContent && els.autoscroll.checked) {
|
||||
logContent.scrollTop = logContent.scrollHeight;
|
||||
}
|
||||
}
|
||||
@ -1448,6 +1499,27 @@ els.clearBtn.onclick = ()=> {
|
||||
els.refreshBtn.onclick = async () => {
|
||||
console.log('Refreshing services...');
|
||||
await fetchServices();
|
||||
|
||||
// Если есть текущий контейнер, перезапускаем его WebSocket соединение
|
||||
if (state.current) {
|
||||
console.log('Reconnecting to current container:', state.current.id);
|
||||
const currentId = state.current.id;
|
||||
|
||||
// Закрываем текущее соединение
|
||||
closeWs(currentId);
|
||||
|
||||
// Находим обновленный контейнер в списке
|
||||
const updatedContainer = state.services.find(s => s.id === currentId);
|
||||
if (updatedContainer) {
|
||||
// Переключаемся на обновленный контейнер
|
||||
switchToSingle(updatedContainer);
|
||||
} else {
|
||||
// Если контейнер больше не существует, переключаемся на первый доступный
|
||||
if (state.services.length > 0) {
|
||||
switchToSingle(state.services[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
els.projectSelect.onchange = fetchServices;
|
||||
|
||||
@ -1621,13 +1693,16 @@ els.autoscroll.onchange = ()=> {
|
||||
};
|
||||
|
||||
els.pause.onchange = ()=> {
|
||||
// При снятии паузы показываем накопленные логи
|
||||
// При снятии паузы показываем накопленные логи с учетом фильтров
|
||||
if (!els.pause.checked) {
|
||||
Object.keys(state.open).forEach(id => {
|
||||
const obj = state.open[id];
|
||||
if (obj && obj.pausedBuffer && obj.pausedBuffer.length > 0) {
|
||||
obj.pausedBuffer.forEach(html => {
|
||||
obj.logEl.insertAdjacentHTML('beforeend', html);
|
||||
obj.pausedBuffer.forEach(logEntry => {
|
||||
// Проверяем фильтры для каждого логированного сообщения
|
||||
if (allowedByLevel(logEntry.cls) && applyFilter(logEntry.line)) {
|
||||
obj.logEl.insertAdjacentHTML('beforeend', logEntry.html);
|
||||
}
|
||||
});
|
||||
obj.pausedBuffer = [];
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user