/*
* 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 = ` `;
}
/**
* @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 => `${escapeHtml(db.name)} ${db.collections} Просмотр `).join('')}
`;
} 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 => `
${escapeHtml(coll.name)}
${coll.count}
${(coll.size / 1024).toFixed(2)} KB
${coll.indexes.length}
`).join('')}
`;
} 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} всего) ID Поля Создан Действия
${data.data.documents.map(doc => `
${escapeHtml(doc.id)}
${escapeHtml(JSON.stringify(doc.fields, null, 2))}
${new Date(doc.created_at).toLocaleString()}
`).join('')}
`;
} 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} Фактор репликации
Узлы кластера ID узла Адрес Статус Последний контакт
${nodes.data.map(node => `${escapeHtml(node.id)}${escapeHtml(node.ip)}:${node.port} ${node.status} ${new Date(node.last_seen * 1000).toLocaleString()} `).join('')}
`;
} 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 => `
${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() : '-'}
`).join('')}
`;
} 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 => `
${escapeHtml(role.name)}
${role.permissions.map(p => `${escapeHtml(p)}`).join(' ') || '-'}
Права
`).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 = `
Начать сессию
Начать транзакцию
Зафиксировать
Отменить
Обновить
Транзакции ID транзакции Статус Начало Операций Действия
${data.data.map(tx => `
${escapeHtml(tx.id)}
${escapeHtml(tx.status)}
${new Date(tx.start_time).toLocaleString()}
${tx.operation_count || 0}
Детали
${tx.status === 'active' ? ` Commit Abort ` : ''}
`).join('') || 'Нет активных транзакций '}
Информация о транзакциях: Транзакции поддерживают INSERT, UPDATE, DELETE Используется MVCC для изоляции WAL гарантирует персистентность
`;
} 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 => `${escapeHtml(db.name)} `).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 => `${escapeHtml(coll.name)} `).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 => `${escapeHtml(idx.name)}${idx.fields.join(', ')} ${idx.unique ? 'Да' : 'Нет'} Удалить `).join('') || 'Нет индексов '}
`;
} 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 => `${escapeHtml(db.name)} `).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 => `${escapeHtml(coll.name)} `).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 => `
${escapeHtml(trigger.name)}
${escapeHtml(trigger.event)} ${escapeHtml(trigger.action)}
${trigger.enabled ? 'Включён ' : 'Отключён '}
${escapeHtml(trigger.description || '-')}
${trigger.enabled ? `Отключить ` : `Включить `}
Удалить
`).join('')}
`;
} 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 => `${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 || '-')} `).join('')}
`;
} 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 => `${escapeHtml(db.name)} `).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 => `${escapeHtml(coll.name)} `).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}Поле ${type === 'enum' ? 'Допустимые значения' : (type === 'min' || type === 'max' ? 'Значение' : (type === 'regex' ? 'Шаблон' : ''))} Действия `;
for (const c of grouped[type]) {
html += `${escapeHtml(c.field)}${type === 'enum' ? c.values.map(v => `${escapeHtml(String(v))} `).join(' ') : (c.value || c.pattern || '-')} Удалить `;
}
html += `
`;
}
}
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 => `${escapeHtml(db.name)} `).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 = `
Целевая база данных
JSON файл с данными
Перезаписывать существующие документы
Импортировать
`;
}
/**
* @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 = `База данных
Коллекция
Данные документа (JSON)
`;
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 = `База данных
Коллекция
ID документа
Обновления (JSON)
`;
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 = `
База данных
Коллекция
Имя триггера
Событие BEFORE_INSERT AFTER_INSERT BEFORE_UPDATE AFTER_UPDATE BEFORE_DELETE AFTER_DELETE
Действие modify - Модифицировать документ log - Записать в лог abort - Прервать операцию skip - Пропустить операцию notify - Отправить уведомление
Описание (опционально)
Операции триггера (JSON массив) Доступные операции: 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 ? `
Тип БД Коллекция ID документа ${tx.operations.map(op => `${escapeHtml(op.type)} ${escapeHtml(op.database)} ${escapeHtml(op.collection)} ${escapeHtml(op.document_id)} `).join('')}
` : '
Нет операций
'}
`;
} 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 => `${f.label}
`).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'); }
};
}