/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/api/static/app.js
// JavaScript для веб-интерфейса Futriis DB Dashboard
// Глобальное состояние
let currentSession = null;
let currentDatabase = null;
let currentCollection = null;
let currentUser = null;
// DOM элементы
const contentArea = document.getElementById('contentArea');
const pageTitle = document.getElementById('pageTitle');
const connectionStatus = document.getElementById('connectionStatus');
const userInfoSpan = document.querySelector('#userInfo span');
const logoutBtn = document.getElementById('logoutBtn');
const menuToggle = document.getElementById('menuToggle');
const sidebar = document.querySelector('.sidebar');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalConfirm = document.getElementById('modalConfirm');
const modalCloseBtns = document.querySelectorAll('.modal-close');
// Инициализация приложения
document.addEventListener('DOMContentLoaded', () => {
checkSession();
initNavigation();
initEventListeners();
});
// Проверка сессии
async function checkSession() {
try {
const response = await fetch('/api/webui/session');
const data = await response.json();
if (data.success && data.data.authenticated) {
currentUser = data.data.username;
userInfoSpan.textContent = currentUser;
if (data.data.connection_status === 'connected') {
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = 'СУБД подключена';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = 'СУБД не подключена';
}
loadDashboard();
} else {
showLoginModal();
}
} catch (error) {
console.error('Session check failed:', error);
showLoginModal();
}
}
// Функция для проверки статуса подключения
function startConnectionStatusMonitor() {
setInterval(async () => {
if (currentUser) {
try {
const response = await fetch('/api/webui/session');
const data = await response.json();
if (data.success && data.data.connection_status === 'connected') {
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = 'СУБД подключена';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = 'СУБД не подключена';
}
} catch (error) {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = 'СУБД не подключена';
}
}
}, 5000);
}
// Показать модальное окно входа
function showLoginModal() {
modalTitle.textContent = 'Вход в систему субд Futriis';
modalBody.innerHTML = `
`;
// Меняем текст на кнопке "Подтвердить" на "Войти"
modalConfirm.textContent = 'Войти';
modal.classList.add('show');
const confirmHandler = async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!username || !password) {
showNotification('Пожалуйста, заполните все поля', 'error');
return;
}
try {
const response = await fetch('/api/webui/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
currentUser = username;
userInfoSpan.textContent = username;
modal.classList.remove('show');
showNotification('Вход выполнен успешно', 'success');
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = 'СУБД подключена';
startConnectionStatusMonitor();
loadDashboard();
} else {
showNotification(data.error || 'Неверный логин и/или пароль', 'error');
}
} catch (error) {
showNotification('Ошибка подключения к серверу', 'error');
}
};
modalConfirm.onclick = confirmHandler;
// Обработка Enter
const handleEnter = (e) => {
if (e.key === 'Enter') {
confirmHandler();
document.removeEventListener('keydown', handleEnter);
}
};
document.addEventListener('keydown', handleEnter);
}
// Инициализация навигации
function initNavigation() {
document.querySelectorAll('.nav-link[data-section]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const section = link.dataset.section;
loadSection(section);
setActiveNav(link);
});
});
document.querySelectorAll('[data-action]').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const action = item.dataset.action;
handleCrudAction(action);
});
});
document.querySelectorAll('.has-submenu > .nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const parent = link.closest('.has-submenu');
parent.classList.toggle('open');
});
});
}
// Инициализация обработчиков событий
function initEventListeners() {
logoutBtn.addEventListener('click', async () => {
await fetch('/api/webui/logout', { method: 'POST' });
currentSession = null;
currentUser = null;
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = 'СУБД не подключена';
showLoginModal();
});
if (menuToggle) {
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
}
modalCloseBtns.forEach(btn => {
btn.addEventListener('click', () => {
modal.classList.remove('show');
});
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
}
});
}
// Загрузка секции
async function loadSection(section) {
switch(section) {
case 'dashboard':
loadDashboard();
break;
case 'cluster':
loadClusterManagement();
break;
case 'audit':
loadAuditLog();
break;
case 'settings':
loadSettings();
break;
case 'acl-users':
loadACLUsers();
break;
case 'acl-roles':
loadACLRoles();
break;
case 'acl-permissions':
loadACLPermissions();
break;
case 'tx-list':
loadTransactionList();
break;
case 'indexes-list':
loadIndexesList();
break;
case 'export-data':
loadExportPage();
break;
case 'import-data':
loadImportPage();
break;
default:
loadDashboard();
}
}
// Загрузка дашборда
async function loadDashboard() {
pageTitle.textContent = 'Панель управления';
contentArea.innerHTML = '';
try {
const [statsRes, dbsRes] = await Promise.all([
fetch('/api/webui/stats'),
fetch('/api/webui/databases')
]);
const stats = await statsRes.json();
const databases = await dbsRes.json();
contentArea.innerHTML = `
${stats.data.databases || 0}
Базы данных
${stats.data.collections || 0}
Коллекции
${stats.data.documents || 0}
Документы
${stats.data.storage_used_mb?.toFixed(2) || 0} MB
Использовано памяти
Базы данных
| Имя БД | Коллекции | Действия |
${databases.data.map(db => `
| ${escapeHtml(db.name)} |
${db.collections} |
|
`).join('')}
`;
} catch (error) {
contentArea.innerHTML = 'Ошибка загрузки данных
';
showNotification('Ошибка загрузки дашборда', 'error');
}
}
// Просмотр базы данных
window.viewDatabase = async function(dbName) {
currentDatabase = dbName;
pageTitle.textContent = `База данных: ${dbName}`;
contentArea.innerHTML = '';
try {
const response = await fetch(`/api/webui/collections/${dbName}`);
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
Коллекции
| Имя коллекции | Документов | Размер | Индексы | Действия |
${data.data.collections.map(coll => `
| ${escapeHtml(coll.name)} |
${coll.count} |
${(coll.size / 1024).toFixed(2)} KB |
${coll.indexes.length} |
|
`).join('')}
`;
} else {
contentArea.innerHTML = 'Ошибка загрузки коллекций
';
}
} catch (error) {
contentArea.innerHTML = 'Ошибка подключения
';
}
};
// Просмотр коллекции
window.viewCollection = async function(dbName, collName) {
currentDatabase = dbName;
currentCollection = collName;
pageTitle.textContent = `Коллекция: ${dbName}.${collName}`;
contentArea.innerHTML = '';
try {
const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`);
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
Документы (${data.data.total} всего)
| 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 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 = 'Ошибка загрузки информации о кластере
';
}
}
// Загрузка лога аудита
async function loadAuditLog() {
pageTitle.textContent = 'Лог аудита';
contentArea.innerHTML = '';
contentArea.innerHTML = 'Функция в разработке
';
}
// Загрузка настроек
function loadSettings() {
pageTitle.textContent = 'Настройки';
contentArea.innerHTML = `
Настройки интерфейса
`;
}
// ==================== ACL Functions ====================
// Загрузка списка пользователей
async function loadACLUsers() {
pageTitle.textContent = 'Управление пользователями ACL';
contentArea.innerHTML = 'Загрузка пользователей...
';
try {
const response = await fetch('/api/webui/acl/users');
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
Пользователи
| Имя пользователя | Роли | Статус | Создан | Последний вход | Действия |
${data.data.map(user => `
| ${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 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 function loadACLPermissions() {
pageTitle.textContent = 'Управление разрешениями ACL';
contentArea.innerHTML = '';
try {
const response = await fetch('/api/webui/acl/permissions');
const data = await response.json();
if (data.success) {
let html = 'Разрешения по ролям
';
for (const [roleName, permissions] of Object.entries(data.data)) {
html += `
Роль: ${escapeHtml(roleName)}
${permissions.map(p => `${escapeHtml(p)}`).join('') || 'Нет разрешений'}
`;
}
html += '
';
contentArea.innerHTML = html;
} else {
contentArea.innerHTML = 'Ошибка загрузки разрешений
';
}
} catch (error) {
contentArea.innerHTML = 'Ошибка подключения
';
}
}
// Показать модальное окно создания пользователя
function showCreateUserModal() {
modalTitle.textContent = 'Создать пользователя';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const rolesStr = document.getElementById('roles').value;
const roles = rolesStr ? rolesStr.split(',').map(r => r.trim()) : [];
if (!username || !password) {
showNotification('Заполните имя пользователя и пароль', 'error');
return;
}
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password, roles })
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Пользователь ${username} создан`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка создания пользователя', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно редактирования пользователя
function showEditUserModal(username, currentRoles) {
modalTitle.textContent = `Редактировать пользователя: ${username}`;
modalConfirm.textContent = 'Сохранить';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const newPassword = document.getElementById('newPassword').value;
const addRole = document.getElementById('addRole').value;
const removeRole = document.getElementById('removeRole').value;
const updates = {};
if (newPassword) updates.password = newPassword;
if (addRole) updates.add_role = addRole;
if (removeRole) updates.remove_role = removeRole;
if (Object.keys(updates).length === 0) {
showNotification('Нет изменений для сохранения', 'warning');
return;
}
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Пользователь ${username} обновлён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка обновления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно создания роли
function showCreateRoleModal() {
modalTitle.textContent = 'Создать роль';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const roleName = document.getElementById('roleName').value;
if (!roleName) {
showNotification('Введите название роли', 'error');
return;
}
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Роль ${roleName} создана`, 'success');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка создания роли', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно редактирования роли
function showEditRoleModal(roleName, currentPermissions) {
modalTitle.textContent = `Редактировать роль: ${roleName}`;
modalConfirm.textContent = 'Добавить разрешение';
modalBody.innerHTML = `
Формат: database.collection:read|write|delete|admin (можно использовать * как wildcard)
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const permission = document.getElementById('newPermission').value;
if (!permission) {
showNotification('Введите разрешение', 'error');
return;
}
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/grant/${encodeURIComponent(permission)}`, {
method: 'PUT'
});
const data = await response.json();
if (data.success) {
showNotification(`Разрешение ${permission} добавлено`, 'success');
modal.classList.remove('show');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка добавления разрешения', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// ==================== Transaction Functions ====================
// Загрузка списка транзакций
async function loadTransactionList() {
pageTitle.textContent = 'Активные транзакции';
contentArea.innerHTML = '';
try {
const response = await fetch('/api/webui/transactions');
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
Активные транзакции
| ID транзакции | Статус | Начало | Операций |
${data.data.map(tx => `
${escapeHtml(tx.id)} |
${escapeHtml(tx.status)} |
${new Date(tx.start_time).toLocaleString()} |
${tx.operation_count || 0} |
`).join('') || '| Нет активных транзакций |
'}
`;
} else {
contentArea.innerHTML = 'Ошибка загрузки транзакций
';
}
} catch (error) {
contentArea.innerHTML = 'Ошибка подключения
';
}
}
// ==================== Index Functions ====================
// Загрузка списка индексов
async function loadIndexesList() {
pageTitle.textContent = 'Управление индексами';
contentArea.innerHTML = `
Выберите базу данных и коллекцию
`;
// Загружаем список БД
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('indexDbSelect');
dbSelect.innerHTML = '' +
data.data.map(db => ``).join('');
}
} catch (error) {
showNotification('Ошибка загрузки БД', 'error');
}
}
// Загрузка коллекций для выбранной БД
window.loadCollectionsForIndex = async function() {
const dbName = document.getElementById('indexDbSelect').value;
const collSelect = document.getElementById('indexCollSelect');
if (!dbName) {
collSelect.innerHTML = '';
document.getElementById('indexesContent').innerHTML = 'Выберите базу данных и коллекцию
';
return;
}
collSelect.innerHTML = '';
try {
const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
const data = await response.json();
if (data.success && data.data.collections) {
collSelect.innerHTML = '' +
data.data.collections.map(coll => ``).join('');
} else {
collSelect.innerHTML = '';
}
} catch (error) {
collSelect.innerHTML = '';
}
};
// Загрузка индексов для выбранной коллекции
async function loadIndexesForCollection() {
const dbName = document.getElementById('indexDbSelect').value;
const collName = document.getElementById('indexCollSelect').value;
if (!dbName || !collName) {
document.getElementById('indexesContent').innerHTML = 'Выберите базу данных и коллекцию
';
return;
}
document.getElementById('indexesContent').innerHTML = '';
try {
const response = await fetch(`/api/webui/indexes/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`);
const data = await response.json();
if (data.success) {
document.getElementById('indexesContent').innerHTML = `
Индексы коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}
| Имя индекса | Поля | Уникальный | Действия |
${data.data.map(idx => `
${escapeHtml(idx.name)} |
${idx.fields.join(', ')} |
${idx.unique ? 'Да' : 'Нет'} |
|
`).join('') || '| Нет индексов |
'}
`;
} else {
document.getElementById('indexesContent').innerHTML = 'Ошибка загрузки индексов
';
}
} catch (error) {
document.getElementById('indexesContent').innerHTML = 'Ошибка подключения
';
}
}
// ==================== Import/Export Functions ====================
// Загрузка страницы экспорта
async function loadExportPage() {
pageTitle.textContent = 'Экспорт данных';
contentArea.innerHTML = `
`;
// Загружаем список БД
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('exportDbSelect');
dbSelect.innerHTML = '' +
data.data.map(db => ``).join('');
}
} catch (error) {
showNotification('Ошибка загрузки БД', 'error');
}
}
// Выполнение экспорта
async function performExport() {
const dbName = document.getElementById('exportDbSelect').value;
const filename = document.getElementById('exportFilename').value;
if (!dbName) {
showNotification('Выберите базу данных', 'error');
return;
}
const exportResult = document.getElementById('exportResult');
exportResult.innerHTML = '';
try {
const response = await fetch('/api/webui/export', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ database: dbName, filename })
});
const data = await response.json();
if (data.success) {
// Создаём JSON файл для скачивания
const exportData = data.data.data;
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = data.data.filename.replace('.msgpack', '.json');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
exportResult.innerHTML = `
Экспорт завершён
БД: ${escapeHtml(dbName)}
Коллекций: ${data.data.collections}
Файл: ${data.data.filename}
`;
showNotification('Экспорт завершён', 'success');
} else {
exportResult.innerHTML = `Ошибка: ${data.error}
`;
}
} catch (error) {
exportResult.innerHTML = 'Ошибка подключения
';
}
}
// Загрузка страницы импорта
async function loadImportPage() {
pageTitle.textContent = 'Импорт данных';
contentArea.innerHTML = `
`;
}
// Выполнение импорта
async function performImport() {
const dbName = document.getElementById('importDbName').value;
const fileInput = document.getElementById('importFile');
const overwrite = document.getElementById('importOverwrite').checked;
if (!dbName) {
showNotification('Введите имя целевой базы данных', 'error');
return;
}
if (!fileInput.files || fileInput.files.length === 0) {
showNotification('Выберите файл для импорта', 'error');
return;
}
const importResult = document.getElementById('importResult');
importResult.innerHTML = '';
try {
const file = fileInput.files[0];
const fileContent = await file.text();
const importData = JSON.parse(fileContent);
const response = await fetch('/api/webui/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ database: dbName, data: importData, overwrite })
});
const data = await response.json();
if (data.success) {
importResult.innerHTML = `
Импорт завершён
БД: ${escapeHtml(dbName)}
Импортировано коллекций: ${data.data.collections}
Импортировано документов: ${data.data.documents}
`;
showNotification('Импорт завершён', 'success');
} else {
importResult.innerHTML = `Ошибка: ${data.error}
`;
}
} catch (error) {
importResult.innerHTML = `Ошибка: ${error.message}
`;
}
}
// ==================== CRUD Action Handlers ====================
// Обработка CRUD действий
function handleCrudAction(action) {
switch(action) {
case 'create-db':
showCreateDatabaseModal();
break;
case 'create-collection':
showCreateCollectionModal();
break;
case 'insert-doc':
showInsertDocumentModal();
break;
case 'find-doc':
showFindDocumentModal();
break;
case 'update-doc':
showUpdateDocumentModal();
break;
case 'delete-doc':
showDeleteDocumentModal();
break;
case 'acl-create-user':
showCreateUserModal();
break;
case 'acl-create-role':
showCreateRoleModal();
break;
case 'tx-start-session':
startSession();
break;
case 'tx-start':
startTransaction();
break;
case 'tx-commit':
commitTransaction();
break;
case 'tx-abort':
abortTransaction();
break;
case 'index-create':
showCreateIndexModal();
break;
}
}
// Показать модальное окно создания БД
function showCreateDatabaseModal() {
modalTitle.textContent = 'Создать базу данных';
modalConfirm.textContent = 'Подтвердить';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const dbName = document.getElementById('dbName').value;
if (!dbName) {
showNotification('Введите имя базы данных', 'error');
return;
}
try {
const response = await fetch('/api/db/' + dbName, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (response.ok) {
modal.classList.remove('show');
showNotification(`База данных "${dbName}" создана`, 'success');
loadDashboard();
} else {
const error = await response.json();
showNotification(error.error || 'Ошибка создания БД', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно создания коллекции
function showCreateCollectionModal() {
if (!currentDatabase) {
showNotification('Сначала выберите базу данных', 'warning');
return;
}
modalTitle.textContent = 'Создать коллекцию';
modalConfirm.textContent = 'Подтвердить';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const collName = document.getElementById('collName').value;
if (!collName) {
showNotification('Введите имя коллекции', 'error');
return;
}
try {
const response = await fetch(`/api/db/${currentDatabase}/${collName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (response.ok) {
modal.classList.remove('show');
showNotification(`Коллекция "${collName}" создана`, 'success');
viewDatabase(currentDatabase);
} else {
const error = await response.json();
showNotification(error.error || 'Ошибка создания коллекции', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно вставки документа
function showInsertDocumentModal() {
if (!currentDatabase || !currentCollection) {
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
return;
}
modalTitle.textContent = 'Вставить документ';
modalConfirm.textContent = 'Подтвердить';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const docData = document.getElementById('docData').value;
if (!docData) {
showNotification('Введите данные документа', 'error');
return;
}
try {
const data = JSON.parse(docData);
const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
modal.classList.remove('show');
showNotification('Документ вставлен', 'success');
viewCollection(currentDatabase, currentCollection);
} else {
showNotification(result.error || 'Ошибка вставки документа', 'error');
}
} catch (error) {
if (error instanceof SyntaxError) {
showNotification('Неверный формат JSON', 'error');
} else {
showNotification('Ошибка подключения', 'error');
}
}
};
}
// Показать модальное окно поиска документа
function showFindDocumentModal() {
if (!currentDatabase || !currentCollection) {
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
return;
}
modalTitle.textContent = 'Найти документ';
modalConfirm.textContent = 'Найти';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const docId = document.getElementById('docId').value;
if (!docId) {
showNotification('Введите ID документа', 'error');
return;
}
try {
const response = await fetch(`/api/db/${currentDatabase}/${currentCollection}/${docId}`);
if (response.ok) {
const data = await response.json();
modal.classList.remove('show');
contentArea.innerHTML = `
Результат поиска
${escapeHtml(JSON.stringify(data.data, null, 2))}
`;
} else {
const error = await response.json();
showNotification(error.error || 'Документ не найден', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно обновления документа
function showUpdateDocumentModal(docId, currentFields = null) {
if (!currentDatabase || !currentCollection) {
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
return;
}
const fieldsJson = currentFields ? JSON.stringify(currentFields, null, 2) : '';
modalTitle.textContent = 'Обновить документ';
modalConfirm.textContent = 'Обновить';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const updateDocId = document.getElementById('updateDocId').value;
const updateData = document.getElementById('updateData').value;
if (!updateDocId) {
showNotification('Введите ID документа', 'error');
return;
}
if (!updateData) {
showNotification('Введите данные для обновления', 'error');
return;
}
try {
const data = JSON.parse(updateData);
const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(updateDocId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
modal.classList.remove('show');
showNotification('Документ обновлён', 'success');
viewCollection(currentDatabase, currentCollection);
} else {
showNotification(result.error || 'Ошибка обновления документа', 'error');
}
} catch (error) {
if (error instanceof SyntaxError) {
showNotification('Неверный формат JSON', 'error');
} else {
showNotification('Ошибка подключения', 'error');
}
}
};
}
// Показать модальное окно удаления документа
function showDeleteDocumentModal() {
if (!currentDatabase || !currentCollection) {
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
return;
}
modalTitle.textContent = 'Удалить документ';
modalConfirm.textContent = 'Удалить';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const docId = document.getElementById('deleteDocId').value;
if (!docId) {
showNotification('Введите ID документа', 'error');
return;
}
try {
const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(docId)}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
modal.classList.remove('show');
showNotification('Документ удалён', 'success');
viewCollection(currentDatabase, currentCollection);
} else {
showNotification(result.error || 'Ошибка удаления документа', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно создания индекса
function showCreateIndexModal() {
const dbName = document.getElementById('indexDbSelect')?.value;
const collName = document.getElementById('indexCollSelect')?.value;
if (!dbName || !collName) {
showNotification('Сначала выберите базу данных и коллекцию на странице "Список индексов"', 'warning');
return;
}
modalTitle.textContent = 'Создать индекс';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const indexName = document.getElementById('indexName').value;
const fieldsStr = document.getElementById('indexFields').value;
const unique = document.getElementById('indexUnique').checked;
if (!indexName || !fieldsStr) {
showNotification('Заполните имя индекса и поля', 'error');
return;
}
const fields = fieldsStr.split(',').map(f => f.trim());
try {
const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: indexName, fields, unique })
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Индекс ${indexName} создан`, 'success');
loadIndexesForCollection();
} else {
showNotification(data.error || 'Ошибка создания индекса', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// ==================== Вспомогательные функции ====================
// Удаление коллекции
window.deleteCollection = async function(dbName, collName) {
if (confirm(`Вы уверены, что хотите удалить коллекцию "${collName}"? Это действие необратимо.`)) {
try {
const response = await fetch(`/api/db/${dbName}/${collName}`, {
method: 'DELETE'
});
if (response.ok) {
showNotification(`Коллекция "${collName}" удалена`, 'success');
viewDatabase(dbName);
} else {
const error = await response.json();
showNotification(error.error || 'Ошибка удаления коллекции', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Удаление документа
window.deleteDocument = async function(dbName, collName, docId) {
if (confirm(`Вы уверены, что хотите удалить документ "${docId}"?`)) {
try {
const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showNotification('Документ удалён', 'success');
viewCollection(dbName, collName);
} else {
showNotification(result.error || 'Ошибка удаления документа', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Удаление пользователя
window.deleteUser = async function(username) {
if (confirm(`Удалить пользователя "${username}"?`)) {
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Пользователь ${username} удалён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Удаление роли
window.deleteRole = async function(roleName) {
if (confirm(`Удалить роль "${roleName}"?`)) {
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Роль ${roleName} удалена`, 'success');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Отключение пользователя
window.disableUser = async function(username) {
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ disable: true })
});
const data = await response.json();
if (data.success) {
showNotification(`Пользователь ${username} отключён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Включение пользователя
window.enableUser = async function(username) {
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enable: true })
});
const data = await response.json();
if (data.success) {
showNotification(`Пользователь ${username} включён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Отзыв разрешения у роли
window.revokePermission = async function(roleName, permission) {
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/revoke/${encodeURIComponent(permission)}`, {
method: 'PUT'
});
const data = await response.json();
if (data.success) {
showNotification(`Разрешение отозвано`, 'success');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Удаление индекса
window.dropIndex = async function(dbName, collName, indexName) {
if (confirm(`Удалить индекс "${indexName}"?`)) {
try {
const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/drop/${encodeURIComponent(indexName)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification(`Индекс ${indexName} удалён`, 'success');
loadIndexesForCollection();
} else {
showNotification(data.error || 'Ошибка удаления индекса', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Функции для транзакций
window.startSession = async function() {
try {
const response = await fetch('/api/webui/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'start_session' })
});
const data = await response.json();
if (data.success) {
showNotification('Сессия начата', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
window.startTransaction = async function() {
try {
const response = await fetch('/api/webui/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'start_transaction' })
});
const data = await response.json();
if (data.success) {
showNotification('Транзакция начата', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
window.commitTransaction = async function() {
try {
const response = await fetch('/api/webui/transaction/commit', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Транзакция зафиксирована', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
window.abortTransaction = async function() {
try {
const response = await fetch('/api/webui/transaction/abort', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Транзакция отменена', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Сохранение настроек
function saveSettings() {
const theme = document.getElementById('themeSelect')?.value;
if (theme) {
localStorage.setItem('theme', theme);
showNotification('Настройки сохранены', 'success');
}
}
// Утилиты
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
let icon = '';
switch(type) {
case 'success': icon = ''; break;
case 'error': icon = ''; break;
case 'warning': icon = ''; break;
default: icon = '';
}
notification.innerHTML = `${icon}${escapeHtml(message)}`;
container.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
function setActiveNav(activeLink) {
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
activeLink.classList.add('active');
}
// ==================== Trigger Functions ====================
// Загрузка списка триггеров
async function loadTriggersList() {
pageTitle.textContent = 'Управление триггерами';
contentArea.innerHTML = `
Выберите базу данных и коллекцию
`;
// Загружаем список БД
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('triggerDbSelect');
dbSelect.innerHTML = '' +
data.data.map(db => ``).join('');
}
} catch (error) {
showNotification('Ошибка загрузки БД', 'error');
}
}
// Загрузка коллекций для выбранной БД (для триггеров)
window.loadCollectionsForTrigger = async function() {
const dbName = document.getElementById('triggerDbSelect').value;
const collSelect = document.getElementById('triggerCollSelect');
if (!dbName) {
collSelect.innerHTML = '';
document.getElementById('triggersContent').innerHTML = 'Выберите базу данных и коллекцию
';
return;
}
collSelect.innerHTML = '';
try {
const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
const data = await response.json();
if (data.success && data.data.collections) {
collSelect.innerHTML = '' +
data.data.collections.map(coll => ``).join('');
} else {
collSelect.innerHTML = '';
}
} catch (error) {
collSelect.innerHTML = '';
}
};
// Загрузка триггеров для выбранной коллекции
async function loadTriggersForCollection() {
const dbName = document.getElementById('triggerDbSelect').value;
const collName = document.getElementById('triggerCollSelect').value;
if (!dbName || !collName) {
document.getElementById('triggersContent').innerHTML = 'Выберите базу данных и коллекцию
';
return;
}
document.getElementById('triggersContent').innerHTML = '';
try {
const response = await fetch(`/api/webui/triggers/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`);
const data = await response.json();
if (data.success) {
if (data.data.length === 0) {
document.getElementById('triggersContent').innerHTML = 'Нет триггеров для этой коллекции
';
return;
}
document.getElementById('triggersContent').innerHTML = `
Триггеры коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}
| Имя | Событие | Действие | Статус | Описание | Действия |
${data.data.map(trigger => `
${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 = 'Ошибка подключения
';
}
}
// Показать модальное окно создания триггера
function showCreateTriggerModal() {
const dbName = document.getElementById('triggerDbSelect').value;
const collName = document.getElementById('triggerCollSelect').value;
if (!dbName || !collName) {
showNotification('Сначала выберите базу данных и коллекцию на странице "Список триггеров"', 'warning');
return;
}
modalTitle.textContent = 'Создать триггер';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
Доступные операции: set, unset, inc, mul, rename, currentDate
Специальные значения: $$NOW (текущее время), $$USER (текущий пользователь), $$ROLE (текущая роль)
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const triggerName = document.getElementById('triggerName').value;
const triggerEvent = document.getElementById('triggerEvent').value;
const triggerAction = document.getElementById('triggerAction').value;
const triggerDescription = document.getElementById('triggerDescription').value;
if (!triggerName) {
showNotification('Введите имя триггера', 'error');
return;
}
// Собираем условие
let condition = null;
const conditionField = document.getElementById('conditionField').value;
if (conditionField) {
condition = {
field: conditionField,
operator: document.getElementById('conditionOperator').value,
value: document.getElementById('conditionValue').value
};
// Преобразуем числовые значения
if (condition.value && !isNaN(condition.value) && condition.value.trim() !== '') {
condition.value = parseFloat(condition.value);
}
}
// Парсим операции
let operations = [];
const opsText = document.getElementById('triggerOperations').value;
if (opsText && opsText.trim()) {
try {
operations = JSON.parse(opsText);
} catch (e) {
showNotification('Неверный формат JSON для операций', 'error');
return;
}
}
const requestBody = {
name: triggerName,
event: triggerEvent,
action: triggerAction,
description: triggerDescription,
operations: operations
};
if (condition) {
requestBody.condition = condition;
}
try {
const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Триггер ${triggerName} создан`, 'success');
loadTriggersForCollection();
} else {
showNotification(data.error || 'Ошибка создания триггера', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Включение/отключение триггера
window.toggleTrigger = async function(dbName, collName, triggerName, triggerEvent, enable) {
const action = enable ? 'enable' : 'disable';
try {
const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${action}/${encodeURIComponent(triggerName)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification(`Триггер ${triggerName} ${enable ? 'включён' : 'отключён'}`, 'success');
loadTriggersForCollection();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Удаление триггера
window.deleteTrigger = async function(dbName, collName, triggerName, triggerEvent) {
if (confirm(`Удалить триггер "${triggerName}"?`)) {
try {
const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/delete/${encodeURIComponent(triggerName)}/${encodeURIComponent(triggerEvent)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Триггер ${triggerName} удалён`, 'success');
loadTriggersForCollection();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Загрузка лога выполнения триггеров
async function loadTriggerLog() {
pageTitle.textContent = 'Лог выполнения триггеров';
contentArea.innerHTML = '';
try {
const response = await fetch('/api/webui/trigger/log');
const data = await response.json();
if (data.success) {
if (data.data.length === 0) {
contentArea.innerHTML = 'Лог выполнения триггеров пуст
';
return;
}
contentArea.innerHTML = `
Лог выполнения триггеров
| Триггер | Событие | Коллекция | БД | Документ | Время | Пользователь |
${data.data.map(entry => `
${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 = 'Ошибка подключения
';
}
}
// Обновляем loadSection для секции триггеров
const originalLoadSectionTriggers = window.loadSection;
window.loadSection = function(section) {
switch(section) {
case 'triggers-list':
loadTriggersList();
break;
case 'trigger-log':
loadTriggerLog();
break;
default:
if (originalLoadSectionTriggers) originalLoadSectionTriggers(section);
}
};