feat: Добавлена drag & drop функциональность для multi-view окон

- Добавлены CSS стили для drag & drop (курсоры, анимации, placeholder)
- Реализованы JavaScript функции для перетаскивания окон
- Добавлена иконка перетаскивания в заголовки multi-view панелей
- Автоматическое сохранение порядка окон в localStorage
- Обновлена документация и справка
- Добавлена подробная документация по drag & drop функциональности
This commit is contained in:
Sergey Antropoff 2025-08-21 09:58:45 +03:00
parent c40b2b312e
commit f5f6005b5c
7 changed files with 659 additions and 1 deletions

View File

@ -41,6 +41,8 @@ LogBoard+ особенно полезен для разработчиков, р
### Основные возможности
- **Просмотр логов в реальном времени** - WebSocket соединения для live-логов
- **Multi-view режим** - Одновременный просмотр логов нескольких контейнеров
- **Drag & Drop** - Перетаскивание окон для изменения порядка в multi-view режиме
- **Поддержка множественных проектов** - Фильтрация по проектам Docker Compose
- **Безопасность** - JWT аутентификация и авторизация
- **Фильтрация контейнеров** - Исключение проблемных контейнеров
@ -395,6 +397,18 @@ curl http://localhost:9001/healthz
- Количество исключенных контейнеров
- Время ответа API
## Документация
Подробная документация по всем аспектам LogBoard+:
- **[Установка и настройка](docs/installation.md)** - Пошаговое руководство по установке
- **[Конфигурация](docs/configuration.md)** - Настройка переменных окружения и параметров
- **[API документация](docs/api.md)** - REST API и WebSocket API
- **[Управление проектом](docs/management.md)** - Makefile и Docker Compose команды
- **[Безопасность](docs/security.md)** - Рекомендации по безопасности
- **[Устранение неполадок](docs/troubleshooting.md)** - Решение проблем
- **[Drag & Drop](docs/drag-and-drop.md)** - Перетаскивание окон в multi-view режиме
## Разработка
### Локальная разработка
@ -493,6 +507,13 @@ MIT License - см. файл [LICENSE](LICENSE) для подробностей.
## Changelog
### v2.0.0 (2024-01-XX)
- **Drag & Drop для multi-view** - Перетаскивание окон для изменения порядка
- Улучшенный интерфейс multi-view режима
- Визуальные индикаторы перетаскивания
- Автоматическое сохранение порядка окон
- Обновленная документация
### v1.0.0 (2024-01-XX)
- Первый релиз LogBoard+
- Поддержка множественных проектов Docker Compose

0
app/start.sh Executable file
View File

View File

