feat: Добавлено AJAX обновление логов и улучшения интерфейса
Основные изменения: - Добавлено AJAX обновление логов с чекбоксом 'Auto-update logs' - Добавлена опция 'All logs' в выпадающий список tail lines - Исправлено отображение длинных названий контейнеров в multi-view режиме - Восстановлена загрузка истории логов при включенном AJAX обновлении Новые функции: - Чекбокс 'Auto-update logs' в секции Options (включен по умолчанию) - Настройка интервала обновления через LOGBOARD_AJAX_UPDATE_INTERVAL - API эндпоинт /api/settings для получения настроек приложения - Поддержка параметра tail=all для загрузки всех логов - Автоматический запуск AJAX обновления при включении чекбокса Исправления UI: - Кнопки LogLevels не уезжают вправо при длинных названиях контейнеров - Добавлено обрезание длинных названий с многоточием - Фиксированная высота заголовков в multi-view режиме - Защита от сжатия кнопок LogLevels Тестирование: - Добавлены тесты для AJAX обновления (test_ajax_update.py) - Тест multi-view AJAX обновления (test_multi_view_ajax.py) - Тест опции 'all logs' (test_all_logs.py) - Тест отображения длинных названий (test_multi_view_layout.py) - Команды make test-ajax, make test-multi-view-ajax, make test-all-logs, make test-multi-view-layout Документация: - Создана подробная документация AJAX обновления (app/docs/ajax-update.md) - Обновлен CHANGELOG.md с версиями 1.3.0, 1.5.0, 1.6.0 - Обновлен README.md с описанием новых функций Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
@@ -1395,6 +1395,7 @@ a{color:var(--link)}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.multi-view-title {
|
||||
@@ -1403,6 +1404,10 @@ a{color:var(--link)}
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0; /* Позволяет flex-элементу сжиматься */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -1432,6 +1437,7 @@ a{color:var(--link)}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.single-view-title {
|
||||
@@ -1440,6 +1446,10 @@ a{color:var(--link)}
|
||||
color: var(--fg);
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0; /* Позволяет flex-элементу сжиматься */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Кнопки уровней логирования для заголовков */
|
||||
@@ -1448,6 +1458,7 @@ a{color:var(--link)}
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
flex-shrink: 0; /* Предотвращает сжатие кнопок */
|
||||
}
|
||||
|
||||
.level-btn {
|
||||
@@ -1464,7 +1475,9 @@ a{color:var(--link)}
|
||||
transition: all 0.2s ease;
|
||||
font-size: 10px;
|
||||
min-width: 40px;
|
||||
max-width: 50px;
|
||||
position: relative;
|
||||
flex-shrink: 0; /* Предотвращает сжатие кнопок */
|
||||
}
|
||||
|
||||
.level-btn:hover {
|
||||
@@ -1984,13 +1997,17 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-content" id="tail-content">
|
||||
<select id="tail">
|
||||
<option value="50" selected>50</option>
|
||||
<option value="100">100</option>
|
||||
<option value="200">200</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="control-content" id="tail-content">
|
||||
<select id="tail">
|
||||
<option value="50">50</option>
|
||||
<option value="100" selected>100</option>
|
||||
<option value="200">200</option>
|
||||
<option value="300">300</option>
|
||||
<option value="500">500</option>
|
||||
<option value="1000">1000</option>
|
||||
<option value="all">All logs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group collapsible" data-section="options">
|
||||
@@ -2010,9 +2027,15 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
<input type="checkbox" id="wrap" checked>
|
||||
<label for="wrap">Wrap text</label>
|
||||
</div>
|
||||
<div class="checkbox-item">
|
||||
<input type="checkbox" id="autoupdate" checked>
|
||||
<label for="autoupdate">Auto-update logs</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6678,6 +6701,404 @@ window.addEventListener('keydown', async (e)=>{
|
||||
|
||||
// Инициализируем видимость кнопок LogLevels
|
||||
updateLogLevelsVisibility();
|
||||
|
||||
// ========================================
|
||||
// AJAX ОБНОВЛЕНИЕ ЛОГОВ
|
||||
// ========================================
|
||||
|
||||
// Глобальные переменные для AJAX обновления
|
||||
let ajaxUpdateInterval = null;
|
||||
let ajaxUpdateEnabled = false;
|
||||
let ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию (будет переопределено из env)
|
||||
|
||||
// Состояние для каждого контейнера (для multi-view)
|
||||
let containerStates = new Map(); // containerId -> {lastTimestamp, lastSecondCount}
|
||||
|
||||
/**
|
||||
* Включить периодическое обновление логов через AJAX
|
||||
* @param {number} intervalMs - Интервал обновления в миллисекундах
|
||||
*/
|
||||
function enableAjaxLogUpdate(intervalMs = null) {
|
||||
if (ajaxUpdateInterval) {
|
||||
clearInterval(ajaxUpdateInterval);
|
||||
}
|
||||
|
||||
// Используем переданный интервал или значение по умолчанию
|
||||
if (intervalMs === null) {
|
||||
intervalMs = ajaxUpdateIntervalMs;
|
||||
}
|
||||
|
||||
ajaxUpdateEnabled = true;
|
||||
ajaxUpdateIntervalMs = intervalMs;
|
||||
|
||||
console.log(`AJAX обновление логов включено с интервалом ${intervalMs}ms`);
|
||||
|
||||
// Запускаем первое обновление сразу
|
||||
performAjaxLogUpdate();
|
||||
|
||||
// Устанавливаем интервал
|
||||
ajaxUpdateInterval = setInterval(performAjaxLogUpdate, intervalMs);
|
||||
|
||||
// Обновляем UI
|
||||
updateAjaxUpdateCheckbox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Отключить периодическое обновление логов через AJAX
|
||||
*/
|
||||
function disableAjaxLogUpdate() {
|
||||
if (ajaxUpdateInterval) {
|
||||
clearInterval(ajaxUpdateInterval);
|
||||
ajaxUpdateInterval = null;
|
||||
}
|
||||
|
||||
ajaxUpdateEnabled = false;
|
||||
console.log('AJAX обновление логов отключено');
|
||||
|
||||
// Обновляем UI
|
||||
updateAjaxUpdateCheckbox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Переключить состояние AJAX обновления
|
||||
*/
|
||||
function toggleAjaxLogUpdate() {
|
||||
if (ajaxUpdateEnabled) {
|
||||
disableAjaxLogUpdate();
|
||||
} else {
|
||||
enableAjaxLogUpdate(ajaxUpdateIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнить обновление логов через AJAX
|
||||
*/
|
||||
async function performAjaxLogUpdate() {
|
||||
if (!ajaxUpdateEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем значение tail, учитывая опцию "all"
|
||||
let tailLines = els.tail.value;
|
||||
if (tailLines === 'all') {
|
||||
tailLines = 'all'; // Оставляем как строку для API
|
||||
} else {
|
||||
tailLines = parseInt(tailLines) || 50;
|
||||
}
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
console.error('AJAX Update: No access token found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем контейнеры для обновления
|
||||
let containersToUpdate = [];
|
||||
|
||||
if (state.multiViewMode && state.selectedContainers.length > 0) {
|
||||
// Multi-view режим: обновляем все выбранные контейнеры
|
||||
containersToUpdate = state.selectedContainers;
|
||||
} else if (state.current) {
|
||||
// Single-view режим: обновляем текущий контейнер
|
||||
containersToUpdate = [state.current.id];
|
||||
} else {
|
||||
console.log('AJAX Update: Нет контейнеров для обновления');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Обновляем ${containersToUpdate.length} контейнеров:`, containersToUpdate);
|
||||
|
||||
// Обновляем каждый контейнер
|
||||
for (const containerId of containersToUpdate) {
|
||||
await updateContainerLogs(containerId, tailLines, token);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('AJAX Update Error:', error);
|
||||
// Не отключаем обновление при ошибке, просто логируем
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить логи для конкретного контейнера
|
||||
*/
|
||||
async function updateContainerLogs(containerId, tailLines, token) {
|
||||
try {
|
||||
// Формируем URL с параметрами
|
||||
const url = new URL(`/api/logs/${containerId}`, window.location.origin);
|
||||
|
||||
// Передаем tail параметр как строку (для поддержки "all")
|
||||
url.searchParams.set('tail', String(tailLines));
|
||||
|
||||
// Получаем состояние контейнера
|
||||
const containerState = containerStates.get(containerId) || { lastTimestamp: null, lastSecondCount: 0 };
|
||||
|
||||
// Если у нас есть временная метка последнего обновления, используем её
|
||||
if (containerState.lastTimestamp) {
|
||||
url.searchParams.set('since', containerState.lastTimestamp);
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Запрашиваем логи для ${containerId} с tail=${tailLines}`);
|
||||
|
||||
// Формируем заголовки запроса
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Cache-Control': 'no-cache'
|
||||
};
|
||||
const response = await fetch(url.toString(), { headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
// Клиентская дедупликация: пропускаем уже учтённые строки в ту же секунду
|
||||
let newPortion = data.logs || [];
|
||||
|
||||
// Извлекаем секундную часть из timestamp ответа сервера
|
||||
const serverTs = (data.timestamp || '').split('.')[0]; // отбрасываем миллисекунды
|
||||
if (!containerState.lastTimestamp || serverTs !== containerState.lastTimestamp) {
|
||||
// Новая секунда — сбрасываем счётчик
|
||||
containerState.lastTimestamp = serverTs;
|
||||
containerState.lastSecondCount = 0;
|
||||
}
|
||||
|
||||
if (newPortion.length > 0) {
|
||||
// Обрезаем уже учтённые строки в той же секунде
|
||||
if (containerState.lastSecondCount > 0 && newPortion.length > containerState.lastSecondCount) {
|
||||
newPortion = newPortion.slice(containerState.lastSecondCount);
|
||||
} else if (containerState.lastSecondCount >= newPortion.length) {
|
||||
newPortion = [];
|
||||
}
|
||||
|
||||
if (newPortion.length > 0) {
|
||||
console.log(`AJAX Update: К обработке ${newPortion.length} строк для ${containerId} (из ${data.logs.length}), lastSecondCount=${containerState.lastSecondCount}`);
|
||||
appendNewLogsForContainer(containerId, newPortion);
|
||||
containerState.lastSecondCount += newPortion.length;
|
||||
} else {
|
||||
console.log(`AJAX Update: Новых логов нет для ${containerId} после дедупликации по секундам`);
|
||||
}
|
||||
} else {
|
||||
console.log(`AJAX Update: Логи не пришли для ${containerId}`);
|
||||
}
|
||||
|
||||
// Обновляем состояние контейнера
|
||||
containerStates.set(containerId, containerState);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`AJAX Update Error for ${containerId}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить новые логи в конец существующих (универсальная функция для single и multi view)
|
||||
* @param {string} containerId - ID контейнера
|
||||
* @param {Array} newLogs - Массив новых логов
|
||||
*/
|
||||
function appendNewLogsForContainer(containerId, newLogs) {
|
||||
const obj = state.open[containerId];
|
||||
|
||||
if (!obj) {
|
||||
console.warn(`AJAX Update: Object not found for container ${containerId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обрабатываем каждую новую строку лога через handleLine
|
||||
let addedCount = 0;
|
||||
newLogs.forEach(log => {
|
||||
const message = log.message || log.raw || '';
|
||||
if (message.trim()) {
|
||||
// Используем существующую функцию handleLine для правильной обработки
|
||||
handleLine(containerId, message);
|
||||
addedCount++;
|
||||
}
|
||||
});
|
||||
|
||||
// Прокручиваем к концу, если включена автопрокрутка
|
||||
if (state.autoScroll) {
|
||||
if (state.multiViewMode) {
|
||||
// Для multi-view прокручиваем все контейнеры
|
||||
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||
if (multiViewLog) {
|
||||
multiViewLog.scrollTop = multiViewLog.scrollHeight;
|
||||
}
|
||||
} else if (els.logContent) {
|
||||
// Для single-view прокручиваем основной контент
|
||||
els.logContent.scrollTop = els.logContent.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем счетчики
|
||||
if (state.multiViewMode) {
|
||||
// Для multi-view обновляем счетчики конкретного контейнера
|
||||
updateContainerCounters(containerId);
|
||||
} else {
|
||||
// Для single-view обновляем общие счетчики
|
||||
recalculateCounters();
|
||||
}
|
||||
|
||||
// Очищаем дублированные строки
|
||||
if (state.multiViewMode) {
|
||||
const multiViewLog = document.querySelector(`.multi-view-log[data-container-id="${containerId}"]`);
|
||||
if (multiViewLog) {
|
||||
cleanMultiViewDuplicateLines(multiViewLog);
|
||||
cleanMultiViewEmptyLines(multiViewLog);
|
||||
}
|
||||
} else {
|
||||
cleanDuplicateLines(els.logContent);
|
||||
cleanSingleViewEmptyLines(els.logContent);
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Обработано ${addedCount} новых строк логов для ${containerId} через handleLine`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить новые логи в конец существующих (для обратной совместимости)
|
||||
* @param {Array} newLogs - Массив новых логов
|
||||
*/
|
||||
function appendNewLogs(newLogs) {
|
||||
if (!state.current || !els.logContent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const containerId = state.current.id;
|
||||
appendNewLogsForContainer(containerId, newLogs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить чекбокс AJAX обновления в UI
|
||||
*/
|
||||
function updateAjaxUpdateCheckbox() {
|
||||
const checkbox = document.getElementById('autoupdate');
|
||||
if (checkbox) {
|
||||
checkbox.checked = ajaxUpdateEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализировать чекбокс AJAX обновления
|
||||
*/
|
||||
function initAjaxUpdateCheckbox() {
|
||||
const checkbox = document.getElementById('autoupdate');
|
||||
if (!checkbox) {
|
||||
console.error('AJAX Update Checkbox not found in HTML');
|
||||
return;
|
||||
}
|
||||
|
||||
// Настраиваем чекбокс
|
||||
checkbox.title = 'Автоматическое обновление логов через AJAX';
|
||||
|
||||
// Добавляем обработчик изменения
|
||||
checkbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
enableAjaxLogUpdate();
|
||||
} else {
|
||||
disableAjaxLogUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
// Устанавливаем начальное состояние (включен по умолчанию)
|
||||
checkbox.checked = true;
|
||||
ajaxUpdateEnabled = true;
|
||||
|
||||
console.log('AJAX Update Checkbox initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация AJAX обновления
|
||||
*/
|
||||
async function initAjaxUpdate() {
|
||||
initAjaxUpdateCheckbox();
|
||||
|
||||
// Получаем настройки с сервера
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
const response = await fetch('/api/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const settings = await response.json();
|
||||
ajaxUpdateIntervalMs = settings.ajax_update_interval || 2000;
|
||||
console.log(`AJAX Update: Интервал обновления получен с сервера: ${ajaxUpdateIntervalMs}ms`);
|
||||
} else {
|
||||
console.warn('AJAX Update: Не удалось получить настройки с сервера, используем значение по умолчанию');
|
||||
ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию
|
||||
}
|
||||
} else {
|
||||
console.warn('AJAX Update: Токен не найден, используем значение по умолчанию');
|
||||
ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AJAX Update: Ошибка получения настроек:', error);
|
||||
ajaxUpdateIntervalMs = 2000; // 2 секунды по умолчанию
|
||||
}
|
||||
|
||||
console.log(`AJAX Update: Интервал обновления установлен на ${ajaxUpdateIntervalMs}ms`);
|
||||
|
||||
// Останавливаем AJAX обновление при смене контейнера
|
||||
const originalSwitchToSingle = window.switchToSingle;
|
||||
window.switchToSingle = function(containerId) {
|
||||
disableAjaxLogUpdate();
|
||||
// Очищаем состояние для всех контейнеров
|
||||
containerStates.clear();
|
||||
return originalSwitchToSingle.call(this, containerId);
|
||||
};
|
||||
|
||||
// Останавливаем AJAX обновление при переключении в multi-view
|
||||
const originalSwitchToMultiView = window.switchToMultiView;
|
||||
window.switchToMultiView = function() {
|
||||
disableAjaxLogUpdate();
|
||||
// Очищаем состояние для всех контейнеров
|
||||
containerStates.clear();
|
||||
return originalSwitchToMultiView.call(this);
|
||||
};
|
||||
|
||||
console.log('AJAX обновление логов инициализировано');
|
||||
}
|
||||
|
||||
// Запускаем инициализацию AJAX обновления
|
||||
initAjaxUpdate().then(() => {
|
||||
// Автоматически запускаем AJAX обновление (так как чекбокс включен по умолчанию)
|
||||
setTimeout(() => {
|
||||
if (ajaxUpdateEnabled) {
|
||||
console.log('AJAX Update: Автоматический запуск обновления логов');
|
||||
enableAjaxLogUpdate();
|
||||
}
|
||||
}, 1000); // Запускаем через 1 секунду после инициализации
|
||||
});
|
||||
|
||||
// Экспортируем функции в глобальную область для отладки
|
||||
window.enableAjaxLogUpdate = enableAjaxLogUpdate;
|
||||
window.disableAjaxLogUpdate = disableAjaxLogUpdate;
|
||||
window.toggleAjaxLogUpdate = toggleAjaxLogUpdate;
|
||||
window.performAjaxLogUpdate = performAjaxLogUpdate;
|
||||
window.updateContainerLogs = updateContainerLogs;
|
||||
|
||||
// Добавляем обработчик изменения выбранных контейнеров в multi-view
|
||||
const originalToggleContainerSelection = window.toggleContainerSelection;
|
||||
window.toggleContainerSelection = function(containerId) {
|
||||
const result = originalToggleContainerSelection.call(this, containerId);
|
||||
|
||||
// Если AJAX обновление активно, очищаем состояние для измененных контейнеров
|
||||
if (ajaxUpdateEnabled) {
|
||||
// Очищаем состояние для всех контейнеров, чтобы избежать проблем с синхронизацией
|
||||
containerStates.clear();
|
||||
console.log('AJAX Update: Очищено состояние контейнеров после изменения выбора');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
})();
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user