feat: Добавлены кнопки уровней логирования в заголовки single-view и multi-view

- Добавлены кнопки LogLevels в заголовки контейнеров
- Реализована индивидуальная фильтрация логов для каждого контейнера в multi-view режиме
- Исправлена проблема с потерей цветов при фильтрации логов
- Добавлена статистика по уровням логирования для каждого контейнера
- Кнопки показывают количество логов каждого уровня
- Каждый контейнер может иметь свои настройки фильтрации уровней
- Сохранена цветовая схема при переключении уровней логирования
This commit is contained in:
Sergey Antropoff 2025-08-18 14:32:50 +03:00
parent 7007d4be74
commit e5b0c3f553

View File

@ -1244,6 +1244,120 @@ a{color:var(--link)}
flex: 1;
}
/* Кнопки уровней логирования для заголовков */
.single-view-levels,
.multi-view-levels {
display: flex;
gap: 4px;
align-items: center;
}
.level-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: 4px 6px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--panel);
color: var(--fg);
cursor: pointer;
transition: all 0.2s ease;
font-size: 10px;
min-width: 40px;
position: relative;
}
.level-btn:hover {
background: var(--chip);
border-color: var(--accent);
}
.level-btn.active {
background: var(--accent);
color: #0b0d12;
border-color: var(--accent);
}
.level-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.level-btn.disabled:hover {
background: var(--panel);
border-color: var(--border);
}
.level-label {
font-weight: 500;
font-size: 9px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.level-value {
font-weight: 600;
font-size: 11px;
}
/* Цвета для разных уровней */
.level-btn.debug-btn {
border-color: var(--ok);
color: var(--ok);
}
.level-btn.debug-btn:hover,
.level-btn.debug-btn.active {
background: var(--ok);
color: #0b0d12;
}
.level-btn.info-btn {
border-color: var(--accent);
color: var(--accent);
}
.level-btn.info-btn:hover,
.level-btn.info-btn.active {
background: var(--accent);
color: #0b0d12;
}
.level-btn.warn-btn {
border-color: var(--warn);
color: var(--warn);
}
.level-btn.warn-btn:hover,
.level-btn.warn-btn.active {
background: var(--warn);
color: #0b0d12;
}
.level-btn.error-btn {
border-color: var(--err);
color: var(--err);
}
.level-btn.error-btn:hover,
.level-btn.error-btn.active {
background: var(--err);
color: #0b0d12;
}
.level-btn.other-btn {
border-color: var(--muted);
color: var(--muted);
}
.level-btn.other-btn:hover,
.level-btn.other-btn.active {
background: var(--muted);
color: #0b0d12;
}
.single-view-content {
flex: 1;
overflow: hidden;
@ -1856,6 +1970,28 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
<div class="single-view-panel" id="singleViewPanel">
<div class="single-view-header">
<h4 class="single-view-title" id="singleViewTitle">No container selected</h4>
<div class="single-view-levels">
<button class="level-btn debug-btn" data-level="debug" title="DEBUG">
<span class="level-label">DEBUG</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn info-btn" data-level="info" title="INFO">
<span class="level-label">INFO</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn warn-btn" data-level="warn" title="WARN">
<span class="level-label">WARN</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn error-btn" data-level="err" title="ERROR">
<span class="level-label">ERROR</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn other-btn" data-level="other" title="OTHER">
<span class="level-label">OTHER</span>
<span class="level-value" data-container="single">0</span>
</button>
</div>
</div>
<div class="single-view-content">
<pre class="log" id="logContent">No container selected</pre>
@ -2083,6 +2219,218 @@ function allowedByLevel(cls){
if (cls==='other') return state.levels.other; // Показываем неклассифицированные строки в зависимости от настройки
return true;
}
// Функция для проверки уровня логирования для конкретного контейнера
function allowedByContainerLevel(cls, containerId) {
// Если настройки контейнера не инициализированы, инициализируем их
if (!state.containerLevels) {
state.containerLevels = {};
}
if (!state.containerLevels[containerId]) {
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
}
const containerLevels = state.containerLevels[containerId];
let result;
if (cls==='dbg') result = containerLevels.debug;
else if (cls==='err') result = containerLevels.err;
else if (cls==='warn') result = containerLevels.warn;
else if (cls==='ok') result = containerLevels.info;
else if (cls==='other') result = containerLevels.other;
else result = true;
console.log(`allowedByContainerLevel: containerId=${containerId}, cls=${cls}, result=${result}, levels=`, containerLevels);
return result;
}
// Функция для обновления видимости логов в Single View
function updateLogVisibility(logElement) {
if (!logElement || !state.current) return;
const containerId = state.current.id;
const obj = state.open[containerId];
if (!obj || !obj.allLogs) return;
// Пересоздаем содержимое лога с учетом фильтров, сохраняя HTML-разметку
let visibleHtml = '';
obj.allLogs.forEach(logEntry => {
const shouldShow = allowedByLevel(logEntry.cls) && applyFilter(logEntry.line);
if (shouldShow) {
visibleHtml += logEntry.html + '\n';
}
});
logElement.innerHTML = visibleHtml;
// Обновляем счетчики
recalculateCounters();
// Обновляем состояние кнопок уровней логирования только для single-view
const singleLevelBtns = document.querySelectorAll('.single-view-levels .level-btn');
singleLevelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const isActive = state.levels[level];
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
}
// Функция для обновления видимости логов конкретного контейнера в Multi View
function updateContainerLogVisibility(containerId) {
if (!state.multiViewMode) return;
console.log(`updateContainerLogVisibility: Обновляем видимость логов для контейнера ${containerId}`);
const logElement = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
if (!logElement) return;
const obj = state.open[containerId];
if (!obj || !obj.allLogs) return;
// Пересоздаем содержимое лога с учетом фильтров контейнера, сохраняя HTML-разметку
let visibleHtml = '';
obj.allLogs.forEach(logEntry => {
const shouldShow = allowedByContainerLevel(logEntry.cls, containerId) && applyFilter(logEntry.line);
if (shouldShow) {
visibleHtml += logEntry.html + '\n';
}
});
logElement.innerHTML = visibleHtml;
// Обновляем счетчики для этого контейнера
updateContainerCounters(containerId);
// Обновляем состояние кнопок уровней логирования только для этого контейнера
const levelBtns = document.querySelectorAll(`.level-btn[data-container-id="${containerId}"]`);
levelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
// Используем настройки контейнера, если они есть
const containerLevels = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId] : {debug: true, info: true, warn: true, err: true, other: true};
const isActive = containerLevels[level];
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
}
// Функция для обновления счетчиков конкретного контейнера
function updateContainerCounters(containerId) {
const obj = state.open[containerId];
if (!obj || !obj.allLogs) return;
// Получаем значение Tail Lines
const tailLines = parseInt(els.tail.value) || 50;
// Берем только последние N логов
const visibleLogs = obj.allLogs.slice(-tailLines);
// Сбрасываем счетчики
obj.counters = {dbg: 0, info: 0, warn: 0, err: 0, other: 0};
// Пересчитываем счетчики только для отображаемых логов
visibleLogs.forEach(logEntry => {
const shouldShow = allowedByContainerLevel(logEntry.cls, containerId) && applyFilter(logEntry.line);
if (shouldShow) {
if (logEntry.cls === 'dbg') obj.counters.dbg++;
if (logEntry.cls === 'ok') obj.counters.info++;
if (logEntry.cls === 'warn') obj.counters.warn++;
if (logEntry.cls === 'err') obj.counters.err++;
if (logEntry.cls === 'other') obj.counters.other++;
}
});
// Обновляем отображение счетчиков в кнопках заголовка
const levelBtns = document.querySelectorAll(`.level-btn[data-container-id="${containerId}"]`);
levelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const valueEl = btn.querySelector('.level-value');
if (valueEl) {
switch (level) {
case 'debug': valueEl.textContent = obj.counters.dbg; break;
case 'info': valueEl.textContent = obj.counters.info; break;
case 'warn': valueEl.textContent = obj.counters.warn; break;
case 'err': valueEl.textContent = obj.counters.err; break;
case 'other': valueEl.textContent = obj.counters.other; break;
}
}
});
}
// Функция для обновления счетчиков в кнопках заголовков
function updateHeaderCounters(containerId, counters) {
// Обновляем счетчики для single-view (если это текущий контейнер)
if (state.current && state.current.id === containerId) {
const singleLevelBtns = document.querySelectorAll('.single-view-levels .level-btn');
singleLevelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const valueEl = btn.querySelector('.level-value');
if (valueEl) {
switch (level) {
case 'debug': valueEl.textContent = counters.dbg; break;
case 'info': valueEl.textContent = counters.info; break;
case 'warn': valueEl.textContent = counters.warn; break;
case 'err': valueEl.textContent = counters.err; break;
case 'other': valueEl.textContent = counters.other; break;
}
}
});
}
// Обновляем счетчики для multi-view (только для конкретного контейнера)
if (state.multiViewMode && state.selectedContainers.includes(containerId)) {
const multiLevelBtns = document.querySelectorAll(`.level-btn[data-container-id="${containerId}"]`);
multiLevelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const valueEl = btn.querySelector('.level-value');
if (valueEl) {
switch (level) {
case 'debug': valueEl.textContent = counters.dbg; break;
case 'info': valueEl.textContent = counters.info; break;
case 'warn': valueEl.textContent = counters.warn; break;
case 'err': valueEl.textContent = counters.err; break;
case 'other': valueEl.textContent = counters.other; break;
}
}
});
}
}
// Функция для инициализации состояния кнопок уровней логирования
function initializeLevelButtons() {
// Инициализируем кнопки для single-view
const singleLevelBtns = document.querySelectorAll('.single-view-levels .level-btn');
singleLevelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const isActive = state.levels[level];
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
// Инициализируем кнопки для multi-view (если есть)
const multiLevelBtns = document.querySelectorAll('.multi-view-levels .level-btn');
multiLevelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const containerId = btn.getAttribute('data-container-id');
// Инициализируем настройки контейнера, если их нет
if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) {
if (!state.containerLevels) {
state.containerLevels = {};
}
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
}
// Используем настройки контейнера
const isActive = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId][level] : true;
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
}
function applyFilter(line){
if(!state.filter) return true;
try{
@ -2672,6 +3020,11 @@ async function updateMultiViewMode() {
}
console.log(`Multi-view mode updated: multiViewMode = ${state.multiViewMode}`);
// Обновляем состояние кнопок уровней логирования при переключении режимов
setTimeout(() => {
initializeLevelButtons();
}, 100);
}
async function setupMultiView() {
@ -2799,6 +3152,28 @@ function createMultiViewPanel(service) {
panel.innerHTML = `
<div class="multi-view-header">
<h4 class="multi-view-title">${escapeHtml(service.name)}</h4>
<div class="multi-view-levels">
<button class="level-btn debug-btn" data-level="debug" data-container-id="${service.id}" title="DEBUG">
<span class="level-label">DEBUG</span>
<span class="level-value" data-container="${service.id}">0</span>
</button>
<button class="level-btn info-btn" data-level="info" data-container-id="${service.id}" title="INFO">
<span class="level-label">INFO</span>
<span class="level-value" data-container="${service.id}">0</span>
</button>
<button class="level-btn warn-btn" data-level="warn" data-container-id="${service.id}" title="WARN">
<span class="level-label">WARN</span>
<span class="level-value" data-container="${service.id}">0</span>
</button>
<button class="level-btn error-btn" data-level="err" data-container-id="${service.id}" title="ERROR">
<span class="level-label">ERROR</span>
<span class="level-value" data-container="${service.id}">0</span>
</button>
<button class="level-btn other-btn" data-level="other" data-container-id="${service.id}" title="OTHER">
<span class="level-label">OTHER</span>
<span class="level-value" data-container="${service.id}">0</span>
</button>
</div>
</div>
<div class="multi-view-content">
<div class="multi-view-log" data-container-id="${service.id}"></div>
@ -2817,6 +3192,30 @@ function createMultiViewPanel(service) {
console.error(`Failed to create multi-view log element for ${service.name}`);
}
// Инициализируем состояние кнопок уровней логирования для этого контейнера
setTimeout(() => {
const levelBtns = panel.querySelectorAll('.level-btn');
levelBtns.forEach(btn => {
const level = btn.getAttribute('data-level');
const containerId = btn.getAttribute('data-container-id');
// Инициализируем настройки контейнера, если их нет
if (containerId && (!state.containerLevels || !state.containerLevels[containerId])) {
if (!state.containerLevels) {
state.containerLevels = {};
}
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
}
// Используем настройки контейнера
const isActive = state.containerLevels && state.containerLevels[containerId] ?
state.containerLevels[containerId][level] : true;
btn.classList.toggle('active', isActive);
btn.classList.toggle('disabled', !isActive);
});
}, 100);
console.log(`Multi-view panel created for ${service.name}`);
return panel;
}
@ -3949,8 +4348,15 @@ function handleLine(id, line){
const cls = classify(normalizedLine);
// Обновляем счетчики только для отображаемых логов
// Проверяем фильтры для отображения
const shouldShow = allowedByLevel(cls) && applyFilter(normalizedLine);
// Проверяем фильтры для отображения в зависимости от режима
let shouldShow;
if (state.multiViewMode && state.selectedContainers.includes(id)) {
// Для multi-view используем настройки конкретного контейнера
shouldShow = allowedByContainerLevel(cls, id) && applyFilter(normalizedLine);
} else {
// Для single-view используем глобальные настройки
shouldShow = allowedByLevel(cls) && applyFilter(normalizedLine);
}
// Обновляем счетчики только если строка будет отображаться
if (obj.counters && shouldShow) {
@ -3961,6 +4367,9 @@ function handleLine(id, line){
if (cls==='other') obj.counters.other++;
}
// Обновляем счетчики в кнопках заголовков
updateHeaderCounters(id, obj.counters);
// Для Single View НЕ добавляем перенос строки после каждой строки лога
const html = `<span class="line ${cls}">${ansiToHtml(normalizedLine)}</span>`;
@ -4024,7 +4433,10 @@ function handleLine(id, line){
if (state.multiViewMode && state.selectedContainers.includes(id)) {
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${id}"]`);
if (multiViewLog) {
if (shouldShow) {
// Проверяем фильтры для конкретного контейнера
const shouldShowInMultiView = allowedByContainerLevel(cls, id) && applyFilter(normalizedLine);
if (shouldShowInMultiView) {
// Применяем ограничение tail lines в multi view
const tailLines = parseInt(els.tail.value) || 50;
@ -4155,6 +4567,28 @@ async function switchToSingle(svc){
<div class="single-view-panel" id="singleViewPanel">
<div class="single-view-header">
<h4 class="single-view-title" id="singleViewTitle">${svc.name} (${svc.service || svc.name})</h4>
<div class="single-view-levels">
<button class="level-btn debug-btn" data-level="debug" title="DEBUG">
<span class="level-label">DEBUG</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn info-btn" data-level="info" title="INFO">
<span class="level-label">INFO</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn warn-btn" data-level="warn" title="WARN">
<span class="level-label">WARN</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn error-btn" data-level="err" title="ERROR">
<span class="level-label">ERROR</span>
<span class="level-value" data-container="single">0</span>
</button>
<button class="level-btn other-btn" data-level="other" title="OTHER">
<span class="level-label">OTHER</span>
<span class="level-value" data-container="single">0</span>
</button>
</div>
</div>
<div class="single-view-content">
<pre class="log" id="logContent">Connecting...</pre>
@ -4258,6 +4692,12 @@ async function switchToSingle(svc){
// Добавляем обработчики для счетчиков после переключения контейнера
addCounterClickHandlers();
// Обновляем состояние кнопок уровней логирования
setTimeout(() => {
initializeLevelButtons();
}, 100);
} catch (error) {
console.error('switchToSingle: Error occurred:', error);
console.error('switchToSingle: Error stack:', error.stack);
@ -4465,9 +4905,6 @@ async function updateMultiViewCounters() {
// Используем новую функцию пересчета счетчиков
recalculateMultiViewCounters();
// Обновляем видимость счетчиков
updateCounterVisibility();
// Добавляем обработчики для счетчиков
addCounterClickHandlers();
@ -4518,6 +4955,9 @@ function recalculateCounters() {
if (cerr) cerr.textContent = obj.counters.err;
if (cother) cother.textContent = obj.counters.other;
// Обновляем счетчики в кнопках заголовка single-view
updateHeaderCounters(containerId, obj.counters);
console.log(`Counters recalculated for container ${containerId} (tail: ${tailLines}):`, obj.counters);
}
@ -4552,7 +4992,7 @@ function recalculateMultiViewCounters() {
// Пересчитываем счетчики только для отображаемых логов
visibleLogs.forEach(logEntry => {
const shouldShow = allowedByLevel(logEntry.cls) && applyFilter(logEntry.line);
const shouldShow = allowedByContainerLevel(logEntry.cls, containerId) && applyFilter(logEntry.line);
if (shouldShow) {
if (logEntry.cls === 'dbg') obj.counters.dbg++;
if (logEntry.cls === 'ok') obj.counters.info++;
@ -4562,6 +5002,9 @@ function recalculateMultiViewCounters() {
}
});
// Обновляем счетчики в кнопках заголовка для этого контейнера
updateHeaderCounters(containerId, obj.counters);
// Добавляем к общим счетчикам
totalDebug += obj.counters.dbg;
totalInfo += obj.counters.info;
@ -4588,6 +5031,7 @@ function recalculateMultiViewCounters() {
// Функция для обновления видимости счетчиков
function updateCounterVisibility() {
// Обновляем старые кнопки счетчиков (только для legacy интерфейса)
const debugBtn = document.querySelector('.debug-btn');
const infoBtn = document.querySelector('.info-btn');
const warnBtn = document.querySelector('.warn-btn');
@ -5120,6 +5564,9 @@ document.addEventListener('DOMContentLoaded', () => {
els.optionsBtn.title = 'Показать настройки';
localStorage.setItem('lb_options_hidden', 'true');
}
// Инициализируем состояние кнопок уровней логирования
initializeLevelButtons();
}
// Обработчик для кнопки выхода
@ -5590,6 +6037,62 @@ window.addEventListener('keydown', async (e)=>{
}
});
// Обработчики для кнопок уровней логирования в заголовках
document.addEventListener('click', (e) => {
if (e.target.closest('.level-btn')) {
const levelBtn = e.target.closest('.level-btn');
const level = levelBtn.getAttribute('data-level');
const containerId = levelBtn.getAttribute('data-container-id');
console.log(`Клик по кнопке уровня логирования: level=${level}, containerId=${containerId}, multiViewMode=${state.multiViewMode}`);
// Переключаем состояние кнопки
const isActive = levelBtn.classList.contains('active');
levelBtn.classList.toggle('active');
// Обновляем состояние уровней логирования
if (containerId) {
// Для multi-view: конкретный контейнер
if (!state.containerLevels) {
state.containerLevels = {};
}
if (!state.containerLevels[containerId]) {
state.containerLevels[containerId] = {debug: true, info: true, warn: true, err: true, other: true};
}
state.containerLevels[containerId][level] = !isActive;
// Обновляем видимость логов только для этого контейнера
updateContainerLogVisibility(containerId);
// Пересчитываем счетчики только для этого контейнера
setTimeout(() => {
updateContainerCounters(containerId);
}, 100);
// Обновляем видимость логов для всех контейнеров в multi-view
// чтобы убедиться, что изменения применились только к нужному контейнеру
state.selectedContainers.forEach(id => {
if (id !== containerId) {
updateContainerLogVisibility(id);
}
});
} else {
// Для single-view: глобальные настройки
state.levels[level] = !isActive;
// Обновляем видимость логов только для текущего контейнера
if (state.current) {
updateLogVisibility(els.logContent);
}
// Пересчитываем счетчики только для текущего контейнера
setTimeout(() => {
recalculateCounters();
}, 100);
}
}
});
// Добавляем тестовые функции в глобальную область для отладки
window.testDuplicateRemoval = testDuplicateRemoval;
window.testSingleViewDuplicateRemoval = testSingleViewDuplicateRemoval;