diff --git a/internal/api/static/app.js b/internal/api/static/app.js deleted file mode 100644 index ed9ff77..0000000 --- a/internal/api/static/app.js +++ /dev/null @@ -1,1921 +0,0 @@ -/* - * 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'); } - }; -}