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>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
<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__"/>
|
<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">
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||||
<style>
|
<style>
|
||||||
@ -110,20 +110,39 @@ a{color:var(--link)}
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Цвета для ws состояния */
|
/* Кнопка состояния WebSocket */
|
||||||
#wsstate.ws-on {
|
.ws-status-btn {
|
||||||
color: var(--ok);
|
background: var(--chip);
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 11px;
|
||||||
font-weight: 500;
|
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 {
|
.ws-status-btn.ws-on {
|
||||||
color: var(--err);
|
background: var(--ok);
|
||||||
font-weight: 500;
|
color: #0b0d12;
|
||||||
|
border-color: var(--ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
#wsstate.ws-err {
|
.ws-status-btn.ws-off {
|
||||||
color: var(--warn);
|
background: var(--err);
|
||||||
font-weight: 500;
|
color: #0b0d12;
|
||||||
|
border-color: var(--err);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-status-btn.ws-err {
|
||||||
|
background: var(--warn);
|
||||||
|
color: #0b0d12;
|
||||||
|
border-color: var(--warn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sidebar Controls */
|
/* Sidebar Controls */
|
||||||
@ -271,6 +290,22 @@ a{color:var(--link)}
|
|||||||
gap: 8px;
|
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 {
|
.checkbox-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -279,6 +314,16 @@ a{color:var(--link)}
|
|||||||
color: var(--muted);
|
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"] {
|
.checkbox-item input[type="checkbox"] {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
@ -320,6 +365,13 @@ a{color:var(--link)}
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-group.actions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Main Content */
|
/* Main Content */
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -614,7 +666,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="subtitle">Modern Log Viewer</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-controls">
|
<div class="sidebar-controls">
|
||||||
@ -640,7 +692,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-content" id="levels-content">
|
<div class="control-content" id="levels-content">
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group levels-grid">
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="lvlDebug" checked>
|
<input type="checkbox" id="lvlDebug" checked>
|
||||||
<label for="lvlDebug">DEBUG</label>
|
<label for="lvlDebug">DEBUG</label>
|
||||||
@ -686,7 +738,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-content" id="options-content">
|
<div class="control-content" id="options-content">
|
||||||
<div class="checkbox-group">
|
<div class="checkbox-group options-grid">
|
||||||
<div class="checkbox-item">
|
<div class="checkbox-item">
|
||||||
<input type="checkbox" id="autoscroll" checked>
|
<input type="checkbox" id="autoscroll" checked>
|
||||||
<label for="autoscroll">Auto-scroll</label>
|
<label for="autoscroll">Auto-scroll</label>
|
||||||
@ -723,7 +775,7 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-content" id="actions-content">
|
<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="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="clear" class="btn"><i class="fas fa-trash"></i> Clear</button>
|
||||||
<button id="snapshot" class="btn"><i class="fas fa-download"></i> Snapshot</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>
|
<span>Theme</span>
|
||||||
<input id="themeSwitch" type="checkbox" />
|
<input id="themeSwitch" type="checkbox" />
|
||||||
</div>
|
</div>
|
||||||
<span id="wsstate">ws: off</span>
|
<button id="wsstate" class="ws-status-btn">ws: off</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -865,25 +917,22 @@ function refreshAllLogs() {
|
|||||||
const obj = state.open[id];
|
const obj = state.open[id];
|
||||||
if (!obj || !obj.logEl) return;
|
if (!obj || !obj.logEl) return;
|
||||||
|
|
||||||
// Получаем все строки логов из буфера
|
// Получаем все логи из буфера
|
||||||
const allLines = obj.allLogs || [];
|
const allLogs = obj.allLogs || [];
|
||||||
const filteredLines = [];
|
const filteredHtml = [];
|
||||||
|
|
||||||
allLines.forEach(line => {
|
allLogs.forEach(logEntry => {
|
||||||
if (line.trim() === '') return;
|
|
||||||
|
|
||||||
// Проверяем уровень логирования
|
// Проверяем уровень логирования
|
||||||
const cls = classify(line);
|
if (!allowedByLevel(logEntry.cls)) return;
|
||||||
if (!allowedByLevel(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) {
|
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 (/\berr(or)?\b|level=error| \[error\]/.test(l)) return 'err';
|
||||||
if (/\bwarn(ing)?\b|level=warn| \[warn\]/.test(l)) return 'warn';
|
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';
|
if (/\b(info|started|listening|ready|up)\b|level=info/.test(l)) return 'ok';
|
||||||
return '';
|
return 'other'; // Изменено с пустой строки на 'other'
|
||||||
}
|
}
|
||||||
function allowedByLevel(cls){
|
function allowedByLevel(cls){
|
||||||
if (cls==='dbg') return state.levels.debug;
|
if (cls==='dbg') return state.levels.debug;
|
||||||
if (cls==='err') return state.levels.err;
|
if (cls==='err') return state.levels.err;
|
||||||
if (cls==='warn') return state.levels.warn;
|
if (cls==='warn') return state.levels.warn;
|
||||||
if (cls==='ok') return state.levels.info;
|
if (cls==='ok') return state.levels.info;
|
||||||
|
if (cls==='other') return true; // Всегда показываем неклассифицированные строки
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function applyFilter(line){
|
function applyFilter(line){
|
||||||
@ -1223,33 +1273,34 @@ function openWs(svc, panel){
|
|||||||
const obj = state.open[id];
|
const obj = state.open[id];
|
||||||
if (!obj) return;
|
if (!obj) return;
|
||||||
|
|
||||||
// Сохраняем все логи в буфере
|
// Сохраняем все логи в буфере (всегда)
|
||||||
if (!obj.allLogs) obj.allLogs = [];
|
if (!obj.allLogs) obj.allLogs = [];
|
||||||
obj.allLogs.push(html);
|
obj.allLogs.push({html: html, line: line, cls: cls});
|
||||||
|
|
||||||
// Ограничиваем размер буфера
|
// Ограничиваем размер буфера
|
||||||
if (obj.allLogs.length > 10000) {
|
if (obj.allLogs.length > 10000) {
|
||||||
obj.allLogs = obj.allLogs.slice(-5000);
|
obj.allLogs = obj.allLogs.slice(-5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем фильтры
|
// Проверяем фильтры для отображения
|
||||||
if (!allowedByLevel(cls)) return;
|
if (!allowedByLevel(cls)) return;
|
||||||
if (!applyFilter(line)) return;
|
if (!applyFilter(line)) return;
|
||||||
|
|
||||||
if (els.pause.checked){
|
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();
|
if (obj.pausedBuffer.length>5000) obj.pausedBuffer.shift();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.logEl.insertAdjacentHTML('beforeend', html);
|
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
|
// 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;
|
els.logContent.innerHTML = obj.logEl.innerHTML;
|
||||||
const logContent = document.querySelector('.log-content');
|
const logContent = document.querySelector('.log-content');
|
||||||
if (els.autoscroll.checked) {
|
if (logContent && els.autoscroll.checked) {
|
||||||
logContent.scrollTop = logContent.scrollHeight;
|
logContent.scrollTop = logContent.scrollHeight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1448,6 +1499,27 @@ els.clearBtn.onclick = ()=> {
|
|||||||
els.refreshBtn.onclick = async () => {
|
els.refreshBtn.onclick = async () => {
|
||||||
console.log('Refreshing services...');
|
console.log('Refreshing services...');
|
||||||
await fetchServices();
|
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;
|
els.projectSelect.onchange = fetchServices;
|
||||||
|
|
||||||
@ -1621,13 +1693,16 @@ els.autoscroll.onchange = ()=> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
els.pause.onchange = ()=> {
|
els.pause.onchange = ()=> {
|
||||||
// При снятии паузы показываем накопленные логи
|
// При снятии паузы показываем накопленные логи с учетом фильтров
|
||||||
if (!els.pause.checked) {
|
if (!els.pause.checked) {
|
||||||
Object.keys(state.open).forEach(id => {
|
Object.keys(state.open).forEach(id => {
|
||||||
const obj = state.open[id];
|
const obj = state.open[id];
|
||||||
if (obj && obj.pausedBuffer && obj.pausedBuffer.length > 0) {
|
if (obj && obj.pausedBuffer && obj.pausedBuffer.length > 0) {
|
||||||
obj.pausedBuffer.forEach(html => {
|
obj.pausedBuffer.forEach(logEntry => {
|
||||||
obj.logEl.insertAdjacentHTML('beforeend', html);
|
// Проверяем фильтры для каждого логированного сообщения
|
||||||
|
if (allowedByLevel(logEntry.cls) && applyFilter(logEntry.line)) {
|
||||||
|
obj.logEl.insertAdjacentHTML('beforeend', logEntry.html);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
obj.pausedBuffer = [];
|
obj.pausedBuffer = [];
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user