diff --git a/app.py b/app.py index 427e808..bf70aec 100644 --- a/app.py +++ b/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""" diff --git a/excluded_containers.json b/excluded_containers.json new file mode 100644 index 0000000..e1bfa73 --- /dev/null +++ b/excluded_containers.json @@ -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": "Список контейнеров, которые генерируют слишком много логов и исключаются из отображения" +} diff --git a/templates/index.html b/templates/index.html index 549cf71..3dfef38 100644 --- a/templates/index.html +++ b/templates/index.html @@ -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} + + @@ -920,6 +1098,24 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px} + +
+
+ + +
+
+
+ +
+
+ + +
+
+
@@ -942,9 +1138,18 @@ footer{position:fixed;right:10px;bottom:10px;opacity:.6;font-size:11px}

Logs

- +
+
+ All Projects + +
+ +
@@ -1277,10 +1482,18 @@ function buildTabs(){
${escapeHtml(svc.status)} + ${svc.url ? `` : ''}
`; - 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 = ''; - 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 = ` + + + `; + 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 = ` + + + `; + 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 = '
Нет исключенных контейнеров
'; + return; + } + + containers.forEach(container => { + const item = document.createElement('div'); + item.className = 'excluded-container-item'; + item.innerHTML = ` + ${escapeHtml(container)} + + `; + 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(); + } + }; + } })();