diff --git a/internal/api/static/app.js b/internal/api/static/app.js new file mode 100644 index 0000000..338003e --- /dev/null +++ b/internal/api/static/app.js @@ -0,0 +1,2351 @@ +/* + * 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 + */ + +// Файл: internal/api/static/app.js +// JavaScript для веб-интерфейса Futriis DB Dashboard + +// Глобальное состояние +let currentSession = null; +let currentDatabase = null; +let currentCollection = null; +let currentUser = null; + +// DOM элементы +const contentArea = document.getElementById('contentArea'); +const pageTitle = document.getElementById('pageTitle'); +const connectionStatus = document.getElementById('connectionStatus'); +const userInfoSpan = document.querySelector('#userInfo span'); +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'); + +// Инициализация приложения +document.addEventListener('DOMContentLoaded', () => { + checkSession(); + initNavigation(); + initEventListeners(); +}); + +// Проверка сессии +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.connection_status === 'connected') { + connectionStatus.className = 'connection-status online'; + connectionStatus.innerHTML = 'СУБД подключена'; + } else { + connectionStatus.className = 'connection-status offline'; + connectionStatus.innerHTML = 'СУБД не подключена'; + } + + loadDashboard(); + } else { + showLoginModal(); + } + } catch (error) { + console.error('Session check failed:', error); + showLoginModal(); + } +} + +// Функция для проверки статуса подключения +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); +} + +// Показать модальное окно входа +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; + 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); +} + +// Инициализация навигации +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'); + }); + }); +} + +// Инициализация обработчиков событий +function initEventListeners() { + logoutBtn.addEventListener('click', async () => { + await fetch('/api/webui/logout', { method: 'POST' }); + currentSession = null; + currentUser = null; + connectionStatus.className = 'connection-status offline'; + connectionStatus.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 function loadSection(section) { + switch(section) { + case 'dashboard': + loadDashboard(); + break; + case 'cluster': + loadClusterManagement(); + break; + case 'audit': + loadAuditLog(); + break; + case 'settings': + loadSettings(); + break; + case 'acl-users': + loadACLUsers(); + break; + case 'acl-roles': + loadACLRoles(); + break; + case 'acl-permissions': + loadACLPermissions(); + break; + case 'tx-list': + loadTransactionList(); + break; + case 'indexes-list': + loadIndexesList(); + break; + case 'export-data': + loadExportPage(); + break; + case 'import-data': + loadImportPage(); + break; + default: + loadDashboard(); + } +} + +// Загрузка дашборда +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'); + } +} + +// Просмотр базы данных +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 = '
Ошибка подключения
'; + } +}; + +// Просмотр коллекции +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 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 = '
Ошибка загрузки информации о кластере
'; + } +} + +// Загрузка лога аудита +async function loadAuditLog() { + pageTitle.textContent = 'Лог аудита'; + contentArea.innerHTML = '

Загрузка лога аудита...

'; + contentArea.innerHTML = '
Функция в разработке
'; +} + +// Загрузка настроек +function loadSettings() { + pageTitle.textContent = 'Настройки'; + contentArea.innerHTML = ` +
+

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

