/*
* 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
*/
/**
* Futriis DB Dashboard - Web Interface JavaScript для веб-интерфейса Futriis DB Dashboard
* @version 1.0.0
* @description Обеспечивает полное управление СУБД: CRUD операции, ACL, индексы,
* транзакции, триггеры, ограничения (constraints), импорт/экспорт,
* управление кластером и аудит. Использует async/await, Fetch API,
* динамическую отрисовку DOM и модальные окна.
* @author Futriis Team
* @license CDDL-1.0
*/
// ======================== СОСТОЯНИЕ ПРИЛОЖЕНИЯ ========================
/** @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;
/** @type {boolean} Флаг администратора */
let isAdmin = false;
// ======================== DOM ЭЛЕМЕНТЫ ========================
const DOM = {
content: document.getElementById('contentArea'),
title: document.getElementById('pageTitle'),
connection: document.getElementById('connectionStatus'),
userName: document.querySelector('#userName'),
userRole: document.getElementById('userRole'),
logoutBtn: document.getElementById('logoutBtn'),
menuToggle: document.getElementById('menuToggle'),
sidebar: document.querySelector('.sidebar'),
modal: document.getElementById('modal'),
modalTitle: document.getElementById('modalTitle'),
modalBody: document.getElementById('modalBody'),
modalConfirm: document.getElementById('modalConfirm'),
modalCloseBtns: document.querySelectorAll('.modal-close'),
changePasswordIcon: document.getElementById('changePasswordIcon'),
userAvatar: document.getElementById('userAvatar'),
notificationContainer: document.getElementById('notificationContainer')
};
// ======================== ИНИЦИАЛИЗАЦИЯ ========================
/**
* Главная точка входа. Выполняется после загрузки DOM.
*/
document.addEventListener('DOMContentLoaded', async () => {
await checkSession();
initNavigation();
initEventListeners();
initAvatarUpload();
initChangePassword();
});
// ======================== АУТЕНТИФИКАЦИЯ ========================
/**
* Проверяет активность сессии на сервере
*/
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;
isAdmin = data.data.is_admin || false;
DOM.userName.textContent = currentUser;
DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь';
if (data.data.avatar) {
updateAvatarDisplay(data.data.avatar);
} else {
loadUserAvatar();
}
updateConnectionStatus(data.data.connection_status);
loadDashboard();
startConnectionStatusMonitor();
} 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();
updateConnectionStatus(data.data?.connection_status || 'disconnected');
} catch (error) {
updateConnectionStatus('disconnected');
}
}
}, 5000);
}
/**
* Обновляет индикатор подключения
* @param {string} status - Статус подключения ('connected' или 'disconnected')
*/
function updateConnectionStatus(status) {
if (status === 'connected') {
DOM.connection.className = 'connection-status online';
DOM.connection.innerHTML = 'СУБД подключена ';
} else {
DOM.connection.className = 'connection-status offline';
DOM.connection.innerHTML = 'СУБД не подключена ';
}
}
/**
* Отображает модальное окно для входа в систему
*/
function showLoginModal() {
DOM.modalTitle.textContent = 'Вход в систему СУБД Futriis';
DOM.modalBody.innerHTML = `
Имя пользователя
Пароль
`;
DOM.modalConfirm.textContent = 'Войти';
DOM.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;
isAdmin = data.data.is_admin || false;
DOM.userName.textContent = username;
DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь';
if (data.data.avatar) {
updateAvatarDisplay(data.data.avatar);
}
DOM.modal.classList.remove('show');
showNotification('Вход выполнен успешно', 'success');
updateConnectionStatus('connected');
startConnectionStatusMonitor();
loadDashboard();
} else {
showNotification(data.error || 'Неверный логин и/или пароль', 'error');
}
} catch (error) {
showNotification('Ошибка подключения к серверу', 'error');
}
};
DOM.modalConfirm.onclick = confirmHandler;
const handleEnter = (e) => {
if (e.key === 'Enter') {
confirmHandler();
document.removeEventListener('keydown', handleEnter);
}
};
document.addEventListener('keydown', handleEnter);
}
// ======================== АВАТАР ПОЛЬЗОВАТЕЛЯ ========================
/**
* Загружает аватар пользователя с сервера
*/
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);
}
}
/**
* Обновляет отображение аватара
* @param {string} avatarBase64 - Аватар в формате base64
*/
function updateAvatarDisplay(avatarBase64) {
if (!DOM.userAvatar) return;
DOM.userAvatar.innerHTML = ` `;
}
/**
* Инициализирует загрузку аватара
*/
function initAvatarUpload() {
if (!DOM.userAvatar) return;
DOM.userAvatar.style.cursor = 'pointer';
DOM.userAvatar.addEventListener('click', () => {
showAvatarUploadModal();
});
}
/**
* Показывает модальное окно загрузки аватара
*/
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');
};
});
}
// ======================== СМЕНА ПАРОЛЯ ========================
/**
* Инициализирует смену пароля
*/
function initChangePassword() {
if (DOM.changePasswordIcon) {
DOM.changePasswordIcon.addEventListener('click', () => {
showChangePasswordModal();
});
}
}
/**
* Показывает модальное окно смены пароля
*/
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?.value || '';
const newPassword = newPasswordInput?.value || '';
const confirmPassword = 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');
};
});
}
// ======================== НАВИГАЦИЯ ========================
/**
* Инициализирует обработчики навигации
*/
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;
handleAction(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() {
DOM.logoutBtn.addEventListener('click', async () => {
await fetch('/api/webui/logout', { method: 'POST' });
currentUser = null;
isAdmin = false;
updateConnectionStatus('disconnected');
if (DOM.userAvatar) {
DOM.userAvatar.innerHTML = ' ';
}
showLoginModal();
});
if (DOM.menuToggle) {
DOM.menuToggle.addEventListener('click', () => {
DOM.sidebar.classList.toggle('open');
});
}
DOM.modalCloseBtns.forEach(btn => {
btn.addEventListener('click', () => {
DOM.modal.classList.remove('show');
});
});
DOM.modal.addEventListener('click', (e) => {
if (e.target === DOM.modal) {
DOM.modal.classList.remove('show');
}
});
}
/**
* Загружает соответствующую секцию интерфейса
* @param {string} section - Идентификатор секции
*/
async function loadSection(section) {
const sections = {
dashboard: loadDashboard,
cluster: loadClusterManagement,
logs: loadLogs,
'plugins-list': loadPluginsList,
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
};
const loader = sections[section];
if (loader) {
await loader();
} else {
DOM.content.innerHTML = 'Раздел в разработке
';
}
}
/**
* Устанавливает активный пункт навигации
* @param {HTMLElement} activeLink - Активный элемент ссылки
*/
function setActiveNav(activeLink) {
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
activeLink.classList.add('active');
}
// ======================== ДАШБОРД ========================
/**
* Загружает и отображает главную панель управления
*/
async function loadDashboard() {
DOM.title.textContent = 'Панель управления';
DOM.content.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();
DOM.content.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) {
DOM.content.innerHTML = 'Ошибка загрузки данных
';
showNotification('Ошибка загрузки дашборда', 'error');
}
}
// ======================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ========================
/**
* Отображает список коллекций в выбранной базе данных
* @param {string} dbName - Имя базы данных
*/
window.viewDatabase = async function(dbName) {
currentDatabase = dbName;
DOM.title.textContent = `База данных: ${dbName}`;
DOM.content.innerHTML = '';
try {
const response = await fetch(`/api/webui/collections/${dbName}`);
const data = await response.json();
if (data.success) {
DOM.content.innerHTML = `
Коллекции
Создать коллекцию
Имя коллекции Документов Размер Индексы Действия
${data.data.collections.map(coll => `
${escapeHtml(coll.name)}
${coll.count}
${(coll.size / 1024).toFixed(2)} KB
${coll.indexes.length}
`).join('')}
`;
} else {
DOM.content.innerHTML = 'Ошибка загрузки коллекций
';
}
} catch (error) {
DOM.content.innerHTML = 'Ошибка подключения
';
}
};
/**
* Отображает документы выбранной коллекции
* @param {string} dbName - Имя базы данных
* @param {string} collName - Имя коллекции
*/
window.viewCollection = async function(dbName, collName) {
currentDatabase = dbName;
currentCollection = collName;
DOM.title.textContent = `Коллекция: ${dbName}.${collName}`;
DOM.content.innerHTML = '';
try {
const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`);
const data = await response.json();
if (data.success) {
DOM.content.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 {
DOM.content.innerHTML = 'Ошибка загрузки документов
';
}
} catch (error) {
DOM.content.innerHTML = 'Ошибка подключения
';
}
};
/**
* Удаляет коллекцию
* @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' });
if (response.ok) {
showNotification(`Коллекция "${collName}" удалена`, 'success');
viewDatabase(dbName);
} else {
const error = await response.json();
showNotification(error.error || 'Ошибка удаления коллекции', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
/**
* Удаляет документ
* @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();
if (result.success) {
showNotification('Документ удалён', 'success');
viewCollection(dbName, collName);
} else {
showNotification(result.error || 'Ошибка удаления документа', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// ======================== ЛОГИ ОПЕРАЦИЙ ========================
/**
* Загружает и отображает лог операций веб-интерфейса
*/
async function loadLogs() {
if (!isAdmin) {
DOM.content.innerHTML = 'Доступ запрещён. Только для администраторов.
';
return;
}
DOM.title.textContent = 'Лог операций';
DOM.content.innerHTML = '';
try {
const response = await fetch('/api/webui/logs?limit=500');
const data = await response.json();
if (data.success) {
const logs = data.data;
if (logs.length === 0) {
DOM.content.innerHTML = 'Лог операций пуст
';
return;
}
DOM.content.innerHTML = `
Лог операций веб-интерфейса
Обновить
Время Операция Цель Пользователь Статус Ошибка
${logs.map(log => `
${new Date(log.timestamp).toLocaleString()}
${escapeHtml(log.operation)}
${escapeHtml(log.target)}
${escapeHtml(log.user)}
${log.status === 'success' ? 'Успех' : 'Ошибка'}
${log.error_msg ? `${escapeHtml(log.error_msg)} ` : '-'}
`).join('')}
`;
} else {
DOM.content.innerHTML = 'Ошибка загрузки логов
';
}
} catch (error) {
DOM.content.innerHTML = 'Ошибка подключения
';
}
}
// ======================== ПЛАГИНЫ ========================
/**
* Загружает и отображает список плагинов
*/
async function loadPluginsList() {
if (!isAdmin) {
DOM.content.innerHTML = 'Доступ запрещён. Только для администраторов.
';
return;
}
DOM.title.textContent = 'Управление плагинами';
DOM.content.innerHTML = '';
try {
const response = await fetch('/api/webui/plugins');
const data = await response.json();
if (data.success) {
const plugins = data.data;
DOM.content.innerHTML = `
Загрузить плагин
Обновить
Плагины
Имя Версия Автор Описание Загружен Действия
${plugins.map(plugin => `
${escapeHtml(plugin.name)}
${escapeHtml(plugin.version)}
${escapeHtml(plugin.author)}
${escapeHtml(plugin.description)}
${new Date(plugin.loaded_at).toLocaleString()}
Старт
Стоп
Удалить
`).join('')}
${plugins.length === 0 ? 'Нет загруженных плагинов ' : ''}
`;
} else {
DOM.content.innerHTML = 'Ошибка загрузки плагинов
';
}
} catch (error) {
DOM.content.innerHTML = 'Ошибка подключения
';
}
}
/**
* Показывает модальное окно загрузки плагина
*/
function showUploadPluginModal() {
DOM.modalTitle.textContent = 'Загрузить плагин';
DOM.modalBody.innerHTML = `
Файл плагина (.lua)
Плагины должны быть написаны на Lua и содержать функции on_load, on_start, on_stop, on_unload
`;
DOM.modalConfirm.textContent = 'Загрузить';
DOM.modal.classList.add('show');
DOM.modalConfirm.onclick = async () => {
const fileInput = document.getElementById('pluginFile');
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
showNotification('Выберите файл плагина', 'warning');
return;
}
const formData = new FormData();
formData.append('plugin', fileInput.files[0]);
try {
const response = await fetch('/api/webui/plugin/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
DOM.modal.classList.remove('show');
showNotification('Плагин загружен', 'success');
loadPluginsList();
} else {
showNotification(data.error || 'Ошибка загрузки плагина', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
/**
* Запускает плагин
* @param {string} pluginName - Имя плагина
*/
window.startPlugin = async function(pluginName) {
try {
const response = await fetch(`/api/webui/plugin/${pluginName}/start`, { method: 'POST' });
const data = await response.json();
if (data.success) {
showNotification(`Плагин ${pluginName} запущен`, 'success');
loadPluginsList();
} else {
showNotification(data.error || 'Ошибка запуска', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
/**
* Останавливает плагин
* @param {string} pluginName - Имя плагина
*/
window.stopPlugin = async function(pluginName) {
try {
const response = await fetch(`/api/webui/plugin/${pluginName}/stop`, { method: 'POST' });
const data = await response.json();
if (data.success) {
showNotification(`Плагин ${pluginName} остановлен`, 'success');
loadPluginsList();
} else {
showNotification(data.error || 'Ошибка остановки', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
/**
* Удаляет плагин
* @param {string} pluginName - Имя плагина
*/
window.deletePlugin = async function(pluginName) {
if (!confirm(`Удалить плагин "${pluginName}"?`)) return;
try {
const response = await fetch(`/api/webui/plugin/${pluginName}/delete`, { method: 'DELETE' });
const data = await response.json();
if (data.success) {
showNotification(`Плагин ${pluginName} удалён`, 'success');
loadPluginsList();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// ======================== ОСТАЛЬНЫЕ СЕКЦИИ ========================
/**
* Загружает страницу управления кластером
*/
async function loadClusterManagement() {
DOM.title.textContent = 'Управление кластером';
DOM.content.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();
DOM.content.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) {
DOM.content.innerHTML = 'Ошибка загрузки информации о кластере
';
}
}
/**
* Загружает страницу настроек
*/
function loadSettings() {
DOM.title.textContent = 'Настройки';
DOM.content.innerHTML = `
Настройки интерфейса
Тема оформления Тёмная Светлая
Сохранить настройки
`;
}
/**
* Сохраняет настройки интерфейса
*/
function saveSettings() {
const theme = document.getElementById('themeSelect')?.value;
if (theme) {
localStorage.setItem('theme', theme);
showNotification('Настройки сохранены', 'success');
}
}
// ======================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========================
/**
* Экранирует HTML-спецсимволы для предотвращения XSS
* @param {any} str - Входная строка
* @returns {string} Экранированная строка
*/
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
}
/**
* Отображает всплывающее уведомление
* @param {string} message - Текст уведомления
* @param {string} type - Тип уведомления (success, error, warning, info)
*/
function showNotification(message, type = 'info') {
const container = DOM.notificationContainer || document.getElementById('notificationContainer');
if (!container) return;
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);
}
/**
* Обрабатывает быстрые действия из меню
* @param {string} action - Идентификатор действия
*/
function handleAction(action) {
const actions = {
'create-db': showCreateDatabaseModal,
'create-collection': showCreateCollectionModal,
'insert-doc': showInsertDocumentModal,
'update-doc': () => showUpdateDocumentModal(),
'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');
}
},
'plugin-upload': showUploadPluginModal,
'constraint-add-required': () => showConstraintModal('required'),
'constraint-add-unique': () => showConstraintModal('unique'),
'constraint-add-min': () => showConstraintModal('min'),
'constraint-add-max': () => showConstraintModal('max'),
'constraint-add-enum': () => showConstraintModal('enum'),
'constraint-add-regex': () => showConstraintModal('regex')
};
const handler = actions[action];
if (handler) {
handler();
} else {
showNotification('Неизвестное действие', 'warning');
}
}
// ======================== МОДАЛЬНЫЕ ОКНА ДЛЯ CRUD ========================
/**
* Показывает модальное окно создания базы данных
*/
function showCreateDatabaseModal() {
DOM.modalTitle.textContent = 'Создать базу данных';
DOM.modalBody.innerHTML = `Имя базы данных
`;
DOM.modalConfirm.textContent = 'Создать';
DOM.modal.classList.add('show');
DOM.modalConfirm.onclick = async () => {
const dbName = document.getElementById('dbName').value;
if (!dbName) {
showNotification('Введите имя базы данных', 'error');
return;
}
try {
const response = await fetch('/api/db/' + dbName, { method: 'POST' });
if (response.ok) {
DOM.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;
}
DOM.modalTitle.textContent = 'Создать коллекцию';
DOM.modalBody.innerHTML = `
База данных
Имя коллекции
`;
DOM.modalConfirm.textContent = 'Создать';
DOM.modal.classList.add('show');
DOM.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' });
if (response.ok) {
DOM.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;
}
DOM.modalTitle.textContent = 'Вставить документ';
DOM.modalBody.innerHTML = `
База данных
Коллекция
Данные документа (JSON)
`;
DOM.modalConfirm.textContent = 'Вставить';
DOM.modal.classList.add('show');
DOM.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) {
DOM.modal.classList.remove('show');
showNotification('Документ вставлен', 'success');
viewCollection(currentDatabase, currentCollection);
} else {
showNotification(result.error || 'Ошибка вставки документа', 'error');
}
} catch (error) {
showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
}
};
}
/**
* Показывает модальное окно обновления документа
* @param {string} docId - ID документа
* @param {Object} currentFields - Текущие поля документа
*/
function showUpdateDocumentModal(docId = '', currentFields = null) {
if (!currentDatabase || !currentCollection) {
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
return;
}
DOM.modalTitle.textContent = 'Обновить документ';
DOM.modalBody.innerHTML = `
База данных
Коллекция
ID документа
Обновления (JSON)
`;
DOM.modalConfirm.textContent = 'Обновить';
DOM.modal.classList.add('show');
DOM.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) {
DOM.modal.classList.remove('show');
showNotification('Документ обновлён', 'success');
viewCollection(currentDatabase, currentCollection);
} else {
showNotification(result.error || 'Ошибка обновления документа', 'error');
}
} catch (error) {
showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
}
};
}
// ======================== ТРАНЗАКЦИИ ========================
/**
* Загружает и отображает список активных транзакций
*/
async function loadTransactionList() {
DOM.title.textContent = 'Активные транзакции';
DOM.content.innerHTML = '';
try {
const response = await fetch('/api/webui/transactions');
const data = await response.json();
if (data.success) {
DOM.content.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('') || 'Нет активных транзакций '}
`;
} else {
DOM.content.innerHTML = 'Ошибка загрузки транзакций
';
}
} catch (error) {
DOM.content.innerHTML = 'Ошибка подключения
';
}
}
/**
* Загружает детали транзакции
* @param {string} txId - ID транзакции
*/
async function loadTransactionDetails(txId) {
DOM.modalTitle.textContent = `Детали транзакции ${txId}`;
DOM.modalConfirm.textContent = 'Закрыть';
DOM.modalBody.innerHTML = '';
DOM.modal.classList.add('show');
const originalConfirmHandler = DOM.modalConfirm.onclick;
DOM.modalConfirm.onclick = () => {
DOM.modal.classList.remove('show');
DOM.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;
DOM.modalBody.innerHTML = `ID: ${escapeHtml(tx.id)}
Статус: ${escapeHtml(tx.status)}
Время начала: ${new Date(tx.start_time).toLocaleString()}
Количество операций: ${tx.operation_count}
Операции
${tx.operations?.length ? `Тип БД Коллекция ID документа ${tx.operations.map(op => `${escapeHtml(op.type)} ${escapeHtml(op.database)} ${escapeHtml(op.collection)} ${escapeHtml(op.document_id)} `).join('')}
` : 'Нет операций
'}`;
} else {
DOM.modalBody.innerHTML = `Ошибка загрузки деталей
`;
}
} catch (error) {
DOM.modalBody.innerHTML = 'Ошибка подключения
';
}
}
/**
* Начинает новую сессию транзакций
*/
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();
if (data.success) {
showNotification('Сессия начата', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
/**
* Начинает новую транзакцию
*/
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();
if (data.success) {
showNotification('Транзакция начата', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
/**
* Фиксирует текущую транзакцию
*/
async function commitTransaction() {
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');
}
}
/**
* Отменяет текущую транзакцию
*/
async function abortTransaction() {
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');
}
}
/**
* Фиксирует транзакцию по 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();
if (data.success) {
showNotification(`Транзакция ${txId} зафиксирована`, 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка фиксации', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
/**
* Отменяет транзакцию по 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();
if (data.success) {
showNotification(`Транзакция ${txId} отменена`, 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка отмены', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
// ======================== ОСТАЛЬНЫЕ ФУНКЦИИ (ЗАГЛУШКИ ДЛЯ ПОЛНОТЫ) ========================
// ACL функции
async function loadACLUsers() { DOM.content.innerHTML = 'Раздел в разработке
'; }
async function loadACLRoles() { DOM.content.innerHTML = 'Раздел в разработке
'; }
async function loadACLPermissions() { DOM.content.innerHTML = 'Раздел в разработке
'; }
// Индексы
async function loadIndexesList() { DOM.content.innerHTML = 'Раздел в разработке
'; }
// Импорт/Экспорт
async function loadExportPage() { DOM.content.innerHTML = 'Раздел в разработке
'; }
async function loadImportPage() { DOM.content.innerHTML = 'Раздел в разработке
'; }
// Ограничения
async function loadConstraintsList() { DOM.content.innerHTML = 'Раздел в разработке
'; }
// Триггеры
async function loadTriggersList() { DOM.content.innerHTML = 'Раздел в разработке
'; }
async function loadTriggerLog() { DOM.content.innerHTML = 'Раздел в разработке
'; }
// Модальные окна для ACL и ограничений
function showCreateUserModal() { showNotification('Функция в разработке', 'info'); }
function showCreateRoleModal() { showNotification('Функция в разработке', 'info'); }
function showCreateIndexModal() { showNotification('Функция в разработке', 'info'); }
function showConstraintModal(type) { showNotification(`Добавление ограничения "${type}" в разработке`, 'info'); }