@ -2582,3 +2582,126 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
.notification-close:hover {
background: var(--chip);
color: var(--fg);
}
/* Мультипросмотр */
.multi-view-grid {
display: grid;
gap: 2px;
height: 100%;
padding: 0px;
/* Равная высота строк для нескольких рядов (3+ окон) */
grid-auto-rows: 1fr;
align-items: stretch;
}
/* Drag & Drop стили для multi-view */
.multi-view-panel {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
padding: 2px;
cursor: move; /* Курсор для перетаскивания */
user-select: none; /* Запрещаем выделение текста при перетаскивании */
transition: transform 0.2s ease, box-shadow 0.2s ease;
position: relative;
}
/* Стили для перетаскиваемого элемента */
.multi-view-panel.dragging {
opacity: 0.8;
transform: scale(1.02);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
z-index: 1000;
cursor: grabbing;
}
/* Стили для области, куда можно бросить элемент */
.multi-view-panel.drag-over {
border-color: var(--accent);
background: var(--tab-active);
transform: scale(1.01);
}
/* Индикатор перетаскивания в заголовке */
.multi-view-header {
padding: 5px 16px;
background: var(--chip);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 16px;
cursor: grab; /* Курсор для перетаскивания в заголовке */
}
.multi-view-header:active {
cursor: grabbing;
}
/* Иконка перетаскивания */
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
color: var(--muted);
cursor: grab;
margin-right: 8px;
transition: color 0.2s ease;
}
.drag-handle:hover {
color: var(--accent);
}
.drag-handle:active {
cursor: grabbing;
}
/* Подсказка для перетаскивания */
.drag-tooltip {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px;
padding: 4px 8px;
font-size: 11px;
color: var(--fg);
white-space: nowrap;
z-index: 1001;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.drag-tooltip.show {
opacity: 1;
visibility: visible;
}
/* Анимация для перестановки элементов */
.multi-view-panel {
transition: transform 0.3s ease, box-shadow 0.3s ease, border-color 0.3s ease;
}
/* Стили для сетки при перетаскивании */
.multi-view-grid.dragging {
gap: 4px; /* Увеличиваем отступы для лучшей видимости */
}
/* Стили для placeholder при перетаскивании */
.multi-view-panel.drag-placeholder {
background: var(--chip);
border: 2px dashed var(--accent);
opacity: 0.5;
pointer-events: none;
}

View File

@ -1838,6 +1838,9 @@ async function setupMultiView() {
// Обновляем видимость кнопок LogLevels
updateLogLevelsVisibility();
// Обновляем drag & drop для новых панелей
updateDragAndDrop();
}
/**
@ -1854,7 +1857,10 @@ function createMultiViewPanel(service) {
console.log(`createMultiViewPanel: Panel element created with data-container-id: ${service.id}`);
panel.innerHTML = `
<div class="multi-view-header">
<div class="multi-view-header" draggable="true" data-container-id="${service.id}">
<div class="drag-handle" title="Перетащите для изменения порядка">
<i class="fas fa-grip-vertical"></i>
</div>
<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">
@ -5810,3 +5816,317 @@ function reinitializeElements() {
};
})();
/**
* Drag & Drop функциональность для multi-view окон
* Позволяет перетаскивать окна логов и менять их порядок
*/
// Глобальные переменные для drag & drop
let dragState = {
isDragging: false,
draggedElement: null,
draggedIndex: -1,
originalIndex: -1,
placeholder: null
};
/**
* Инициализация drag & drop для multi-view панелей
*/
function initDragAndDrop() {
console.log('Инициализация drag & drop для multi-view');
// Добавляем обработчики событий для всех multi-view панелей
document.addEventListener('dragstart', handleDragStart);
document.addEventListener('dragend', handleDragEnd);
document.addEventListener('dragover', handleDragOver);
document.addEventListener('dragenter', handleDragEnter);
document.addEventListener('dragleave', handleDragLeave);
document.addEventListener('drop', handleDrop);
// Добавляем обработчики для заголовков панелей
document.addEventListener('mousedown', handleMouseDown);
}
/**
* Обработчик начала перетаскивания
*/
function handleDragStart(event) {
const header = event.target.closest('.multi-view-header');
if (!header || !state.multiViewMode) return;
const panel = header.closest('.multi-view-panel');
if (!panel) return;
event.preventDefault();
const containerId = header.getAttribute('data-container-id');
if (!containerId) return;
// Находим индекс панели в сетке
const grid = panel.closest('.multi-view-grid');
if (!grid) return;
const panels = Array.from(grid.querySelectorAll('.multi-view-panel'));
const draggedIndex = panels.indexOf(panel);
if (draggedIndex === -1) return;
// Устанавливаем состояние перетаскивания
dragState.isDragging = true;
dragState.draggedElement = panel;
dragState.draggedIndex = draggedIndex;
dragState.originalIndex = draggedIndex;
// Добавляем стили для перетаскиваемого элемента
panel.classList.add('dragging');
grid.classList.add('dragging');
// Создаем placeholder
createDragPlaceholder(panel);
// Устанавливаем данные для перетаскивания
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', containerId);
console.log(`Начато перетаскивание панели ${containerId} с индекса ${draggedIndex}`);
}
/**
* Обработчик окончания перетаскивания
*/
function handleDragEnd(event) {
if (!dragState.isDragging) return;
const panel = dragState.draggedElement;
if (panel) {
panel.classList.remove('dragging');
}
const grid = panel?.closest('.multi-view-grid');
if (grid) {
grid.classList.remove('dragging');
}
// Удаляем placeholder
removeDragPlaceholder();
// Убираем стили drag-over со всех панелей
document.querySelectorAll('.multi-view-panel.drag-over').forEach(p => {
p.classList.remove('drag-over');
});
// Сбрасываем состояние
dragState.isDragging = false;
dragState.draggedElement = null;
dragState.draggedIndex = -1;
dragState.originalIndex = -1;
dragState.placeholder = null;
console.log('Перетаскивание завершено');
}
/**
* Обработчик перетаскивания над элементом
*/
function handleDragOver(event) {
if (!dragState.isDragging) return;
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}
/**
* Обработчик входа в зону перетаскивания
*/
function handleDragEnter(event) {
if (!dragState.isDragging) return;
const panel = event.target.closest('.multi-view-panel');
if (!panel || panel === dragState.draggedElement) return;
panel.classList.add('drag-over');
}
/**
* Обработчик выхода из зоны перетаскивания
*/
function handleDragLeave(event) {
if (!dragState.isDragging) return;
const panel = event.target.closest('.multi-view-panel');
if (!panel) return;
// Проверяем, что мы действительно вышли из панели
const rect = panel.getBoundingClientRect();
const x = event.clientX;
const y = event.clientY;
if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) {
panel.classList.remove('drag-over');
}
}
/**
* Обработчик сброса элемента
*/
function handleDrop(event) {
if (!dragState.isDragging) return;
event.preventDefault();
const targetPanel = event.target.closest('.multi-view-panel');
if (!targetPanel || targetPanel === dragState.draggedElement) return;
const grid = targetPanel.closest('.multi-view-grid');
if (!grid) return;
const panels = Array.from(grid.querySelectorAll('.multi-view-panel'));
const targetIndex = panels.indexOf(targetPanel);
if (targetIndex === -1) return;
// Переставляем элементы
reorderMultiViewPanels(dragState.originalIndex, targetIndex);
// Убираем стили
targetPanel.classList.remove('drag-over');
console.log(`Панель перемещена с позиции ${dragState.originalIndex} на позицию ${targetIndex}`);
}
/**
* Обработчик нажатия мыши для улучшения UX
*/
function handleMouseDown(event) {
const header = event.target.closest('.multi-view-header');
if (!header || !state.multiViewMode) return;
const dragHandle = event.target.closest('.drag-handle');
if (!dragHandle) return;
// Показываем подсказку
showDragTooltip(header, 'Перетащите для изменения порядка');
}
/**
* Создает placeholder для перетаскивания
*/
function createDragPlaceholder(originalPanel) {
const placeholder = originalPanel.cloneNode(true);
placeholder.classList.add('drag-placeholder');
placeholder.classList.remove('dragging');
// Очищаем содержимое placeholder
const content = placeholder.querySelector('.multi-view-content');
if (content) {
content.innerHTML = '<div class="multi-view-log"></div>';
}
// Вставляем placeholder на место оригинального элемента
originalPanel.parentNode.insertBefore(placeholder, originalPanel);
dragState.placeholder = placeholder;
}
/**
* Удаляет placeholder
*/
function removeDragPlaceholder() {
if (dragState.placeholder) {
dragState.placeholder.remove();
dragState.placeholder = null;
}
}
/**
* Переставляет панели в multi-view
*/
function reorderMultiViewPanels(fromIndex, toIndex) {
const grid = document.querySelector('.multi-view-grid');
if (!grid) return;
const panels = Array.from(grid.querySelectorAll('.multi-view-panel'));
if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0 ||
fromIndex >= panels.length || toIndex >= panels.length) return;
// Получаем ID контейнеров в текущем порядке
const containerIds = panels.map(panel =>
panel.querySelector('.multi-view-header').getAttribute('data-container-id')
);
// Переставляем элементы в массиве
const movedContainer = containerIds.splice(fromIndex, 1)[0];
containerIds.splice(toIndex, 0, movedContainer);
// Обновляем порядок в state
state.selectedContainers = containerIds;
// Сохраняем новый порядок
saveViewMode(true, state.selectedContainers);
// Пересоздаем сетку с новым порядком
setTimeout(() => {
setupMultiView();
}, 100);
console.log(`Порядок контейнеров обновлен: ${containerIds.join(', ')}`);
}
/**
* Показывает подсказку при перетаскивании
*/
function showDragTooltip(element, text) {
// Удаляем существующие подсказки
document.querySelectorAll('.drag-tooltip').forEach(tooltip => tooltip.remove());
const tooltip = document.createElement('div');
tooltip.className = 'drag-tooltip';
tooltip.textContent = text;
element.appendChild(tooltip);
// Показываем подсказку
setTimeout(() => {
tooltip.classList.add('show');
}, 10);
// Скрываем подсказку через 2 секунды
setTimeout(() => {
tooltip.classList.remove('show');
setTimeout(() => {
tooltip.remove();
}, 200);
}, 2000);
}
/**
* Обновляет drag & drop для новых панелей
*/
function updateDragAndDrop() {
if (!state.multiViewMode) return;
// Удаляем старые обработчики
document.querySelectorAll('.multi-view-header').forEach(header => {
header.removeEventListener('dragstart', handleDragStart);
header.removeEventListener('dragend', handleDragEnd);
});
// Добавляем обработчики для новых панелей
document.querySelectorAll('.multi-view-header').forEach(header => {
header.setAttribute('draggable', 'true');
});
}
// Инициализируем drag & drop при загрузке страницы
document.addEventListener('DOMContentLoaded', () => {
initDragAndDrop();
});
// Обновляем drag & drop при изменении multi-view
const originalSetupMultiView = window.setupMultiView;
window.setupMultiView = async function() {
await originalSetupMultiView.call(this);
updateDragAndDrop();
};