+
+ + +
+ +
+ `; +} + +// ==================== ACL Functions ==================== + +// Загрузка списка пользователей +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 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 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 = '
Ошибка подключения
'; + } +} + +// Показать модальное окно создания пользователя +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'); + } + }; +} + +// Показать модальное окно редактирования пользователя +function showEditUserModal(username, currentRoles) { + modalTitle.textContent = `Редактировать пользователя: ${username}`; + modalConfirm.textContent = 'Сохранить'; + modalBody.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ `; + + modal.classList.add('show'); + + modalConfirm.onclick = async () => { + const newPassword = document.getElementById('newPassword').value; + const addRole = document.getElementById('addRole').value; + const removeRole = document.getElementById('removeRole').value; + + const updates = {}; + if (newPassword) updates.password = newPassword; + if (addRole) updates.add_role = addRole; + if (removeRole) updates.remove_role = removeRole; + + if (Object.keys(updates).length === 0) { + showNotification('Нет изменений для сохранения', 'warning'); + return; + } + + try { + const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(updates) + }); + + 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'); + } + }; +} + +// Показать модальное окно создания роли +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'); + } + }; +} + +// Показать модальное окно редактирования роли +function showEditRoleModal(roleName, currentPermissions) { + modalTitle.textContent = `Редактировать роль: ${roleName}`; + modalConfirm.textContent = 'Добавить разрешение'; + modalBody.innerHTML = ` +
+ +
+ ${currentPermissions.map(p => ` +
+ ${escapeHtml(p)} + +
+ `).join('') || 'Нет разрешений'} +
+
+
+ + + Формат: database.collection:read|write|delete|admin (можно использовать * как wildcard) +
+ `; + + modal.classList.add('show'); + + modalConfirm.onclick = async () => { + const permission = document.getElementById('newPermission').value; + + if (!permission) { + showNotification('Введите разрешение', 'error'); + return; + } + + try { + const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/grant/${encodeURIComponent(permission)}`, { + method: 'PUT' + }); + + const data = await response.json(); + + if (data.success) { + showNotification(`Разрешение ${permission} добавлено`, 'success'); + modal.classList.remove('show'); + loadACLRoles(); + } else { + showNotification(data.error || 'Ошибка добавления разрешения', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; +} + +// ==================== Transaction Functions ==================== + +// Загрузка списка транзакций +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}
Нет активных транзакций
+
+ `; + } else { + contentArea.innerHTML = '
Ошибка загрузки транзакций
'; + } + } catch (error) { + contentArea.innerHTML = '
Ошибка подключения
'; + } +} + +// ==================== Index Functions ==================== + +// Загрузка списка индексов +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'); + } +} + +// Загрузка коллекций для выбранной БД +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 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 = '
Ошибка подключения
'; + } +} + +// ==================== Import/Export Functions ==================== + +// Загрузка страницы экспорта +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 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) { + // Создаём JSON файл для скачивания + const exportData = data.data.data; + const jsonStr = JSON.stringify(exportData, 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 function loadImportPage() { + pageTitle.textContent = 'Импорт данных'; + contentArea.innerHTML = ` +
+ + +
+
+ + +
+
+ +
+ +
+ `; +} + +// Выполнение импорта +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 Action Handlers ==================== + +// Обработка CRUD действий +function handleCrudAction(action) { + switch(action) { + case 'create-db': + showCreateDatabaseModal(); + break; + case 'create-collection': + showCreateCollectionModal(); + break; + case 'insert-doc': + showInsertDocumentModal(); + break; + case 'find-doc': + showFindDocumentModal(); + break; + case 'update-doc': + showUpdateDocumentModal(); + break; + case 'delete-doc': + showDeleteDocumentModal(); + break; + case 'acl-create-user': + showCreateUserModal(); + break; + case 'acl-create-role': + showCreateRoleModal(); + break; + case 'tx-start-session': + startSession(); + break; + case 'tx-start': + startTransaction(); + break; + case 'tx-commit': + commitTransaction(); + break; + case 'tx-abort': + abortTransaction(); + break; + case 'index-create': + showCreateIndexModal(); + break; + } +} + +// Показать модальное окно создания БД +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'); + } + }; +} + +// Показать модальное окно создания коллекции +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'); + } + }; +} + +// Показать модальное окно вставки документа +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) { + if (error instanceof SyntaxError) { + showNotification('Неверный формат JSON', 'error'); + } else { + showNotification('Ошибка подключения', 'error'); + } + } + }; +} + +// Показать модальное окно поиска документа +function showFindDocumentModal() { + if (!currentDatabase || !currentCollection) { + showNotification('Сначала выберите базу данных и коллекцию', 'warning'); + return; + } + + modalTitle.textContent = 'Найти документ'; + modalConfirm.textContent = 'Найти'; + modalBody.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + + modal.classList.add('show'); + + modalConfirm.onclick = async () => { + const docId = document.getElementById('docId').value; + if (!docId) { + showNotification('Введите ID документа', 'error'); + return; + } + + try { + const response = await fetch(`/api/db/${currentDatabase}/${currentCollection}/${docId}`); + + if (response.ok) { + const data = await response.json(); + modal.classList.remove('show'); + + contentArea.innerHTML = ` +
+

Результат поиска

+
+                            ${escapeHtml(JSON.stringify(data.data, null, 2))}
+                        
+ +
+ `; + } else { + const error = await response.json(); + showNotification(error.error || 'Документ не найден', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + }; +} + +// Показать модальное окно обновления документа +function showUpdateDocumentModal(docId, currentFields = null) { + if (!currentDatabase || !currentCollection) { + showNotification('Сначала выберите базу данных и коллекцию', 'warning'); + return; + } + + const fieldsJson = currentFields ? JSON.stringify(currentFields, null, 2) : ''; + + 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) { + if (error instanceof SyntaxError) { + showNotification('Неверный формат JSON', 'error'); + } else { + showNotification('Ошибка подключения', 'error'); + } + } + }; +} + +// Показать модальное окно удаления документа +function showDeleteDocumentModal() { + if (!currentDatabase || !currentCollection) { + showNotification('Сначала выберите базу данных и коллекцию', 'warning'); + return; + } + + modalTitle.textContent = 'Удалить документ'; + modalConfirm.textContent = 'Удалить'; + modalBody.innerHTML = ` +
+ + +
+
+ + +
+
+ + +
+ `; + + modal.classList.add('show'); + + modalConfirm.onclick = async () => { + const docId = document.getElementById('deleteDocId').value; + if (!docId) { + showNotification('Введите ID документа', 'error'); + return; + } + + try { + const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(docId)}`, { + method: 'DELETE' + }); + + 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'); + } + }; +} + +// Показать модальное окно создания индекса +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'); + } + }; +} + +// ==================== Вспомогательные функции ==================== + +// Удаление коллекции +window.deleteCollection = async function(dbName, collName) { + if (confirm(`Вы уверены, что хотите удалить коллекцию "${collName}"? Это действие необратимо.`)) { + try { + const response = await fetch(`/api/db/${dbName}/${collName}`, { + method: 'DELETE' + }); + + if (response.ok) { + showNotification(`Коллекция "${collName}" удалена`, 'success'); + viewDatabase(dbName); + } else { + const error = await response.json(); + showNotification(error.error || 'Ошибка удаления коллекции', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + } +}; + +// Удаление документа +window.deleteDocument = async function(dbName, collName, docId) { + if (confirm(`Вы уверены, что хотите удалить документ "${docId}"?`)) { + try { + const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, { + method: 'DELETE' + }); + + const result = await response.json(); + + if (result.success) { + showNotification('Документ удалён', 'success'); + viewCollection(dbName, collName); + } else { + showNotification(result.error || 'Ошибка удаления документа', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + } +}; + +// Удаление пользователя +window.deleteUser = async function(username) { + if (confirm(`Удалить пользователя "${username}"?`)) { + try { + const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, { + method: 'DELETE' + }); + + const data = await response.json(); + if (data.success) { + showNotification(`Пользователь ${username} удалён`, 'success'); + loadACLUsers(); + } else { + showNotification(data.error || 'Ошибка удаления', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + } +}; + +// Удаление роли +window.deleteRole = async function(roleName) { + if (confirm(`Удалить роль "${roleName}"?`)) { + try { + const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, { + method: 'DELETE' + }); + + const data = await response.json(); + if (data.success) { + showNotification(`Роль ${roleName} удалена`, 'success'); + loadACLRoles(); + } else { + showNotification(data.error || 'Ошибка удаления', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + } +}; + +// Отключение пользователя +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(); + if (data.success) { + showNotification(`Пользователь ${username} отключён`, 'success'); + loadACLUsers(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// Включение пользователя +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(); + if (data.success) { + showNotification(`Пользователь ${username} включён`, 'success'); + loadACLUsers(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// Отзыв разрешения у роли +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(); + if (data.success) { + showNotification(`Разрешение отозвано`, 'success'); + loadACLRoles(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// Удаление индекса +window.dropIndex = async function(dbName, collName, indexName) { + if (confirm(`Удалить индекс "${indexName}"?`)) { + try { + const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/drop/${encodeURIComponent(indexName)}`, { + method: 'POST' + }); + + const data = await response.json(); + + if (data.success) { + showNotification(`Индекс ${indexName} удалён`, 'success'); + loadIndexesForCollection(); + } else { + showNotification(data.error || 'Ошибка удаления индекса', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + } +}; + +// Функции для транзакций +window.startSession = async function() { + 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(); + if (data.success) { + showNotification('Сессия начата', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +window.startTransaction = async function() { + 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(); + if (data.success) { + showNotification('Транзакция начата', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +window.commitTransaction = async function() { + try { + const response = await fetch('/api/webui/transaction/commit', { + method: 'POST' + }); + + const data = await response.json(); + if (data.success) { + showNotification('Транзакция зафиксирована', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +window.abortTransaction = async function() { + try { + const response = await fetch('/api/webui/transaction/abort', { + method: 'POST' + }); + + const data = await response.json(); + if (data.success) { + showNotification('Транзакция отменена', 'success'); + loadTransactionList(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// Сохранение настроек +function saveSettings() { + const theme = document.getElementById('themeSelect')?.value; + if (theme) { + localStorage.setItem('theme', theme); + showNotification('Настройки сохранены', 'success'); + } +} + +// Утилиты +function escapeHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function showNotification(message, type = 'info') { + const container = document.getElementById('notificationContainer'); + const notification = document.createElement('div'); + notification.className = `notification ${type}`; + + let icon = ''; + switch(type) { + case 'success': icon = ''; break; + case 'error': icon = ''; break; + case 'warning': icon = ''; break; + default: icon = ''; + } + + notification.innerHTML = `${icon}${escapeHtml(message)}`; + container.appendChild(notification); + + setTimeout(() => { + notification.style.animation = 'slideOutRight 0.3s ease'; + setTimeout(() => notification.remove(), 300); + }, 3000); +} + +function setActiveNav(activeLink) { + document.querySelectorAll('.nav-link').forEach(link => { + link.classList.remove('active'); + }); + activeLink.classList.add('active'); +} + +// ==================== Trigger Functions ==================== + +// Загрузка списка триггеров +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'); + } +} + +// Загрузка коллекций для выбранной БД (для триггеров) +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 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 = '
Ошибка подключения
'; + } +} + +// Показать модальное окно создания триггера +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: 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'); + } + }; +} + +// Включение/отключение триггера +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(); + + if (data.success) { + showNotification(`Триггер ${triggerName} ${enable ? 'включён' : 'отключён'}`, 'success'); + loadTriggersForCollection(); + } else { + showNotification(data.error || 'Ошибка', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } +}; + +// Удаление триггера +window.deleteTrigger = async function(dbName, collName, triggerName, triggerEvent) { + if (confirm(`Удалить триггер "${triggerName}"?`)) { + try { + const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/delete/${encodeURIComponent(triggerName)}/${encodeURIComponent(triggerEvent)}`, { + method: 'DELETE' + }); + + const data = await response.json(); + + if (data.success) { + showNotification(`Триггер ${triggerName} удалён`, 'success'); + loadTriggersForCollection(); + } else { + showNotification(data.error || 'Ошибка удаления', 'error'); + } + } catch (error) { + showNotification('Ошибка подключения', 'error'); + } + } +}; + +// Загрузка лога выполнения триггеров +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 = '
Ошибка подключения
'; + } +} + +// Обновляем loadSection для секции триггеров +const originalLoadSectionTriggers = window.loadSection; +window.loadSection = function(section) { + switch(section) { + case 'triggers-list': + loadTriggersList(); + break; + case 'trigger-log': + loadTriggerLog(); + break; + default: + if (originalLoadSectionTriggers) originalLoadSectionTriggers(section); + } +};