/* * Copyright 2026 Safronov Grigorii * * Licensed under the CDDL, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * https://opensource.org/licenses/CDDL-1.0 */ /** * @fileoverview JavaScript для веб-интерфейса Futriis DB Dashboard * @version 1.0.0 * @description Обеспечивает полное управление СУБД: CRUD операции, ACL, индексы, * транзакции, триггеры, ограничения (constraints), импорт/экспорт, * управление кластером и аудит. Использует async/await, Fetch API, * динамическую отрисовку DOM и модальные окна. */ // ============================== ГЛОБАЛЬНОЕ СОСТОЯНИЕ ============================== /** @type {string|null} ID текущей сессии */ let currentSession = null; /** @type {string|null} Имя текущей базы данных */ let currentDatabase = null; /** @type {string|null} Имя текущей коллекции */ let currentCollection = null; /** @type {string|null} Имя текущего пользователя */ let currentUser = null; // ============================== DOM ЭЛЕМЕНТЫ ============================== /** * @description DOM элементы, используемые для управления интерфейсом. * Инициализируются при загрузке документа. */ const contentArea = document.getElementById('contentArea'); const pageTitle = document.getElementById('pageTitle'); const connectionStatus = document.getElementById('connectionStatus'); const userInfoSpan = document.querySelector('#userName'); const userRoleSpan = document.getElementById('userRole'); const logoutBtn = document.getElementById('logoutBtn'); const menuToggle = document.getElementById('menuToggle'); const sidebar = document.querySelector('.sidebar'); const modal = document.getElementById('modal'); const modalTitle = document.getElementById('modalTitle'); const modalBody = document.getElementById('modalBody'); const modalConfirm = document.getElementById('modalConfirm'); const modalCloseBtns = document.querySelectorAll('.modal-close'); const changePasswordIcon = document.getElementById('changePasswordIcon'); const userAvatar = document.getElementById('userAvatar'); // ============================== ИНИЦИАЛИЗАЦИЯ ============================== /** * @description Главная точка входа. Выполняется после загрузки DOM. * Проверяет активную сессию, инициализирует навигацию и обработчики. */ document.addEventListener('DOMContentLoaded', () => { checkSession(); initNavigation(); initEventListeners(); initAvatarUpload(); initChangePassword(); }); // ============================== АУТЕНТИФИКАЦИЯ И СЕССИЯ ============================== /** * @async * @description Проверяет активность сессии на сервере. * При успешной аутентификации загружает дашборд, иначе показывает форму входа. */ async function checkSession() { try { const response = await fetch('/api/webui/session'); const data = await response.json(); if (data.success && data.data.authenticated) { currentUser = data.data.username; userInfoSpan.textContent = currentUser; // Загружаем аватар if (data.data.avatar) { updateAvatarDisplay(data.data.avatar); } else { loadUserAvatar(); } // Обновляем индикатор подключения к СУБД if (data.data.connection_status === 'connected') { connectionStatus.className = 'connection-status online'; connectionStatus.innerHTML = 'СУБД подключена'; } else { connectionStatus.className = 'connection-status offline'; connectionStatus.innerHTML = 'СУБД не подключена'; } loadDashboard(); startConnectionStatusMonitor(); } else { showLoginModal(); } } catch (error) { console.error('Session check failed:', error); showLoginModal(); } } /** * @description Запускает периодическую проверку статуса подключения к СУБД. * Обновляет индикатор каждые 5 секунд. */ function startConnectionStatusMonitor() { setInterval(async () => { if (currentUser) { try { const response = await fetch('/api/webui/session'); const data = await response.json(); if (data.success && data.data.connection_status === 'connected') { connectionStatus.className = 'connection-status online'; connectionStatus.innerHTML = 'СУБД подключена'; } else { connectionStatus.className = 'connection-status offline'; connectionStatus.innerHTML = 'СУБД не подключена'; } } catch (error) { connectionStatus.className = 'connection-status offline'; connectionStatus.innerHTML = 'СУБД не подключена'; } } }, 5000); } /** * @description Отображает модальное окно для входа в систему. * Обрабатывает отправку учётных данных и сохраняет сессию. */ function showLoginModal() { modalTitle.textContent = 'Вход в систему СУБД Futriis'; modalBody.innerHTML = `
`; modalConfirm.textContent = 'Войти'; modal.classList.add('show'); const confirmHandler = async () => { const username = document.getElementById('username').value; const password = document.getElementById('password').value; if (!username || !password) { showNotification('Пожалуйста, заполните все поля', 'error'); return; } try { const response = await fetch('/api/webui/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (data.success) { currentUser = username; userInfoSpan.textContent = username; if (data.data.avatar) { updateAvatarDisplay(data.data.avatar); } modal.classList.remove('show'); showNotification('Вход выполнен успешно', 'success'); connectionStatus.className = 'connection-status online'; connectionStatus.innerHTML = 'СУБД подключена'; startConnectionStatusMonitor(); loadDashboard(); } else { showNotification(data.error || 'Неверный логин и/или пароль', 'error'); } } catch (error) { showNotification('Ошибка подключения к серверу', 'error'); } }; modalConfirm.onclick = confirmHandler; // Закрытие модального окна по клавише Enter const handleEnter = (e) => { if (e.key === 'Enter') { confirmHandler(); document.removeEventListener('keydown', handleEnter); } }; document.addEventListener('keydown', handleEnter); } // ============================== АВАТАР ПОЛЬЗОВАТЕЛЯ ============================== /** * @description Инициализирует загрузку аватара */ async function loadUserAvatar() { try { const response = await fetch('/api/webui/user/info'); const data = await response.json(); if (data.success && data.data.avatar) { updateAvatarDisplay(data.data.avatar); } } catch (error) { console.error('Failed to load avatar:', error); } } /** * @description Обновляет отображение аватара * @param {string} avatarBase64 - Аватар в формате base64 */ function updateAvatarDisplay(avatarBase64) { if (!userAvatar) return; userAvatar.innerHTML = `Avatar`; } /** * @description Инициализирует загрузку аватара */ function initAvatarUpload() { const avatarModal = document.getElementById('avatarUploadModal'); if (!avatarModal) return; // Клик по аватару для загрузки новой картинки if (userAvatar) { userAvatar.style.cursor = 'pointer'; userAvatar.addEventListener('click', () => { showAvatarUploadModal(); }); } } /** * @description Показывает модальное окно загрузки аватара */ function showAvatarUploadModal() { const avatarModal = document.getElementById('avatarUploadModal'); const fileInput = document.getElementById('avatarFile'); const preview = document.getElementById('avatarPreview'); const uploadBtn = document.getElementById('uploadAvatarBtn'); if (!avatarModal) return; // Очищаем предыдущие значения if (fileInput) fileInput.value = ''; if (preview) preview.innerHTML = ''; avatarModal.classList.add('show'); // Предпросмотр изображения if (fileInput) { fileInput.onchange = function() { if (this.files && this.files[0]) { const reader = new FileReader(); reader.onload = function(e) { if (preview) { preview.innerHTML = ``; } }; reader.readAsDataURL(this.files[0]); } }; } // Загрузка аватара if (uploadBtn) { uploadBtn.onclick = async () => { if (!fileInput || !fileInput.files || fileInput.files.length === 0) { showNotification('Выберите изображение', 'warning'); return; } const formData = new FormData(); formData.append('avatar', fileInput.files[0]); try { const response = await fetch('/api/webui/user/avatar', { method: 'POST', body: formData }); const data = await response.json(); if (data.success) { updateAvatarDisplay(data.data.avatar); avatarModal.classList.remove('show'); showNotification('Аватар успешно загружен', 'success'); } else { showNotification(data.error || 'Ошибка загрузки аватара', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } // Закрытие модального окна const closeButtons = avatarModal.querySelectorAll('.modal-close'); closeButtons.forEach(btn => { btn.onclick = () => { avatarModal.classList.remove('show'); }; }); } // ============================== СМЕНА ПАРОЛЯ ============================== /** * @description Инициализирует смену пароля */ function initChangePassword() { if (changePasswordIcon) { changePasswordIcon.addEventListener('click', () => { showChangePasswordModal(); }); } } /** * @description Показывает модальное окно смены пароля */ function showChangePasswordModal() { const passwordModal = document.getElementById('changePasswordModal'); const currentPasswordInput = document.getElementById('currentPassword'); const newPasswordInput = document.getElementById('newPassword'); const confirmPasswordInput = document.getElementById('confirmPassword'); const changeBtn = document.getElementById('changePasswordBtn'); if (!passwordModal) return; // Очищаем поля if (currentPasswordInput) currentPasswordInput.value = ''; if (newPasswordInput) newPasswordInput.value = ''; if (confirmPasswordInput) confirmPasswordInput.value = ''; passwordModal.classList.add('show'); // Смена пароля if (changeBtn) { changeBtn.onclick = async () => { const currentPassword = currentPasswordInput ? currentPasswordInput.value : ''; const newPassword = newPasswordInput ? newPasswordInput.value : ''; const confirmPassword = confirmPasswordInput ? confirmPasswordInput.value : ''; if (!currentPassword) { showNotification('Введите текущий пароль', 'warning'); return; } if (!newPassword) { showNotification('Введите новый пароль', 'warning'); return; } if (newPassword !== confirmPassword) { showNotification('Новый пароль и подтверждение не совпадают', 'error'); return; } if (newPassword.length < 4) { showNotification('Новый пароль должен содержать минимум 4 символа', 'error'); return; } try { const response = await fetch('/api/webui/change-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword }) }); const data = await response.json(); if (data.success) { passwordModal.classList.remove('show'); showNotification('Пароль успешно изменён', 'success'); } else { showNotification(data.error || 'Ошибка смены пароля', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } // Закрытие модального окна const closeButtons = passwordModal.querySelectorAll('.modal-close'); closeButtons.forEach(btn => { btn.onclick = () => { passwordModal.classList.remove('show'); }; }); } // ============================== НАВИГАЦИЯ ============================== /** * @description Инициализирует обработчики навигации: * - Переключение секций (data-section) * - Выполнение действий (data-action) * - Раскрытие подменю (has-submenu) */ function initNavigation() { // Обработчики для основных секций document.querySelectorAll('.nav-link[data-section]').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const section = link.dataset.section; loadSection(section); setActiveNav(link); }); }); // Обработчики для быстрых действий из подменю document.querySelectorAll('[data-action]').forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); const action = item.dataset.action; handleCrudAction(action); }); }); // Раскрытие/скрытие подменю document.querySelectorAll('.has-submenu > .nav-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const parent = link.closest('.has-submenu'); parent.classList.toggle('open'); }); }); } /** * @description Инициализирует глобальные обработчики событий: * - Выход из системы * - Мобильное меню * - Закрытие модальных окон */ function initEventListeners() { logoutBtn.addEventListener('click', async () => { await fetch('/api/webui/logout', { method: 'POST' }); currentSession = null; currentUser = null; connectionStatus.className = 'connection-status offline'; connectionStatus.innerHTML = 'СУБД не подключена'; // Сбрасываем аватар if (userAvatar) { userAvatar.innerHTML = ''; } showLoginModal(); }); if (menuToggle) { menuToggle.addEventListener('click', () => { sidebar.classList.toggle('open'); }); } modalCloseBtns.forEach(btn => { btn.addEventListener('click', () => { modal.classList.remove('show'); }); }); modal.addEventListener('click', (e) => { if (e.target === modal) { modal.classList.remove('show'); } }); } // ============================== ЗАГРУЗКА СЕКЦИЙ ============================== /** * @async * @description Загружает соответствующую секцию интерфейса. * @param {string} section - Идентификатор секции (значение data-section) */ async function loadSection(section) { const sections = { dashboard: loadDashboard, cluster: loadClusterManagement, audit: () => { contentArea.innerHTML = '
Функция в разработке
'; }, settings: loadSettings, 'acl-users': loadACLUsers, 'acl-roles': loadACLRoles, 'acl-permissions': loadACLPermissions, 'tx-list': loadTransactionList, 'indexes-list': loadIndexesList, 'export-data': loadExportPage, 'import-data': loadImportPage, 'constraints-list': loadConstraintsList, 'triggers-list': loadTriggersList, 'trigger-log': loadTriggerLog }; (sections[section] || loadDashboard)(); } // ============================== ДАШБОРД ============================== /** * @async * @description Загружает и отображает главную панель управления со статистикой и списком БД. */ async function loadDashboard() { pageTitle.textContent = 'Панель управления'; contentArea.innerHTML = '

Загрузка данных...

'; try { const [statsRes, dbsRes] = await Promise.all([ fetch('/api/webui/stats'), fetch('/api/webui/databases') ]); const stats = await statsRes.json(); const databases = await dbsRes.json(); contentArea.innerHTML = `