View File

@ -274,6 +274,8 @@
<div class="help-tooltip-description">Добавить в мультивыбор</div>
<div class="help-tooltip-hotkey">Выделение текста</div>
<div class="help-tooltip-description">Показать кнопку копирования</div>
<div class="help-tooltip-hotkey">Перетаскивание заголовка</div>
<div class="help-tooltip-description">Изменить порядок окон в multi-view</div>
</div>
</div>
@ -399,6 +401,14 @@
<li><kbd>Ctrl</kbd> + <kbd>И</kbd> <span>Свернуть/развернуть панель (русская раскладка)</span></li>
</ul>
</div>
<div class="hotkeys-section">
<h4 class="hotkeys-section-title">Multi-View</h4>
<ul class="hotkeys-list">
<li><span>Перетащите заголовок окна</span> <span>Изменить порядок окон</span></li>
<li><span>Иконка ⋮⋮ в заголовке</span> <span>Индикатор перетаскивания</span></li>
</ul>
</div>
</div>
</div>
</div>

View File

@ -9,6 +9,8 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./snapshots:/app/snapshots
- ./app:/app
- ./start.sh:/app/start.sh
restart: unless-stopped
user: 0:0
networks:

182
docs/drag-and-drop.md Normal file
View File

@ -0,0 +1,182 @@
# Drag & Drop для Multi-View режима
## Обзор
LogBoard+ поддерживает drag & drop функциональность в режиме мультипросмотра (multi-view), позволяя пользователям изменять порядок окон логов путем перетаскивания их заголовков.
## Возможности
### Основные функции
- **Перетаскивание окон**: Изменение порядка окон логов в multi-view режиме
- **Визуальная обратная связь**: Анимации и стили во время перетаскивания
- **Сохранение порядка**: Автоматическое сохранение нового порядка в localStorage
- **Placeholder**: Визуальный индикатор места вставки во время перетаскивания
### Индикаторы перетаскивания
- **Иконка ⋮⋮**: Отображается в заголовке каждого окна
- **Курсор**: Изменяется на `grab`/`grabbing` при наведении и перетаскивании
- **Подсказки**: Всплывающие подсказки с инструкциями
## Использование
### Как перетащить окно
1. **Включите multi-view режим**: Выберите несколько контейнеров (Shift+клик или Ctrl+клик)
2. **Найдите иконку перетаскивания**: В заголовке каждого окна есть иконка ⋮⋮
3. **Начните перетаскивание**: Нажмите и удерживайте левую кнопку мыши на заголовке окна
4. **Переместите окно**: Перетащите окно в нужную позицию
5. **Отпустите кнопку мыши**: Окно встанет на новое место
### Визуальные эффекты
- **Во время перетаскивания**:
- Окно становится полупрозрачным
- Появляется тень
- Окно слегка увеличивается
- Создается placeholder на месте исходного окна
- **При наведении на другие окна**:
- Окна подсвечиваются синим цветом
- Показывается, куда будет вставлено окно
## Техническая реализация
### CSS классы
```css
.multi-view-panel.dragging {
opacity: 0.8;
transform: scale(1.02);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
z-index: 1000;
cursor: grabbing;
}
.multi-view-panel.drag-over {
border-color: var(--accent);
background: var(--tab-active);
transform: scale(1.01);
}
.multi-view-panel.drag-placeholder {
background: var(--chip);
border: 2px dashed var(--accent);
opacity: 0.5;
pointer-events: none;
}
```
### JavaScript функции
- `initDragAndDrop()`: Инициализация drag & drop
- `handleDragStart()`: Обработчик начала перетаскивания
- `handleDragEnd()`: Обработчик окончания перетаскивания
- `handleDrop()`: Обработчик сброса элемента
- `reorderMultiViewPanels()`: Перестановка панелей
- `updateDragAndDrop()`: Обновление drag & drop для новых панелей
### Состояние перетаскивания
```javascript
let dragState = {
isDragging: false,
draggedElement: null,
draggedIndex: -1,
originalIndex: -1,
placeholder: null
};
```
## Ограничения
### Поддерживаемые браузеры
- Chrome 66+
- Firefox 60+
- Safari 12+
- Edge 79+
### Условия работы
- Только в multi-view режиме (2+ контейнера)
- Только перетаскивание заголовков окон
- Не работает в single-view режиме
## Настройки
### Сохранение порядка
Порядок окон автоматически сохраняется в localStorage и восстанавливается при перезагрузке страницы.
### Отключение функциональности
Для отключения drag & drop можно удалить обработчики событий:
```javascript
document.removeEventListener('dragstart', handleDragStart);
document.removeEventListener('dragend', handleDragEnd);
// ... и другие обработчики
```
## Устранение неполадок
### Проблемы с перетаскиванием
1. **Окно не перетаскивается**:
- Убедитесь, что вы в multi-view режиме
- Проверьте, что перетаскиваете за заголовок окна
- Убедитесь, что браузер поддерживает HTML5 Drag & Drop
2. **Порядок не сохраняется**:
- Проверьте, что localStorage доступен
- Убедитесь, что нет ошибок в консоли браузера
3. **Визуальные эффекты не работают**:
- Проверьте, что CSS стили загружены
- Убедитесь, что нет конфликтов с другими стилями
### Отладка
Включите отладочные сообщения в консоли браузера:
```javascript
console.log('Drag & Drop Debug:', dragState);
```
## Примеры использования
### Базовое перетаскивание
```javascript
// Автоматически инициализируется при загрузке страницы
initDragAndDrop();
```
### Программное изменение порядка
```javascript
// Перестановка панелей программно
reorderMultiViewPanels(0, 2); // Переместить первую панель на третье место
```
### Получение текущего порядка
```javascript
// Получить текущий порядок контейнеров
const currentOrder = state.selectedContainers;
console.log('Текущий порядок:', currentOrder);
```
## Совместимость
### С существующим функционалом
- Совместимо с AJAX обновлением логов
- Работает с фильтрацией по уровням логирования
- Поддерживает автопрокрутку и перенос строк
- Интегрируется с системой сохранения настроек
### Производительность
- Минимальное влияние на производительность
- Оптимизированные обработчики событий
- Эффективное обновление DOM
## Планы развития
### Будущие улучшения
- Поддержка перетаскивания между разными сетками
- Анимации при изменении размера окон
- Группировка окон по категориям
- Экспорт/импорт конфигурации порядка окон
### Обратная связь
Для предложений по улучшению drag & drop функциональности обращайтесь к автору проекта.
---
**Автор**: Сергей Антропов
**Сайт**: https://devops.org.ru
**Версия**: 2.0