Move excluded containers to separate sidebar section and improve UI
This commit is contained in:
parent
f3e1966f3e
commit
293e9c8cba
184
app.py
184
app.py
@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from typing import Optional, List, Dict
|
||||
@ -52,44 +53,95 @@ def verify_ws_token(token: str) -> bool:
|
||||
return raw == f"{BASIC_USER}:{BASIC_PASS}"
|
||||
|
||||
# ---------- DOCKER HELPERS ----------
|
||||
def load_excluded_containers() -> List[str]:
|
||||
"""
|
||||
Загружает список исключенных контейнеров из JSON файла
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
try:
|
||||
with open("excluded_containers.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return data.get("excluded_containers", [])
|
||||
except FileNotFoundError:
|
||||
print("⚠️ Файл excluded_containers.json не найден, используем пустой список")
|
||||
return []
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"❌ Ошибка парсинга excluded_containers.json: {e}")
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка загрузки excluded_containers.json: {e}")
|
||||
return []
|
||||
|
||||
def save_excluded_containers(containers: List[str]) -> bool:
|
||||
"""
|
||||
Сохраняет список исключенных контейнеров в JSON файл
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
try:
|
||||
data = {
|
||||
"excluded_containers": containers,
|
||||
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
|
||||
}
|
||||
with open("excluded_containers.json", "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка сохранения excluded_containers.json: {e}")
|
||||
return False
|
||||
|
||||
def get_all_projects() -> List[str]:
|
||||
"""
|
||||
Получает список всех проектов Docker Compose
|
||||
Получает список всех проектов Docker Compose с учетом исключенных контейнеров
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
projects = set()
|
||||
excluded_containers = load_excluded_containers()
|
||||
|
||||
try:
|
||||
containers = docker_client.containers.list(all=True)
|
||||
|
||||
# Словарь для подсчета контейнеров по проектам
|
||||
project_containers = {}
|
||||
standalone_containers = []
|
||||
|
||||
for c in containers:
|
||||
try:
|
||||
# Пропускаем исключенные контейнеры
|
||||
if c.name in excluded_containers:
|
||||
continue
|
||||
|
||||
labels = c.labels or {}
|
||||
project = labels.get("com.docker.compose.project")
|
||||
|
||||
if project:
|
||||
projects.add(project)
|
||||
if project not in project_containers:
|
||||
project_containers[project] = 0
|
||||
project_containers[project] += 1
|
||||
else:
|
||||
standalone_containers.append(c.name)
|
||||
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Добавляем контейнеры без проекта как "standalone"
|
||||
standalone_count = 0
|
||||
for c in containers:
|
||||
try:
|
||||
labels = c.labels or {}
|
||||
if not labels.get("com.docker.compose.project"):
|
||||
standalone_count += 1
|
||||
except Exception:
|
||||
continue
|
||||
# Добавляем проекты, у которых есть хотя бы один неисключенный контейнер
|
||||
for project, count in project_containers.items():
|
||||
if count > 0:
|
||||
projects.add(project)
|
||||
|
||||
if standalone_count > 0:
|
||||
# Добавляем standalone, если есть неисключенные контейнеры без проекта
|
||||
if standalone_containers:
|
||||
projects.add("standalone")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Ошибка получения списка проектов: {e}")
|
||||
return []
|
||||
|
||||
return sorted(list(projects))
|
||||
result = sorted(list(projects))
|
||||
print(f"📋 Доступные проекты (с учетом исключенных контейнеров): {result}")
|
||||
return result
|
||||
|
||||
def list_containers(projects: Optional[List[str]] = None, include_stopped: bool = False) -> List[Dict]:
|
||||
"""
|
||||
@ -97,21 +149,13 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
"""
|
||||
# Список контейнеров, которые генерируют слишком много логов
|
||||
excluded_containers = [
|
||||
"buildx_buildkit_multiarch-builder0",
|
||||
"buildx_buildkit_multiarch-builder1",
|
||||
"buildx_buildkit_multiarch-builder2",
|
||||
"buildx_buildkit_multiarch-builder3",
|
||||
"buildx_buildkit_multiarch-builder4",
|
||||
"buildx_buildkit_multiarch-builder5",
|
||||
"buildx_buildkit_multiarch-builder6",
|
||||
"buildx_buildkit_multiarch-builder7",
|
||||
"buildx_buildkit_multiarch-builder8",
|
||||
"buildx_buildkit_multiarch-builder9"
|
||||
]
|
||||
# Загружаем список исключенных контейнеров из JSON файла
|
||||
excluded_containers = load_excluded_containers()
|
||||
|
||||
print(f"🚫 Список исключенных контейнеров: {excluded_containers}")
|
||||
|
||||
items = []
|
||||
excluded_count = 0
|
||||
|
||||
try:
|
||||
# Получаем список контейнеров с базовой обработкой ошибок
|
||||
@ -128,6 +172,8 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
|
||||
"service": c.name,
|
||||
"project": None,
|
||||
"health": None,
|
||||
"ports": [],
|
||||
"url": None,
|
||||
}
|
||||
|
||||
# Безопасно получаем метки
|
||||
@ -147,6 +193,31 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
|
||||
except Exception:
|
||||
pass # Оставляем "unknown"
|
||||
|
||||
# Безопасно получаем информацию о портах
|
||||
try:
|
||||
ports = c.ports or {}
|
||||
if ports:
|
||||
basic_info["ports"] = list(ports.keys())
|
||||
|
||||
# Пытаемся найти HTTP/HTTPS порт для создания URL
|
||||
for port_mapping in ports.values():
|
||||
if port_mapping:
|
||||
for mapping in port_mapping:
|
||||
if isinstance(mapping, dict) and mapping.get("HostPort"):
|
||||
host_port = mapping["HostPort"]
|
||||
# Проверяем, что это HTTP порт (80, 443, 8080, 3000, etc.)
|
||||
host_port_int = int(host_port)
|
||||
if (host_port_int in [80, 443] or
|
||||
(3000 <= host_port_int <= 4000) or
|
||||
(8000 <= host_port_int <= 9000)):
|
||||
protocol = "https" if host_port == "443" else "http"
|
||||
basic_info["url"] = f"{protocol}://localhost:{host_port}"
|
||||
break
|
||||
if basic_info["url"]:
|
||||
break
|
||||
except Exception:
|
||||
pass # Оставляем пустые значения
|
||||
|
||||
# Фильтрация по проектам
|
||||
if projects:
|
||||
# Если проект не указан, считаем его standalone
|
||||
@ -156,6 +227,7 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
|
||||
|
||||
# Фильтрация исключенных контейнеров
|
||||
if basic_info["name"] in excluded_containers:
|
||||
excluded_count += 1
|
||||
print(f"⚠️ Пропускаем исключенный контейнер: {basic_info['name']}")
|
||||
continue
|
||||
|
||||
@ -173,6 +245,31 @@ def list_containers(projects: Optional[List[str]] = None, include_stopped: bool
|
||||
|
||||
# Сортируем по проекту, сервису и имени
|
||||
items.sort(key=lambda x: (x.get("project") or "", x.get("service") or "", x.get("name") or ""))
|
||||
|
||||
# Подсчитываем статистику по проектам
|
||||
project_stats = {}
|
||||
for item in items:
|
||||
project = item.get("project") or "standalone"
|
||||
if project not in project_stats:
|
||||
project_stats[project] = {"visible": 0, "excluded": 0}
|
||||
project_stats[project]["visible"] += 1
|
||||
|
||||
# Подсчитываем исключенные контейнеры по проектам
|
||||
for c in containers:
|
||||
try:
|
||||
if c.name in excluded_containers:
|
||||
labels = c.labels or {}
|
||||
project = labels.get("com.docker.compose.project") or "standalone"
|
||||
if project not in project_stats:
|
||||
project_stats[project] = {"visible": 0, "excluded": 0}
|
||||
project_stats[project]["excluded"] += 1
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
print(f"📊 Статистика: найдено {len(items)} контейнеров, исключено {excluded_count} контейнеров")
|
||||
for project, stats in project_stats.items():
|
||||
print(f" 📦 {project}: {stats['visible']} видимых, {stats['excluded']} исключенных")
|
||||
|
||||
return items
|
||||
|
||||
# ---------- HTML ----------
|
||||
@ -246,6 +343,41 @@ def api_logs_stats(container_id: str, _: HTTPBasicCredentials = Depends(check_ba
|
||||
print(f"Error getting log stats for {container_id}: {e}")
|
||||
return JSONResponse({"error": str(e)}, status_code=500)
|
||||
|
||||
@app.get("/api/excluded-containers")
|
||||
def api_get_excluded_containers(_: HTTPBasicCredentials = Depends(check_basic)):
|
||||
"""
|
||||
Получить список исключенных контейнеров
|
||||
"""
|
||||
return JSONResponse(
|
||||
content={"excluded_containers": load_excluded_containers()},
|
||||
headers={
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0"
|
||||
}
|
||||
)
|
||||
|
||||
@app.post("/api/excluded-containers")
|
||||
def api_update_excluded_containers(
|
||||
containers: List[str] = Body(...),
|
||||
_: HTTPBasicCredentials = Depends(check_basic)
|
||||
):
|
||||
"""
|
||||
Обновить список исключенных контейнеров
|
||||
"""
|
||||
success = save_excluded_containers(containers)
|
||||
if success:
|
||||
return JSONResponse(
|
||||
content={"status": "success", "message": "Список исключенных контейнеров обновлен"},
|
||||
headers={
|
||||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||||
"Pragma": "no-cache",
|
||||
"Expires": "0"
|
||||
}
|
||||
)
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Ошибка сохранения списка")
|
||||
|
||||
@app.get("/api/projects")
|
||||
def api_projects(_: HTTPBasicCredentials = Depends(check_basic)):
|
||||
"""Получить список всех проектов Docker Compose"""
|
||||
|
15
excluded_containers.json
Normal file
15
excluded_containers.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"excluded_containers": [
|
||||
"buildx_buildkit_multiarch-builder0",
|
||||
"buildx_buildkit_multiarch-builder1",
|
||||
"buildx_buildkit_multiarch-builder2",
|
||||
"buildx_buildkit_multiarch-builder3",
|
||||
"buildx_buildkit_multiarch-builder4",
|
||||
"buildx_buildkit_multiarch-builder5",
|
||||
"buildx_buildkit_multiarch-builder6",
|
||||
"buildx_buildkit_multiarch-builder7",
|
||||
"buildx_buildkit_multiarch-builder8",
|
||||
"buildx_buildkit_multiarch-builder9"
|
||||
],
|
||||
"description": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения"
|
||||
}
|
@ -328,6 +328,93 @@ a{color:var(--link)}
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Секция исключенных контейнеров */
|
||||
.excluded-containers-list {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.excluded-container-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.excluded-container-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.excluded-container-item:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
.excluded-container-item:last-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.excluded-container-name {
|
||||
color: var(--fg);
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.remove-excluded-btn {
|
||||
background: var(--err);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-left: 8px;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.remove-excluded-btn:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.excluded-containers-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.excluded-input {
|
||||
flex: 1;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-size: 12px;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.excluded-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.excluded-input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
background: var(--chip);
|
||||
@ -550,6 +637,82 @@ a{color:var(--link)}
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
/* Мультивыбор проектов */
|
||||
.multi-select-container {
|
||||
position: relative;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.multi-select-display {
|
||||
background: var(--chip);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.multi-select-display:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.multi-select-display.active {
|
||||
border-color: var(--accent);
|
||||
background: var(--tab-active);
|
||||
}
|
||||
|
||||
.multi-select-text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.multi-select-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.multi-select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.multi-select-option:hover {
|
||||
background: var(--chip);
|
||||
}
|
||||
|
||||
.multi-select-option input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.multi-select-option label {
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--fg);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
@ -716,6 +879,19 @@ a{color:var(--link)}
|
||||
.status-indicator.stopped { background: var(--err); }
|
||||
.status-indicator.paused { background: var(--warn); }
|
||||
|
||||
.container-link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s ease;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.container-link:hover {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Log Area */
|
||||
.log-area {
|
||||
flex: 1;
|
||||
@ -900,6 +1076,8 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
<label for="wrap">Wrap text</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -920,6 +1098,24 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group collapsible" data-section="excluded">
|
||||
<div class="control-header">
|
||||
<label>Excluded</label>
|
||||
<button class="collapse-btn" data-target="excluded">
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-content" id="excluded-content">
|
||||
<div class="excluded-containers-list" id="excludedContainersList">
|
||||
<!-- Список будет загружен динамически -->
|
||||
</div>
|
||||
<div class="excluded-containers-controls">
|
||||
<input type="text" id="newExcludedContainer" placeholder="Имя контейнера" class="excluded-input">
|
||||
<button id="addExcludedContainer" class="btn btn-small">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Container List -->
|
||||
@ -942,9 +1138,18 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}
|
||||
</button>
|
||||
<h2 class="header-title">Logs</h2>
|
||||
<span class="header-badge" id="projectBadge">
|
||||
<select id="projectSelectHeader">
|
||||
<option value="all">All Projects</option>
|
||||
</select>
|
||||
<div class="multi-select-container">
|
||||
<div class="multi-select-display" id="projectSelectDisplay">
|
||||
<span class="multi-select-text">All Projects</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="multi-select-dropdown" id="projectSelectDropdown" style="display: none;">
|
||||
<div class="multi-select-option" data-value="all">
|
||||
<input type="checkbox" id="project-all" checked>
|
||||
<label for="project-all">All Projects</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<input id="filter" type="text" placeholder="Filter logs (regex)…" class="header-filter"/>
|
||||
<div class="header-controls">
|
||||
@ -1277,10 +1482,18 @@ function buildTabs(){
|
||||
<div class="container-status">
|
||||
<span class="status-indicator ${statusClass}"></span>
|
||||
${escapeHtml(svc.status)}
|
||||
${svc.url ? `<a href="${svc.url}" target="_blank" class="container-link" title="Открыть сайт"><i class="fas fa-external-link-alt"></i></a>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
item.onclick = () => switchToSingle(svc);
|
||||
item.onclick = (e) => {
|
||||
// Не переключаем контейнер, если кликнули на ссылку
|
||||
if (e.target.closest('.container-link')) {
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
switchToSingle(svc);
|
||||
};
|
||||
els.containerList.appendChild(item);
|
||||
});
|
||||
}
|
||||
@ -1307,44 +1520,230 @@ async function fetchProjects(){
|
||||
|
||||
|
||||
|
||||
// Обновляем селектор проектов в заголовке
|
||||
const headerSelect = document.getElementById('projectSelectHeader');
|
||||
console.log('Header select element found:', !!headerSelect);
|
||||
if (headerSelect) {
|
||||
headerSelect.innerHTML = '<option value="all">All Projects</option>';
|
||||
console.log('Adding projects to header select:', projects);
|
||||
// Обновляем мультивыбор проектов в заголовке
|
||||
const dropdown = document.getElementById('projectSelectDropdown');
|
||||
const display = document.getElementById('projectSelectDisplay');
|
||||
const displayText = display?.querySelector('.multi-select-text');
|
||||
|
||||
console.log('Multi-select elements found:', {dropdown: !!dropdown, display: !!display, displayText: !!displayText});
|
||||
|
||||
if (dropdown && displayText) {
|
||||
// Очищаем dropdown
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
// Добавляем опцию "All Projects"
|
||||
const allOption = document.createElement('div');
|
||||
allOption.className = 'multi-select-option';
|
||||
allOption.setAttribute('data-value', 'all');
|
||||
allOption.innerHTML = `
|
||||
<input type="checkbox" id="project-all" checked>
|
||||
<label for="project-all">All Projects</label>
|
||||
`;
|
||||
dropdown.appendChild(allOption);
|
||||
|
||||
// Добавляем проекты
|
||||
console.log('Adding projects to multi-select:', projects);
|
||||
projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project;
|
||||
option.textContent = project;
|
||||
headerSelect.appendChild(option);
|
||||
const option = document.createElement('div');
|
||||
option.className = 'multi-select-option';
|
||||
option.setAttribute('data-value', project);
|
||||
option.innerHTML = `
|
||||
<input type="checkbox" id="project-${project}">
|
||||
<label for="project-${project}">${escapeHtml(project)}</label>
|
||||
`;
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Устанавливаем сохраненный проект
|
||||
if (localStorage.lb_project && projects.includes(localStorage.lb_project)) {
|
||||
headerSelect.value = localStorage.lb_project;
|
||||
}
|
||||
console.log('Header select updated, current value:', headerSelect.value);
|
||||
// Восстанавливаем сохраненные выбранные проекты
|
||||
const savedProjects = JSON.parse(localStorage.getItem('lb_selected_projects') || '["all"]');
|
||||
updateMultiSelect(savedProjects);
|
||||
|
||||
console.log('Multi-select updated, current selection:', savedProjects);
|
||||
} else {
|
||||
console.error('Header select element not found!');
|
||||
console.error('Multi-select elements not found!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching projects:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для обновления мультивыбора проектов
|
||||
function updateMultiSelect(selectedProjects) {
|
||||
const dropdown = document.getElementById('projectSelectDropdown');
|
||||
const displayText = document.querySelector('.multi-select-text');
|
||||
|
||||
if (!dropdown || !displayText) return;
|
||||
|
||||
// Фильтруем выбранные проекты, оставляя только те, которые есть в dropdown
|
||||
const availableProjects = [];
|
||||
dropdown.querySelectorAll('.multi-select-option').forEach(option => {
|
||||
const value = option.getAttribute('data-value');
|
||||
availableProjects.push(value);
|
||||
});
|
||||
|
||||
const filteredProjects = selectedProjects.filter(project =>
|
||||
project === 'all' || availableProjects.includes(project)
|
||||
);
|
||||
|
||||
// Если все выбранные проекты исчезли, выбираем "All Projects"
|
||||
if (filteredProjects.length === 0 || (filteredProjects.length === 1 && filteredProjects[0] === 'all')) {
|
||||
filteredProjects.length = 0;
|
||||
filteredProjects.push('all');
|
||||
}
|
||||
|
||||
// Обновляем чекбоксы
|
||||
dropdown.querySelectorAll('.multi-select-option').forEach(option => {
|
||||
const value = option.getAttribute('data-value');
|
||||
const checkbox = option.querySelector('input[type="checkbox"]');
|
||||
if (checkbox) {
|
||||
checkbox.checked = filteredProjects.includes(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем текст отображения
|
||||
if (filteredProjects.includes('all') || filteredProjects.length === 0) {
|
||||
displayText.textContent = 'All Projects';
|
||||
} else if (filteredProjects.length === 1) {
|
||||
displayText.textContent = filteredProjects[0];
|
||||
} else {
|
||||
displayText.textContent = `${filteredProjects.length} Projects`;
|
||||
}
|
||||
|
||||
// Сохраняем в localStorage
|
||||
localStorage.setItem('lb_selected_projects', JSON.stringify(filteredProjects));
|
||||
}
|
||||
|
||||
// Функция для получения выбранных проектов
|
||||
function getSelectedProjects() {
|
||||
const dropdown = document.getElementById('projectSelectDropdown');
|
||||
if (!dropdown) return ['all'];
|
||||
|
||||
const selectedProjects = [];
|
||||
dropdown.querySelectorAll('.multi-select-option input[type="checkbox"]:checked').forEach(checkbox => {
|
||||
const option = checkbox.closest('.multi-select-option');
|
||||
const value = option.getAttribute('data-value');
|
||||
selectedProjects.push(value);
|
||||
});
|
||||
|
||||
return selectedProjects.length > 0 ? selectedProjects : ['all'];
|
||||
}
|
||||
|
||||
// Функции для работы с исключенными контейнерами
|
||||
async function loadExcludedContainers() {
|
||||
try {
|
||||
const response = await fetch('/api/excluded-containers');
|
||||
if (!response.ok) {
|
||||
console.error('Ошибка загрузки исключенных контейнеров:', response.status);
|
||||
return [];
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.excluded_containers || [];
|
||||
} catch (error) {
|
||||
console.error('Ошибка загрузки исключенных контейнеров:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function saveExcludedContainers(containers) {
|
||||
try {
|
||||
const response = await fetch('/api/excluded-containers', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(containers)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Ошибка сохранения исключенных контейнеров:', response.status);
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Исключенные контейнеры сохранены:', data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Ошибка сохранения исключенных контейнеров:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderExcludedContainers(containers) {
|
||||
const list = document.getElementById('excludedContainersList');
|
||||
if (!list) return;
|
||||
|
||||
list.innerHTML = '';
|
||||
|
||||
if (containers.length === 0) {
|
||||
list.innerHTML = '<div class="excluded-container-item"><span class="excluded-container-name">Нет исключенных контейнеров</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
containers.forEach(container => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'excluded-container-item';
|
||||
item.innerHTML = `
|
||||
<span class="excluded-container-name">${escapeHtml(container)}</span>
|
||||
<button class="remove-excluded-btn" onclick="removeExcludedContainer('${escapeHtml(container)}')">×</button>
|
||||
`;
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async function addExcludedContainer() {
|
||||
const input = document.getElementById('newExcludedContainer');
|
||||
const containerName = input.value.trim();
|
||||
|
||||
if (!containerName) {
|
||||
alert('Введите имя контейнера');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentContainers = await loadExcludedContainers();
|
||||
if (currentContainers.includes(containerName)) {
|
||||
alert('Контейнер уже в списке исключенных');
|
||||
return;
|
||||
}
|
||||
|
||||
currentContainers.push(containerName);
|
||||
const success = await saveExcludedContainers(currentContainers);
|
||||
|
||||
if (success) {
|
||||
renderExcludedContainers(currentContainers);
|
||||
input.value = '';
|
||||
// Обновляем список проектов и контейнеров
|
||||
await fetchProjects();
|
||||
await fetchServices();
|
||||
} else {
|
||||
alert('Ошибка сохранения');
|
||||
}
|
||||
}
|
||||
|
||||
async function removeExcludedContainer(containerName) {
|
||||
const currentContainers = await loadExcludedContainers();
|
||||
const updatedContainers = currentContainers.filter(name => name !== containerName);
|
||||
|
||||
const success = await saveExcludedContainers(updatedContainers);
|
||||
|
||||
if (success) {
|
||||
renderExcludedContainers(updatedContainers);
|
||||
// Обновляем список проектов и контейнеров
|
||||
await fetchProjects();
|
||||
await fetchServices();
|
||||
} else {
|
||||
alert('Ошибка удаления');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchServices(){
|
||||
try {
|
||||
console.log('Fetching services...');
|
||||
const url = new URL(location.origin + '/api/services');
|
||||
const projectSelectHeader = document.getElementById('projectSelectHeader');
|
||||
const selectedProject = projectSelectHeader ? projectSelectHeader.value : 'all';
|
||||
const selectedProjects = getSelectedProjects();
|
||||
|
||||
if (selectedProject && selectedProject !== 'all') {
|
||||
url.searchParams.set('projects', selectedProject);
|
||||
localStorage.lb_project = selectedProject;
|
||||
} else {
|
||||
localStorage.removeItem('lb_project');
|
||||
// Если выбраны конкретные проекты (не "all"), добавляем их в URL как строку через запятую
|
||||
if (selectedProjects.length > 0 && !selectedProjects.includes('all')) {
|
||||
url.searchParams.set('projects', selectedProjects.join(','));
|
||||
}
|
||||
|
||||
const res = await fetch(url);
|
||||
@ -1356,13 +1755,6 @@ async function fetchServices(){
|
||||
const data = await res.json();
|
||||
console.log('Services loaded:', data);
|
||||
state.services = data;
|
||||
const pj = selectedProject === 'all' ? 'all' : selectedProject;
|
||||
|
||||
// Обновляем селектор в заголовке
|
||||
const headerSelect = document.getElementById('projectSelectHeader');
|
||||
if (headerSelect) {
|
||||
headerSelect.value = selectedProject;
|
||||
}
|
||||
|
||||
buildTabs();
|
||||
if (!state.current && state.services.length) switchToSingle(state.services[0]);
|
||||
@ -1874,15 +2266,67 @@ function addCounterClickHandlers() {
|
||||
}
|
||||
|
||||
|
||||
// Функция для добавления обработчика выпадающего списка проектов в заголовке
|
||||
function addHeaderProjectSelectHandler() {
|
||||
const headerProjectSelect = document.getElementById('projectSelectHeader');
|
||||
console.log('Adding handler for header project select, element found:', !!headerProjectSelect);
|
||||
if (headerProjectSelect) {
|
||||
headerProjectSelect.onchange = () => {
|
||||
console.log('Header project select changed to:', headerProjectSelect.value);
|
||||
// Функция для добавления обработчиков мультивыбора проектов
|
||||
function addMultiSelectHandlers() {
|
||||
const display = document.getElementById('projectSelectDisplay');
|
||||
const dropdown = document.getElementById('projectSelectDropdown');
|
||||
|
||||
console.log('Adding multi-select handlers, elements found:', {display: !!display, dropdown: !!dropdown});
|
||||
|
||||
if (display && dropdown) {
|
||||
// Обработчик клика по дисплею
|
||||
display.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = dropdown.style.display !== 'none';
|
||||
|
||||
if (isOpen) {
|
||||
dropdown.style.display = 'none';
|
||||
display.classList.remove('active');
|
||||
} else {
|
||||
dropdown.style.display = 'block';
|
||||
display.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчики кликов по опциям
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const option = e.target.closest('.multi-select-option');
|
||||
if (!option) return;
|
||||
|
||||
const checkbox = option.querySelector('input[type="checkbox"]');
|
||||
if (!checkbox) return;
|
||||
|
||||
const value = option.getAttribute('data-value');
|
||||
|
||||
// Специальная логика для "All Projects"
|
||||
if (value === 'all') {
|
||||
if (checkbox.checked) {
|
||||
// Если "All Projects" выбран, снимаем все остальные
|
||||
dropdown.querySelectorAll('.multi-select-option input[type="checkbox"]').forEach(cb => {
|
||||
if (cb !== checkbox) cb.checked = false;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Если выбран конкретный проект, снимаем "All Projects"
|
||||
const allCheckbox = dropdown.querySelector('#project-all');
|
||||
if (allCheckbox) allCheckbox.checked = false;
|
||||
}
|
||||
|
||||
// Обновляем отображение и загружаем сервисы
|
||||
const selectedProjects = getSelectedProjects();
|
||||
updateMultiSelect(selectedProjects);
|
||||
fetchServices();
|
||||
};
|
||||
});
|
||||
|
||||
// Закрытие dropdown при клике вне его
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!display.contains(e.target) && !dropdown.contains(e.target)) {
|
||||
dropdown.style.display = 'none';
|
||||
display.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2154,8 +2598,29 @@ window.addEventListener('keydown', (e)=>{
|
||||
// Добавляем обработчики для счетчиков
|
||||
addCounterClickHandlers();
|
||||
|
||||
// Добавляем обработчик для выпадающего списка проектов в заголовке
|
||||
addHeaderProjectSelectHandler();
|
||||
// Добавляем обработчик для выпадающего списка проектов в заголовке
|
||||
addMultiSelectHandlers();
|
||||
|
||||
// Загружаем и отображаем исключенные контейнеры
|
||||
loadExcludedContainers().then(containers => {
|
||||
renderExcludedContainers(containers);
|
||||
});
|
||||
|
||||
// Добавляем обработчики для исключенных контейнеров
|
||||
const addExcludedBtn = document.getElementById('addExcludedContainer');
|
||||
const newExcludedInput = document.getElementById('newExcludedContainer');
|
||||
|
||||
if (addExcludedBtn) {
|
||||
addExcludedBtn.onclick = addExcludedContainer;
|
||||
}
|
||||
|
||||
if (newExcludedInput) {
|
||||
newExcludedInput.onkeypress = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
addExcludedContainer();
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
Loading…
x
Reference in New Issue
Block a user