${stats.data.databases || 0}

Базы данных

${stats.data.collections || 0}

Коллекции

${stats.data.documents || 0}

Документы

${stats.data.storage_used_mb?.toFixed(2) || 0} MB

Использовано памяти

Базы данных

${databases.data.map(db => ``).join('')}
Имя БДКоллекцииДействия
${escapeHtml(db.name)}${db.collections}
`; } catch (error) { contentArea.innerHTML = '
Ошибка загрузки данных
'; showNotification('Ошибка загрузки дашборда', 'error'); } } // ============================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ============================== /** * @async * @description Отображает список коллекций в выбранной базе данных. * @param {string} dbName - Имя базы данных */ window.viewDatabase = async function(dbName) { currentDatabase = dbName; pageTitle.textContent = `База данных: ${dbName}`; contentArea.innerHTML = '

Загрузка коллекций...

'; try { const response = await fetch(`/api/webui/collections/${dbName}`); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Коллекции

${data.data.collections.map(coll => ` `).join('')}
Имя коллекцииДокументовРазмерИндексыДействия
${escapeHtml(coll.name)} ${coll.count} ${(coll.size / 1024).toFixed(2)} KB ${coll.indexes.length}
`; } else { contentArea.innerHTML = '
Ошибка загрузки коллекций
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } }; /** * @async * @description Отображает документы выбранной коллекции с пагинацией и действиями. * @param {string} dbName - Имя базы данных * @param {string} collName - Имя коллекции */ window.viewCollection = async function(dbName, collName) { currentDatabase = dbName; currentCollection = collName; pageTitle.textContent = `Коллекция: ${dbName}.${collName}`; contentArea.innerHTML = '

Загрузка документов...

'; try { const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Документы (${data.data.total} всего)

${data.data.documents.map(doc => ` `).join('')}
IDПоляСозданДействия
${escapeHtml(doc.id)}
${escapeHtml(JSON.stringify(doc.fields, null, 2))}
${new Date(doc.created_at).toLocaleString()}
`; } else { contentArea.innerHTML = '
Ошибка загрузки документов
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } }; // ============================== КЛАСТЕР ============================== /** * @async * @description Загружает и отображает статус кластера и список узлов. */ async function loadClusterManagement() { pageTitle.textContent = 'Управление кластером'; contentArea.innerHTML = '

Загрузка информации о кластере...

'; try { const [statusRes, nodesRes] = await Promise.all([ fetch('/api/webui/cluster/status'), fetch('/api/webui/cluster/nodes') ]); const status = await statusRes.json(); const nodes = await nodesRes.json(); contentArea.innerHTML = `

${status.data.health === 'healthy' ? 'Здоров' : status.data.health === 'degraded' ? 'Деградирован' : 'Критический'}

Состояние кластера

${status.data.active_nodes}/${status.data.total_nodes}

Активные узлы

${status.data.replication_factor}

Фактор репликации

Узлы кластера

${nodes.data.map(node => ``).join('')}
ID узлаАдресСтатусПоследний контакт
${escapeHtml(node.id)}${escapeHtml(node.ip)}:${node.port}${node.status}${new Date(node.last_seen * 1000).toLocaleString()}
`; } catch (error) { contentArea.innerHTML = '
Ошибка загрузки информации о кластере
'; } } // ============================== НАСТРОЙКИ ============================== /** * @description Отображает страницу настроек интерфейса. */ function loadSettings() { pageTitle.textContent = 'Настройки'; contentArea.innerHTML = `

Настройки интерфейса

`; } // ============================== ACL (УПРАВЛЕНИЕ ДОСТУПОМ) ============================== /** * @async * @description Загружает и отображает список пользователей системы. */ async function loadACLUsers() { pageTitle.textContent = 'Управление пользователями ACL'; contentArea.innerHTML = '

Загрузка пользователей...

'; try { const response = await fetch('/api/webui/acl/users'); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Пользователи

${data.data.map(user => ` `).join('')}
ИмяРолиСтатусСозданПоследний входДействия
${escapeHtml(user.username)} ${user.roles.map(r => `${escapeHtml(r)}`).join(' ') || '-'} ${user.active ? 'Активен' : 'Отключён'} ${new Date(user.created_at).toLocaleString()} ${user.last_login ? new Date(user.last_login).toLocaleString() : '-'}
`; } else { contentArea.innerHTML = '
Ошибка загрузки пользователей
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } } /** * @async * @description Загружает и отображает список ролей и их разрешений. */ async function loadACLRoles() { pageTitle.textContent = 'Управление ролями ACL'; contentArea.innerHTML = '

Загрузка ролей...

'; try { const response = await fetch('/api/webui/acl/roles'); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Роли

${data.data.map(role => ` `).join('')}
НазваниеРазрешенияДействия
${escapeHtml(role.name)} ${role.permissions.map(p => `${escapeHtml(p)}`).join('
') || '-'}
`; } else { contentArea.innerHTML = '
Ошибка загрузки ролей
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } } /** * @async * @description Загружает и отображает все разрешения, сгруппированные по ролям. */ async function loadACLPermissions() { pageTitle.textContent = 'Управление разрешениями ACL'; contentArea.innerHTML = '

Загрузка разрешений...

'; try { const response = await fetch('/api/webui/acl/permissions'); const data = await response.json(); if (data.success) { let html = '

Разрешения по ролям

'; for (const [roleName, permissions] of Object.entries(data.data)) { html += `

Роль: ${escapeHtml(roleName)}

${permissions.map(p => `${escapeHtml(p)}`).join('') || 'Нет разрешений'}
`; } html += '
'; contentArea.innerHTML = html; } else { contentArea.innerHTML = '
Ошибка загрузки разрешений
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } } // ============================== ТРАНЗАКЦИИ ============================== /** * @async * @description Загружает и отображает список активных транзакций с возможностью управления. */ async function loadTransactionList() { pageTitle.textContent = 'Активные транзакции'; contentArea.innerHTML = '

Загрузка транзакций...

'; try { const response = await fetch('/api/webui/transactions'); const data = await response.json(); if (data.success) { contentArea.innerHTML = `

Транзакции

${data.data.map(tx => ` `).join('') || ''}
ID транзакцииСтатусНачалоОперацийДействия
${escapeHtml(tx.id)} ${escapeHtml(tx.status)} ${new Date(tx.start_time).toLocaleString()} ${tx.operation_count || 0} ${tx.status === 'active' ? `` : ''}
Нет активных транзакций
Информация о транзакциях:
`; } else { contentArea.innerHTML = '
Ошибка загрузки транзакций
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } } // ============================== ИНДЕКСЫ ============================== /** * @async * @description Загружает страницу управления индексами с выбором БД и коллекции. */ async function loadIndexesList() { pageTitle.textContent = 'Управление индексами'; contentArea.innerHTML = `

Выберите базу данных и коллекцию

`; try { const response = await fetch('/api/webui/databases'); const data = await response.json(); if (data.success) { const dbSelect = document.getElementById('indexDbSelect'); dbSelect.innerHTML = '' + data.data.map(db => ``).join(''); } } catch (error) { showNotification('Ошибка загрузки БД', 'error'); } } /** * @description Загружает коллекции для выбранной БД в интерфейсе индексов. */ window.loadCollectionsForIndex = async function() { const dbName = document.getElementById('indexDbSelect').value; const collSelect = document.getElementById('indexCollSelect'); if (!dbName) { collSelect.innerHTML = ''; document.getElementById('indexesContent').innerHTML = '

Выберите базу данных и коллекцию

'; return; } collSelect.innerHTML = ''; try { const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`); const data = await response.json(); if (data.success && data.data.collections) { collSelect.innerHTML = '' + data.data.collections.map(coll => ``).join(''); } else { collSelect.innerHTML = ''; } } catch (error) { collSelect.innerHTML = ''; } }; /** * @async * @description Загружает и отображает список индексов выбранной коллекции. */ async function loadIndexesForCollection() { const dbName = document.getElementById('indexDbSelect').value; const collName = document.getElementById('indexCollSelect').value; if (!dbName || !collName) { document.getElementById('indexesContent').innerHTML = '

Выберите базу данных и коллекцию

'; return; } document.getElementById('indexesContent').innerHTML = '

Загрузка индексов...

'; try { const response = await fetch(`/api/webui/indexes/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`); const data = await response.json(); if (data.success) { document.getElementById('indexesContent').innerHTML = `

Индексы коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}

${data.data.map(idx => ``).join('') || ''}
Имя индексаПоляУникальныйДействия
${escapeHtml(idx.name)}${idx.fields.join(', ')}${idx.unique ? 'Да' : 'Нет'}
Нет индексов
`; } else { document.getElementById('indexesContent').innerHTML = '
Ошибка загрузки индексов
'; } } catch (error) { document.getElementById('indexesContent').innerHTML = '
Ошибка подключения
'; } } // ============================== ТРИГГЕРЫ ============================== /** * @async * @description Загружает страницу управления триггерами. */ async function loadTriggersList() { pageTitle.textContent = 'Управление триггерами'; contentArea.innerHTML = `

Выберите базу данных и коллекцию

`; try { const response = await fetch('/api/webui/databases'); const data = await response.json(); if (data.success) { const dbSelect = document.getElementById('triggerDbSelect'); dbSelect.innerHTML = '' + data.data.map(db => ``).join(''); } } catch (error) { showNotification('Ошибка загрузки БД', 'error'); } } /** * @description Загружает коллекции для выбранной БД в интерфейсе триггеров. */ window.loadCollectionsForTrigger = async function() { const dbName = document.getElementById('triggerDbSelect').value; const collSelect = document.getElementById('triggerCollSelect'); if (!dbName) { collSelect.innerHTML = ''; document.getElementById('triggersContent').innerHTML = '

Выберите базу данных и коллекцию

'; return; } collSelect.innerHTML = ''; try { const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`); const data = await response.json(); if (data.success && data.data.collections) { collSelect.innerHTML = '' + data.data.collections.map(coll => ``).join(''); } else { collSelect.innerHTML = ''; } } catch (error) { collSelect.innerHTML = ''; } }; /** * @async * @description Загружает и отображает список триггеров выбранной коллекции. */ async function loadTriggersForCollection() { const dbName = document.getElementById('triggerDbSelect').value; const collName = document.getElementById('triggerCollSelect').value; if (!dbName || !collName) { document.getElementById('triggersContent').innerHTML = '

Выберите базу данных и коллекцию

'; return; } document.getElementById('triggersContent').innerHTML = '

Загрузка триггеров...

'; try { const response = await fetch(`/api/webui/triggers/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`); const data = await response.json(); if (data.success) { if (data.data.length === 0) { document.getElementById('triggersContent').innerHTML = '

Нет триггеров для этой коллекции

'; return; } document.getElementById('triggersContent').innerHTML = `

Триггеры коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}

${data.data.map(trigger => ` `).join('')}
ИмяСобытиеДействиеСтатусОписаниеДействия
${escapeHtml(trigger.name)} ${escapeHtml(trigger.event)}${escapeHtml(trigger.action)} ${trigger.enabled ? 'Включён' : 'Отключён'} ${escapeHtml(trigger.description || '-')} ${trigger.enabled ? `` : ``}
`; } else { document.getElementById('triggersContent').innerHTML = '
Ошибка загрузки триггеров
'; } } catch (error) { document.getElementById('triggersContent').innerHTML = '
Ошибка подключения
'; } } /** * @async * @description Загружает и отображает лог выполнения триггеров. */ async function loadTriggerLog() { pageTitle.textContent = 'Лог выполнения триггеров'; contentArea.innerHTML = '

Загрузка лога...

'; try { const response = await fetch('/api/webui/trigger/log'); const data = await response.json(); if (data.success) { if (data.data.length === 0) { contentArea.innerHTML = '

Лог выполнения триггеров пуст

'; return; } contentArea.innerHTML = `

Лог выполнения триггеров

${data.data.map(entry => ``).join('')}
ТриггерСобытиеКоллекцияБДДокументВремяПользователь
${escapeHtml(entry.trigger_name)}${escapeHtml(entry.event)}${escapeHtml(entry.collection)}${escapeHtml(entry.database)}${escapeHtml(entry.document_id)}${new Date(entry.timestamp).toLocaleString()}${escapeHtml(entry.user || '-')}
`; } else { contentArea.innerHTML = '
Ошибка загрузки лога
'; } } catch (error) { contentArea.innerHTML = '
Ошибка подключения
'; } } // ============================== ОГРАНИЧЕНИЯ (CONSTRAINTS) ============================== /** * @async * @description Загружает страницу управления ограничениями коллекции. */ async function loadConstraintsList() { pageTitle.textContent = 'Управление ограничениями (Constraints)'; contentArea.innerHTML = `

Выберите базу данных и коллекцию

`; try { const response = await fetch('/api/webui/databases'); const data = await response.json(); if (data.success) { const dbSelect = document.getElementById('constraintDbSelect'); dbSelect.innerHTML = '' + data.data.map(db => ``).join(''); } } catch (error) { showNotification('Ошибка загрузки БД', 'error'); } } /** * @description Загружает коллекции для выбранной БД в интерфейсе ограничений. */ window.loadCollectionsForConstraints = async function() { const dbName = document.getElementById('constraintDbSelect').value; const collSelect = document.getElementById('constraintCollSelect'); if (!dbName) { collSelect.innerHTML = ''; document.getElementById('constraintsContent').innerHTML = '

Выберите базу данных и коллекцию

'; return; } collSelect.innerHTML = ''; try { const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`); const data = await response.json(); if (data.success && data.data.collections) { collSelect.innerHTML = '' + data.data.collections.map(coll => ``).join(''); } else { collSelect.innerHTML = ''; } } catch (error) { collSelect.innerHTML = ''; } }; /** * @async * @description Загружает и отображает все ограничения выбранной коллекции. */ async function loadConstraintsForCollection() { const dbName = document.getElementById('constraintDbSelect').value; const collName = document.getElementById('constraintCollSelect').value; if (!dbName || !collName) { document.getElementById('constraintsContent').innerHTML = '

Выберите базу данных и коллекцию

'; return; } document.getElementById('constraintsContent').innerHTML = '

Загрузка ограничений...

'; try { const response = await fetch(`/api/webui/constraints/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`); const data = await response.json(); if (data.success) { const constraints = data.data; if (constraints.length === 0) { document.getElementById('constraintsContent').innerHTML = '

Нет ограничений для этой коллекции

'; return; } // Группировка ограничений по типу const grouped = { required: [], unique: [], min: [], max: [], enum: [], regex: [] }; constraints.forEach(c => { if (grouped[c.type]) grouped[c.type].push(c); }); let html = `

Ограничения коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}

`; for (const [type, title, icon] of [['required', 'Обязательные поля', 'fa-exclamation-circle'], ['unique', 'Уникальные поля', 'fa-unique'], ['min', 'Минимальные значения', 'fa-greater-than'], ['max', 'Максимальные значения', 'fa-less-than'], ['enum', 'Перечисления', 'fa-list-ul'], ['regex', 'Регулярные выражения', 'fa-code']]) { if (grouped[type].length > 0) { html += `

${title}

`; for (const c of grouped[type]) { html += ``; } html += `
Поле${type === 'enum' ? 'Допустимые значения' : (type === 'min' || type === 'max' ? 'Значение' : (type === 'regex' ? 'Шаблон' : ''))}Действия
${escapeHtml(c.field)}${type === 'enum' ? c.values.map(v => `${escapeHtml(String(v))}`).join(' ') : (c.value || c.pattern || '-')}
`; } } html += `
`; document.getElementById('constraintsContent').innerHTML = html; } else { document.getElementById('constraintsContent').innerHTML = '
Ошибка загрузки ограничений
'; } } catch (error) { document.getElementById('constraintsContent').innerHTML = '
Ошибка подключения
'; } } // ============================== ИМПОРТ/ЭКСПОРТ ============================== /** * @async * @description Загружает страницу экспорта данных. */ async function loadExportPage() { pageTitle.textContent = 'Экспорт данных'; contentArea.innerHTML = `
`; try { const response = await fetch('/api/webui/databases'); const data = await response.json(); if (data.success) { const dbSelect = document.getElementById('exportDbSelect'); dbSelect.innerHTML = '' + data.data.map(db => ``).join(''); } } catch (error) { showNotification('Ошибка загрузки БД', 'error'); } } /** * @async * @description Выполняет экспорт данных в JSON-файл. */ async function performExport() { const dbName = document.getElementById('exportDbSelect').value; const filename = document.getElementById('exportFilename').value; if (!dbName) { showNotification('Выберите базу данных', 'error'); return; } const exportResult = document.getElementById('exportResult'); exportResult.innerHTML = '

Экспорт данных...

'; try { const response = await fetch('/api/webui/export', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ database: dbName, filename }) }); const data = await response.json(); if (data.success) { const jsonStr = JSON.stringify(data.data.data, null, 2); const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = data.data.filename.replace('.msgpack', '.json'); document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); exportResult.innerHTML = `

Экспорт завершён

БД: ${escapeHtml(dbName)}

Коллекций: ${data.data.collections}

Файл: ${data.data.filename}

`; showNotification('Экспорт завершён', 'success'); } else { exportResult.innerHTML = `
Ошибка: ${data.error}
`; } } catch (error) { exportResult.innerHTML = '
Ошибка подключения
'; } } /** * @async * @description Загружает страницу импорта данных. */ async function loadImportPage() { pageTitle.textContent = 'Импорт данных'; contentArea.innerHTML = `
`; } /** * @async * @description Выполняет импорт данных из JSON-файла. */ async function performImport() { const dbName = document.getElementById('importDbName').value; const fileInput = document.getElementById('importFile'); const overwrite = document.getElementById('importOverwrite').checked; if (!dbName) { showNotification('Введите имя целевой базы данных', 'error'); return; } if (!fileInput.files || fileInput.files.length === 0) { showNotification('Выберите файл для импорта', 'error'); return; } const importResult = document.getElementById('importResult'); importResult.innerHTML = '

Импорт данных...

'; try { const file = fileInput.files[0]; const fileContent = await file.text(); const importData = JSON.parse(fileContent); const response = await fetch('/api/webui/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ database: dbName, data: importData, overwrite }) }); const data = await response.json(); if (data.success) { importResult.innerHTML = `

Импорт завершён

БД: ${escapeHtml(dbName)}

Импортировано коллекций: ${data.data.collections}

Импортировано документов: ${data.data.documents}

`; showNotification('Импорт завершён', 'success'); } else { importResult.innerHTML = `
Ошибка: ${data.error}
`; } } catch (error) { importResult.innerHTML = `
Ошибка: ${error.message}
`; } } // ============================== CRUD ОПЕРАЦИИ (МОДАЛЬНЫЕ ОКНА) ============================== /** * @description Отображает модальное окно для создания базы данных. */ function showCreateDatabaseModal() { modalTitle.textContent = 'Создать базу данных'; modalConfirm.textContent = 'Подтвердить'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const dbName = document.getElementById('dbName').value; if (!dbName) { showNotification('Введите имя базы данных', 'error'); return; } try { const response = await fetch('/api/db/' + dbName, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (response.ok) { modal.classList.remove('show'); showNotification(`База данных "${dbName}" создана`, 'success'); loadDashboard(); } else { const error = await response.json(); showNotification(error.error || 'Ошибка создания БД', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для создания коллекции в текущей БД. */ function showCreateCollectionModal() { if (!currentDatabase) { showNotification('Сначала выберите базу данных', 'warning'); return; } modalTitle.textContent = 'Создать коллекцию'; modalConfirm.textContent = 'Подтвердить'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const collName = document.getElementById('collName').value; if (!collName) { showNotification('Введите имя коллекции', 'error'); return; } try { const response = await fetch(`/api/db/${currentDatabase}/${collName}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); if (response.ok) { modal.classList.remove('show'); showNotification(`Коллекция "${collName}" создана`, 'success'); viewDatabase(currentDatabase); } else { const error = await response.json(); showNotification(error.error || 'Ошибка создания коллекции', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для вставки документа в текущую коллекцию. */ function showInsertDocumentModal() { if (!currentDatabase || !currentCollection) { showNotification('Сначала выберите базу данных и коллекцию', 'warning'); return; } modalTitle.textContent = 'Вставить документ'; modalConfirm.textContent = 'Подтвердить'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const docData = document.getElementById('docData').value; if (!docData) { showNotification('Введите данные документа', 'error'); return; } try { const data = JSON.parse(docData); const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { modal.classList.remove('show'); showNotification('Документ вставлен', 'success'); viewCollection(currentDatabase, currentCollection); } else { showNotification(result.error || 'Ошибка вставки документа', 'error'); } } catch (error) { showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для обновления документа. * @param {string} docId - ID документа (опционально) * @param {Object|null} currentFields - Текущие поля документа */ function showUpdateDocumentModal(docId = '', currentFields = null) { if (!currentDatabase || !currentCollection) { showNotification('Сначала выберите базу данных и коллекцию', 'warning'); return; } modalTitle.textContent = 'Обновить документ'; modalConfirm.textContent = 'Обновить'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const updateDocId = document.getElementById('updateDocId').value; const updateData = document.getElementById('updateData').value; if (!updateDocId) { showNotification('Введите ID документа', 'error'); return; } if (!updateData) { showNotification('Введите данные для обновления', 'error'); return; } try { const data = JSON.parse(updateData); const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(updateDocId)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await response.json(); if (result.success) { modal.classList.remove('show'); showNotification('Документ обновлён', 'success'); viewCollection(currentDatabase, currentCollection); } else { showNotification(result.error || 'Ошибка обновления документа', 'error'); } } catch (error) { showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для создания пользователя ACL. */ function showCreateUserModal() { modalTitle.textContent = 'Создать пользователя'; modalConfirm.textContent = 'Создать'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const username = document.getElementById('username').value; const password = document.getElementById('password').value; const rolesStr = document.getElementById('roles').value; const roles = rolesStr ? rolesStr.split(',').map(r => r.trim()) : []; if (!username || !password) { showNotification('Заполните имя пользователя и пароль', 'error'); return; } try { const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ password, roles }) }); const data = await response.json(); if (data.success) { modal.classList.remove('show'); showNotification(`Пользователь ${username} создан`, 'success'); loadACLUsers(); } else { showNotification(data.error || 'Ошибка создания пользователя', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для создания роли ACL. */ function showCreateRoleModal() { modalTitle.textContent = 'Создать роль'; modalConfirm.textContent = 'Создать'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const roleName = document.getElementById('roleName').value; if (!roleName) { showNotification('Введите название роли', 'error'); return; } try { const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, { method: 'POST' }); const data = await response.json(); if (data.success) { modal.classList.remove('show'); showNotification(`Роль ${roleName} создана`, 'success'); loadACLRoles(); } else { showNotification(data.error || 'Ошибка создания роли', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для создания индекса. */ function showCreateIndexModal() { const dbName = document.getElementById('indexDbSelect')?.value; const collName = document.getElementById('indexCollSelect')?.value; if (!dbName || !collName) { showNotification('Сначала выберите базу данных и коллекцию на странице "Список индексов"', 'warning'); return; } modalTitle.textContent = 'Создать индекс'; modalConfirm.textContent = 'Создать'; modalBody.innerHTML = `
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const indexName = document.getElementById('indexName').value; const fieldsStr = document.getElementById('indexFields').value; const unique = document.getElementById('indexUnique').checked; if (!indexName || !fieldsStr) { showNotification('Заполните имя индекса и поля', 'error'); return; } const fields = fieldsStr.split(',').map(f => f.trim()); try { const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: indexName, fields, unique }) }); const data = await response.json(); if (data.success) { modal.classList.remove('show'); showNotification(`Индекс ${indexName} создан`, 'success'); loadIndexesForCollection(); } else { showNotification(data.error || 'Ошибка создания индекса', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } /** * @description Отображает модальное окно для создания триггера. */ function showCreateTriggerModal() { const dbName = document.getElementById('triggerDbSelect').value; const collName = document.getElementById('triggerCollSelect').value; if (!dbName || !collName) { showNotification('Сначала выберите базу данных и коллекцию на странице "Список триггеров"', 'warning'); return; } modalTitle.textContent = 'Создать триггер'; modalConfirm.textContent = 'Создать'; modalBody.innerHTML = `
Доступные операции: set, unset, inc, mul, rename, currentDate. Спецзначения: $$NOW, $$USER, $$ROLE
`; modal.classList.add('show'); modalConfirm.onclick = async () => { const triggerName = document.getElementById('triggerName').value; const triggerEvent = document.getElementById('triggerEvent').value; const triggerAction = document.getElementById('triggerAction').value; const triggerDescription = document.getElementById('triggerDescription').value; if (!triggerName) { showNotification('Введите имя триггера', 'error'); return; } let condition = null; const conditionField = document.getElementById('conditionField').value; if (conditionField) { condition = { field: conditionField, operator: document.getElementById('conditionOperator').value, value: document.getElementById('conditionValue').value }; if (condition.value && !isNaN(condition.value) && condition.value.trim() !== '') condition.value = parseFloat(condition.value); } let operations = []; const opsText = document.getElementById('triggerOperations').value; if (opsText && opsText.trim()) { try { operations = JSON.parse(opsText); } catch (e) { showNotification('Неверный формат JSON для операций', 'error'); return; } } const requestBody = { name: triggerName, event: triggerEvent, action: triggerAction, description: triggerDescription, operations }; if (condition) requestBody.condition = condition; try { const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); const data = await response.json(); if (data.success) { modal.classList.remove('show'); showNotification(`Триггер ${triggerName} создан`, 'success'); loadTriggersForCollection(); } else { showNotification(data.error || 'Ошибка создания триггера', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; } // ============================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ============================== /** * @description Устанавливает активный пункт навигации. * @param {HTMLElement} activeLink - Активный элемент ссылки */ function setActiveNav(activeLink) { document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active')); activeLink.classList.add('active'); } /** * @description Экранирует HTML-спецсимволы для предотвращения XSS. * @param {any} str - Входная строка * @returns {string} Экранированная строка */ function escapeHtml(str) { if (!str) return ''; return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } /** * @description Отображает всплывающее уведомление. * @param {string} message - Текст уведомления * @param {string} type - Тип уведомления (success, error, warning, info) */ function showNotification(message, type = 'info') { const container = document.getElementById('notificationContainer'); const notification = document.createElement('div'); notification.className = `notification ${type}`; const icons = { success: '', error: '', warning: '', info: '' }; notification.innerHTML = `${icons[type] || icons.info}${escapeHtml(message)}`; container.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOutRight 0.3s ease'; setTimeout(() => notification.remove(), 300); }, 3000); } /** * @description Сохраняет настройки интерфейса (тема). */ function saveSettings() { const theme = document.getElementById('themeSelect')?.value; if (theme) { localStorage.setItem('theme', theme); showNotification('Настройки сохранены', 'success'); } } // ============================== ТРАНЗАКЦИИ (ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ) ============================== /** * @async * @description Загружает детали транзакции в модальном окне. * @param {string} txId - ID транзакции */ async function loadTransactionDetails(txId) { modalTitle.textContent = `Детали транзакции ${txId}`; modalConfirm.textContent = 'Закрыть'; modalBody.innerHTML = '

Загрузка деталей...

'; modal.classList.add('show'); const originalConfirmHandler = modalConfirm.onclick; modalConfirm.onclick = () => { modal.classList.remove('show'); modalConfirm.onclick = originalConfirmHandler; }; try { const response = await fetch(`/api/webui/transaction/${txId}/details`); const data = await response.json(); if (data.success && data.data) { const tx = data.data; modalBody.innerHTML = `
ID: ${escapeHtml(tx.id)}
Статус: ${escapeHtml(tx.status)}
Время начала: ${new Date(tx.start_time).toLocaleString()}
Количество операций: ${tx.operation_count}

Операции (${tx.operations?.length || 0})

${tx.operations?.length ? `${tx.operations.map(op => ``).join('')}
ТипБДКоллекцияID документа
${escapeHtml(op.type)}${escapeHtml(op.database)}${escapeHtml(op.collection)}${escapeHtml(op.document_id)}
` : '

Нет операций

'}
`; } else { modalBody.innerHTML = `
Ошибка загрузки деталей: ${data.error || 'Неизвестная ошибка'}
`; } } catch (error) { modalBody.innerHTML = '
Ошибка подключения
'; } } /** * @async * @description Начинает новую сессию транзакций. */ async function startSession() { try { const response = await fetch('/api/webui/transactions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'start_session' }) }); const data = await response.json(); data.success ? (showNotification('Сессия начата', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } } /** * @async * @description Начинает новую транзакцию. */ async function startTransaction() { try { const response = await fetch('/api/webui/transactions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'start_transaction' }) }); const data = await response.json(); data.success ? (showNotification('Транзакция начата', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } } /** * @async * @description Фиксирует текущую транзакцию. */ async function commitTransaction() { try { const response = await fetch('/api/webui/transaction/commit', { method: 'POST' }); const data = await response.json(); data.success ? (showNotification('Транзакция зафиксирована', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } } /** * @async * @description Отменяет текущую транзакцию. */ async function abortTransaction() { try { const response = await fetch('/api/webui/transaction/abort', { method: 'POST' }); const data = await response.json(); data.success ? (showNotification('Транзакция отменена', 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } } /** * @async * @description Фиксирует транзакцию по ID. * @param {string} txId - ID транзакции */ async function commitTransactionById(txId) { if (!confirm(`Зафиксировать транзакцию ${txId}?`)) return; try { const response = await fetch(`/api/webui/transaction/${txId}/commit`, { method: 'POST' }); const data = await response.json(); data.success ? (showNotification(`Транзакция ${txId} зафиксирована`, 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка фиксации', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } } /** * @async * @description Отменяет транзакцию по ID. * @param {string} txId - ID транзакции */ async function abortTransactionById(txId) { if (!confirm(`Отменить транзакцию ${txId}?`)) return; try { const response = await fetch(`/api/webui/transaction/${txId}/abort`, { method: 'POST' }); const data = await response.json(); data.success ? (showNotification(`Транзакция ${txId} отменена`, 'success'), loadTransactionList()) : showNotification(data.error || 'Ошибка отмены', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } } // ============================== УПРАВЛЕНИЕ ДАННЫМИ (CRUD) ============================== /** * @async * @description Удаляет коллекцию. * @param {string} dbName - Имя БД * @param {string} collName - Имя коллекции */ window.deleteCollection = async function(dbName, collName) { if (!confirm(`Удалить коллекцию "${collName}"? Это действие необратимо.`)) return; try { const response = await fetch(`/api/db/${dbName}/${collName}`, { method: 'DELETE' }); response.ok ? (showNotification(`Коллекция "${collName}" удалена`, 'success'), viewDatabase(dbName)) : showNotification((await response.json()).error || 'Ошибка удаления коллекции', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Удаляет документ. * @param {string} dbName - Имя БД * @param {string} collName - Имя коллекции * @param {string} docId - ID документа */ window.deleteDocument = async function(dbName, collName, docId) { if (!confirm(`Удалить документ "${docId}"?`)) return; try { const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, { method: 'DELETE' }); const result = await response.json(); result.success ? (showNotification('Документ удалён', 'success'), viewCollection(dbName, collName)) : showNotification(result.error || 'Ошибка удаления документа', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Удаляет пользователя. * @param {string} username - Имя пользователя */ window.deleteUser = async function(username) { if (!confirm(`Удалить пользователя "${username}"?`)) return; try { const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { method: 'DELETE' }); const data = await response.json(); data.success ? (showNotification(`Пользователь ${username} удалён`, 'success'), loadACLUsers()) : showNotification(data.error || 'Ошибка удаления', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Удаляет роль. * @param {string} roleName - Имя роли */ window.deleteRole = async function(roleName) { if (!confirm(`Удалить роль "${roleName}"?`)) return; try { const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, { method: 'DELETE' }); const data = await response.json(); data.success ? (showNotification(`Роль ${roleName} удалена`, 'success'), loadACLRoles()) : showNotification(data.error || 'Ошибка удаления', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Отключает пользователя. * @param {string} username - Имя пользователя */ window.disableUser = async function(username) { try { const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disable: true }) }); const data = await response.json(); data.success ? (showNotification(`Пользователь ${username} отключён`, 'success'), loadACLUsers()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Включает пользователя. * @param {string} username - Имя пользователя */ window.enableUser = async function(username) { try { const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enable: true }) }); const data = await response.json(); data.success ? (showNotification(`Пользователь ${username} включён`, 'success'), loadACLUsers()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Отзывает разрешение у роли. * @param {string} roleName - Имя роли * @param {string} permission - Разрешение */ window.revokePermission = async function(roleName, permission) { try { const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/revoke/${encodeURIComponent(permission)}`, { method: 'PUT' }); const data = await response.json(); data.success ? (showNotification('Разрешение отозвано', 'success'), loadACLRoles()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Удаляет индекс. * @param {string} dbName - Имя БД * @param {string} collName - Имя коллекции * @param {string} indexName - Имя индекса */ window.dropIndex = async function(dbName, collName, indexName) { if (!confirm(`Удалить индекс "${indexName}"?`)) return; try { const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/drop/${encodeURIComponent(indexName)}`, { method: 'POST' }); const data = await response.json(); data.success ? (showNotification(`Индекс ${indexName} удалён`, 'success'), loadIndexesForCollection()) : showNotification(data.error || 'Ошибка удаления индекса', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Включает или отключает триггер. * @param {string} dbName - Имя БД * @param {string} collName - Имя коллекции * @param {string} triggerName - Имя триггера * @param {string} triggerEvent - Событие триггера * @param {boolean} enable - Включить (true) или отключить (false) */ window.toggleTrigger = async function(dbName, collName, triggerName, triggerEvent, enable) { const action = enable ? 'enable' : 'disable'; try { const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${action}/${encodeURIComponent(triggerName)}`, { method: 'POST' }); const data = await response.json(); data.success ? (showNotification(`Триггер ${triggerName} ${enable ? 'включён' : 'отключён'}`, 'success'), loadTriggersForCollection()) : showNotification(data.error || 'Ошибка', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Удаляет триггер. * @param {string} dbName - Имя БД * @param {string} collName - Имя коллекции * @param {string} triggerName - Имя триггера * @param {string} triggerEvent - Событие триггера */ window.deleteTrigger = async function(dbName, collName, triggerName, triggerEvent) { if (!confirm(`Удалить триггер "${triggerName}"?`)) return; try { const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/delete/${encodeURIComponent(triggerName)}/${encodeURIComponent(triggerEvent)}`, { method: 'DELETE' }); const data = await response.json(); data.success ? (showNotification(`Триггер ${triggerName} удалён`, 'success'), loadTriggersForCollection()) : showNotification(data.error || 'Ошибка удаления', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @async * @description Удаляет ограничение. * @param {string} dbName - Имя БД * @param {string} collName - Имя коллекции * @param {string} constraintType - Тип ограничения * @param {string} field - Имя поля */ window.removeConstraint = async function(dbName, collName, constraintType, field) { const confirmMsg = { required: `Удалить обязательное поле "${field}"?`, unique: `Удалить уникальное ограничение для поля "${field}"?`, min: `Удалить минимальное значение для поля "${field}"?`, max: `Удалить максимальное значение для поля "${field}"?`, enum: `Удалить перечисление для поля "${field}"?`, regex: `Удалить регулярное выражение для поля "${field}"?` }[constraintType] || `Удалить ограничение "${field}"?`; if (!confirm(confirmMsg)) return; try { const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${constraintType}/${encodeURIComponent(field)}`, { method: 'DELETE' }); const data = await response.json(); data.success ? (showNotification('Ограничение удалено', 'success'), loadConstraintsForCollection()) : showNotification(data.error || 'Ошибка удаления', 'error'); } catch (error) { showNotification('Ошибка подключения', 'error'); } }; /** * @description Обрабатывает быстрые действия из меню. * @param {string} action - Идентификатор действия (data-action) */ function handleCrudAction(action) { const actions = { 'create-db': showCreateDatabaseModal, 'create-collection': showCreateCollectionModal, 'insert-doc': showInsertDocumentModal, 'find-doc': () => showUpdateDocumentModal(), 'update-doc': () => showUpdateDocumentModal(), 'delete-doc': () => showDeleteDocumentModal(), 'acl-create-user': showCreateUserModal, 'acl-create-role': showCreateRoleModal, 'tx-start-session': startSession, 'tx-start': startTransaction, 'tx-commit': commitTransaction, 'tx-abort': abortTransaction, 'index-create': () => { if (document.getElementById('indexDbSelect')?.value && document.getElementById('indexCollSelect')?.value) showCreateIndexModal(); else showNotification('Сначала выберите БД и коллекцию на странице индексов', 'warning'); }, 'constraint-add-required': showAddRequiredConstraintModal, 'constraint-add-unique': showAddUniqueConstraintModal, 'constraint-add-min': showAddMinConstraintModal, 'constraint-add-max': showAddMaxConstraintModal, 'constraint-add-enum': showAddEnumConstraintModal, 'constraint-add-regex': showAddRegexConstraintModal }; (actions[action] || (() => showNotification('Неизвестное действие', 'warning')))(); } // ============================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ДЛЯ ОГРАНИЧЕНИЙ ============================== function showAddRequiredConstraintModal() { showConstraintModal('required', 'Добавить обязательное поле', [{ name: 'field', label: 'Имя поля', type: 'text' }]); } function showAddUniqueConstraintModal() { showConstraintModal('unique', 'Добавить уникальное поле', [{ name: 'field', label: 'Имя поля', type: 'text' }]); } function showAddMinConstraintModal() { showConstraintModal('min', 'Добавить минимальное значение', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'value', label: 'Минимальное значение', type: 'number' }]); } function showAddMaxConstraintModal() { showConstraintModal('max', 'Добавить максимальное значение', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'value', label: 'Максимальное значение', type: 'number' }]); } function showAddEnumConstraintModal() { showConstraintModal('enum', 'Добавить перечисление (Enum)', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'values', label: 'Допустимые значения (через запятую)', type: 'text' }]); } function showAddRegexConstraintModal() { showConstraintModal('regex', 'Добавить регулярное выражение', [{ name: 'field', label: 'Имя поля', type: 'text' }, { name: 'pattern', label: 'Регулярное выражение', type: 'text' }]); } function showConstraintModal(type, title, fields) { const dbName = document.getElementById('constraintDbSelect')?.value; const collName = document.getElementById('constraintCollSelect')?.value; if (!dbName || !collName) { showNotification('Сначала выберите БД и коллекцию на странице ограничений', 'warning'); return; } modalTitle.textContent = title; modalConfirm.textContent = 'Добавить'; modalBody.innerHTML = `
${fields.map(f => `
`).join('')}`; modal.classList.add('show'); modalConfirm.onclick = async () => { const field = document.getElementById('constraint_field')?.value; if (!field) { showNotification('Введите имя поля', 'error'); return; } let url = `/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${type}/${encodeURIComponent(field)}`; if (type === 'min' || type === 'max') { const value = document.getElementById('constraint_value')?.value; if (value === '') { showNotification('Введите значение', 'error'); return; } url += `/${encodeURIComponent(value)}`; } else if (type === 'enum') { const valuesStr = document.getElementById('constraint_values')?.value; if (!valuesStr) { showNotification('Введите допустимые значения', 'error'); return; } const values = valuesStr.split(',').map(v => encodeURIComponent(v.trim())); url += `/${values.join('/')}`; } else if (type === 'regex') { const pattern = document.getElementById('constraint_pattern')?.value; if (!pattern) { showNotification('Введите регулярное выражение', 'error'); return; } url += `/${encodeURIComponent(pattern)}`; } try { const response = await fetch(url, { method: 'POST' }); const data = await response.json(); if (data.success) { modal.classList.remove('show'); showNotification(`Ограничение добавлено`, 'success'); loadConstraintsForCollection(); } else { showNotification(data.error || 'Ошибка добавления', 'error'); } } catch (error) { showNotification('Ошибка подключения', 'error'); } }; }