Files
futriix/internal/api/static/app.js

1922 lines
108 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 = '<span>СУБД подключена</span>';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
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 = '<span>СУБД подключена</span>';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
} catch (error) {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
}
}, 5000);
}
/**
* @description Отображает модальное окно для входа в систему.
* Обрабатывает отправку учётных данных и сохраняет сессию.
*/
function showLoginModal() {
modalTitle.textContent = 'Вход в систему СУБД Futriis';
modalBody.innerHTML = `
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" id="username" class="form-control" placeholder="Введите имя пользователя">
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" id="password" class="form-control" placeholder="Введите пароль">
</div>
`;
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 = '<span>СУБД подключена</span>';
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 = `<img src="${avatarBase64}" alt="Avatar" style="width:40px;height:40px;border-radius:50%;object-fit:cover;">`;
}
/**
* @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 = `<img src="${e.target.result}" style="max-width:150px;max-height:150px;border-radius:50%;">`;
}
};
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 = '<span>СУБД не подключена</span>';
// Сбрасываем аватар
if (userAvatar) {
userAvatar.innerHTML = '<i class="fas fa-user-circle" style="font-size: 40px;"></i>';
}
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 = '<div class="info-message">Функция в разработке</div>'; },
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 = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка данных...</p></div>';
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 = `
<div class="dashboard-stats">
<div class="stat-card"><div class="stat-icon"><i class="fas fa-database"></i></div><div class="stat-info"><h3>${stats.data.databases || 0}</h3><p>Базы данных</p></div></div>
<div class="stat-card"><div class="stat-icon"><i class="fas fa-table"></i></div><div class="stat-info"><h3>${stats.data.collections || 0}</h3><p>Коллекции</p></div></div>
<div class="stat-card"><div class="stat-icon"><i class="fas fa-file-alt"></i></div><div class="stat-info"><h3>${stats.data.documents || 0}</h3><p>Документы</p></div></div>
<div class="stat-card"><div class="stat-icon"><i class="fas fa-hdd"></i></div><div class="stat-info"><h3>${stats.data.storage_used_mb?.toFixed(2) || 0} MB</h3><p>Использовано памяти</p></div></div>
</div>
<div class="data-table"><h3>Базы данных</h3><table><thead><tr><th>Имя БД</th><th>Коллекции</th><th>Действия</th></tr></thead><tbody>
${databases.data.map(db => `<tr><td><strong>${escapeHtml(db.name)}</strong></td><td>${db.collections}</td><td><button class="btn btn-sm btn-primary" onclick="viewDatabase('${escapeHtml(db.name)}')"><i class="fas fa-eye"></i> Просмотр</button></td></tr>`).join('')}
</tbody></table></div>
`;
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки данных</div>';
showNotification('Ошибка загрузки дашборда', 'error');
}
}
// ============================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ==============================
/**
* @async
* @description Отображает список коллекций в выбранной базе данных.
* @param {string} dbName - Имя базы данных
*/
window.viewDatabase = async function(dbName) {
currentDatabase = dbName;
pageTitle.textContent = `База данных: ${dbName}`;
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка коллекций...</p></div>';
try {
const response = await fetch(`/api/webui/collections/${dbName}`);
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
<div class="data-table"><div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;"><h3>Коллекции</h3><button class="btn btn-primary btn-sm" onclick="showCreateCollectionModal()"><i class="fas fa-plus"></i> Создать коллекцию</button></div>
<table><thead><tr><th>Имя коллекции</th><th>Документов</th><th>Размер</th><th>Индексы</th><th>Действия</th></tr></thead><tbody>
${data.data.collections.map(coll => `
<tr>
<td><strong>${escapeHtml(coll.name)}</strong></td>
<td>${coll.count}</td>
<td>${(coll.size / 1024).toFixed(2)} KB</td>
<td>${coll.indexes.length}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="viewCollection('${escapeHtml(dbName)}', '${escapeHtml(coll.name)}')"><i class="fas fa-eye"></i></button>
<button class="btn btn-sm btn-danger" onclick="deleteCollection('${escapeHtml(dbName)}', '${escapeHtml(coll.name)}')"><i class="fas fa-trash"></i></button>
</td>
</tr>
`).join('')}
</tbody></table></div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки коллекций</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
};
/**
* @async
* @description Отображает документы выбранной коллекции с пагинацией и действиями.
* @param {string} dbName - Имя базы данных
* @param {string} collName - Имя коллекции
*/
window.viewCollection = async function(dbName, collName) {
currentDatabase = dbName;
currentCollection = collName;
pageTitle.textContent = `Коллекция: ${dbName}.${collName}`;
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка документов...</p></div>';
try {
const response = await fetch(`/api/webui/documents/${dbName}/${collName}?limit=100`);
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
<div style="margin-bottom: 16px; display: flex; gap: 12px;"><button class="btn btn-primary" onclick="showInsertDocumentModal()"><i class="fas fa-plus"></i> Вставить документ</button><button class="btn btn-secondary" onclick="viewDatabase('${escapeHtml(dbName)}')"><i class="fas fa-arrow-left"></i> Назад</button></div>
<div class="data-table"><h3>Документы (${data.data.total} всего)</h3><table><thead><tr><th>ID</th><th>Поля</th><th>Создан</th><th>Действия</th></tr></thead><tbody>
${data.data.documents.map(doc => `
<tr>
<td><code>${escapeHtml(doc.id)}</code></td>
<td><pre style="max-width:400px; overflow-x:auto;">${escapeHtml(JSON.stringify(doc.fields, null, 2))}</pre></td>
<td>${new Date(doc.created_at).toLocaleString()}</td>
<td>
<button class="btn btn-sm btn-secondary" onclick="showUpdateDocumentModal('${escapeHtml(doc.id)}', ${escapeHtml(JSON.stringify(doc.fields))})"><i class="fas fa-edit"></i></button>
<button class="btn btn-sm btn-danger" onclick="deleteDocument('${escapeHtml(dbName)}', '${escapeHtml(collName)}', '${escapeHtml(doc.id)}')"><i class="fas fa-trash"></i></button>
</td>
</tr>
`).join('')}
</tbody></table></div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки документов</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
};
// ============================== КЛАСТЕР ==============================
/**
* @async
* @description Загружает и отображает статус кластера и список узлов.
*/
async function loadClusterManagement() {
pageTitle.textContent = 'Управление кластером';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка информации о кластере...</p></div>';
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 = `
<div class="dashboard-stats">
<div class="stat-card"><div class="stat-icon"><i class="fas fa-heartbeat"></i></div><div class="stat-info"><h3 style="color: ${status.data.health === 'healthy' ? '#28a745' : status.data.health === 'degraded' ? '#ffc107' : '#dc3545'}">${status.data.health === 'healthy' ? 'Здоров' : status.data.health === 'degraded' ? 'Деградирован' : 'Критический'}</h3><p>Состояние кластера</p></div></div>
<div class="stat-card"><div class="stat-icon"><i class="fas fa-server"></i></div><div class="stat-info"><h3>${status.data.active_nodes}/${status.data.total_nodes}</h3><p>Активные узлы</p></div></div>
<div class="stat-card"><div class="stat-icon"><i class="fas fa-copy"></i></div><div class="stat-info"><h3>${status.data.replication_factor}</h3><p>Фактор репликации</p></div></div>
</div>
<div class="data-table"><h3>Узлы кластера</h3><table><thead><tr><th>ID узла</th><th>Адрес</th><th>Статус</th><th>Последний контакт</th></tr></thead><tbody>
${nodes.data.map(node => `<tr><td><code>${escapeHtml(node.id)}</code></td><td>${escapeHtml(node.ip)}:${node.port}</td><td><span class="status-badge status-${node.status}">${node.status}</span></td><td>${new Date(node.last_seen * 1000).toLocaleString()}</td></tr>`).join('')}
</tbody></table></div>
`;
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки информации о кластере</div>';
}
}
// ============================== НАСТРОЙКИ ==============================
/**
* @description Отображает страницу настроек интерфейса.
*/
function loadSettings() {
pageTitle.textContent = 'Настройки';
contentArea.innerHTML = `
<div class="settings-panel"><h3>Настройки интерфейса</h3>
<div class="form-group"><label>Тема оформления</label><select class="form-control" id="themeSelect"><option value="dark">Тёмная</option><option value="light">Светлая</option></select></div>
<button class="btn btn-primary" onclick="saveSettings()">Сохранить настройки</button></div>
`;
}
// ============================== ACL (УПРАВЛЕНИЕ ДОСТУПОМ) ==============================
/**
* @async
* @description Загружает и отображает список пользователей системы.
*/
async function loadACLUsers() {
pageTitle.textContent = 'Управление пользователями ACL';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка пользователей...</p></div>';
try {
const response = await fetch('/api/webui/acl/users');
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
<div style="margin-bottom:16px;"><button class="btn btn-primary" onclick="showCreateUserModal()"><i class="fas fa-user-plus"></i> Создать пользователя</button></div>
<div class="data-table"><h3>Пользователи</h3><table><thead><tr><th>Имя</th><th>Роли</th><th>Статус</th><th>Создан</th><th>Последний вход</th><th>Действия</th></tr></thead><tbody>
${data.data.map(user => `
<tr>
<td><strong>${escapeHtml(user.username)}</strong></td>
<td>${user.roles.map(r => `<span class="badge">${escapeHtml(r)}</span>`).join(' ') || '-'}</td>
<td>${user.active ? '<span class="status-badge status-active">Активен</span>' : '<span class="status-badge status-inactive">Отключён</span>'}</td>
<td>${new Date(user.created_at).toLocaleString()}</td>
<td>${user.last_login ? new Date(user.last_login).toLocaleString() : '-'}</td>
<td><button class="btn btn-sm btn-secondary" onclick="showEditUserModal('${escapeHtml(user.username)}', ${JSON.stringify(user.roles)})"><i class="fas fa-edit"></i></button><button class="btn btn-sm btn-danger" onclick="deleteUser('${escapeHtml(user.username)}')"><i class="fas fa-trash"></i></button></td>
</tr>
`).join('')}
</tbody></table></div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки пользователей</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
/**
* @async
* @description Загружает и отображает список ролей и их разрешений.
*/
async function loadACLRoles() {
pageTitle.textContent = 'Управление ролями ACL';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка ролей...</p></div>';
try {
const response = await fetch('/api/webui/acl/roles');
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
<div style="margin-bottom:16px;"><button class="btn btn-primary" onclick="showCreateRoleModal()"><i class="fas fa-plus-circle"></i> Создать роль</button></div>
<div class="data-table"><h3>Роли</h3><table><thead><tr><th>Название</th><th>Разрешения</th><th>Действия</th></tr></thead><tbody>
${data.data.map(role => `
<tr>
<td><strong>${escapeHtml(role.name)}</strong></td>
<td>${role.permissions.map(p => `<code>${escapeHtml(p)}</code>`).join('<br>') || '-'}</td>
<td><button class="btn btn-sm btn-secondary" onclick="showEditRoleModal('${escapeHtml(role.name)}', ${JSON.stringify(role.permissions)})"><i class="fas fa-key"></i> Права</button><button class="btn btn-sm btn-danger" onclick="deleteRole('${escapeHtml(role.name)}')"><i class="fas fa-trash"></i></button></td>
</tr>
`).join('')}
</tbody></table></div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки ролей</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
/**
* @async
* @description Загружает и отображает все разрешения, сгруппированные по ролям.
*/
async function loadACLPermissions() {
pageTitle.textContent = 'Управление разрешениями ACL';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка разрешений...</p></div>';
try {
const response = await fetch('/api/webui/acl/permissions');
const data = await response.json();
if (data.success) {
let html = '<div class="data-table"><h3>Разрешения по ролям</h3>';
for (const [roleName, permissions] of Object.entries(data.data)) {
html += `<div style="margin-top:20px;"><h4>Роль: ${escapeHtml(roleName)}</h4><div style="background:var(--bg-dark);padding:12px;border-radius:8px;">${permissions.map(p => `<code style="display:inline-block;margin:4px;padding:4px 8px;background:var(--bg-card);border-radius:4px;">${escapeHtml(p)}</code>`).join('') || '<em>Нет разрешений</em>'}</div></div>`;
}
html += '</div>';
contentArea.innerHTML = html;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки разрешений</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
// ============================== ТРАНЗАКЦИИ ==============================
/**
* @async
* @description Загружает и отображает список активных транзакций с возможностью управления.
*/
async function loadTransactionList() {
pageTitle.textContent = 'Активные транзакции';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка транзакций...</p></div>';
try {
const response = await fetch('/api/webui/transactions');
const data = await response.json();
if (data.success) {
contentArea.innerHTML = `
<div style="margin-bottom:16px;display:flex;gap:12px;flex-wrap:wrap;">
<button class="btn btn-primary" onclick="startSession()"><i class="fas fa-play"></i> Начать сессию</button>
<button class="btn btn-success" onclick="startTransaction()"><i class="fas fa-play-circle"></i> Начать транзакцию</button>
<button class="btn btn-success" onclick="commitTransaction()"><i class="fas fa-check-circle"></i> Зафиксировать</button>
<button class="btn btn-danger" onclick="abortTransaction()"><i class="fas fa-times-circle"></i> Отменить</button>
<button class="btn btn-secondary" onclick="loadTransactionList()"><i class="fas fa-sync-alt"></i> Обновить</button>
</div>
<div class="data-table"><h3>Транзакции</h3><table><thead><tr><th>ID транзакции</th><th>Статус</th><th>Начало</th><th>Операций</th><th>Действия</th></tr></thead><tbody>
${data.data.map(tx => `
<tr>
<td><code>${escapeHtml(tx.id)}</code></td>
<td><span class="status-badge status-${tx.status}">${escapeHtml(tx.status)}</span></td>
<td>${new Date(tx.start_time).toLocaleString()}</td>
<td>${tx.operation_count || 0}</td>
<td>
<button class="btn btn-sm btn-info" onclick="loadTransactionDetails('${escapeHtml(tx.id)}')"><i class="fas fa-info-circle"></i> Детали</button>
${tx.status === 'active' ? `<button class="btn btn-sm btn-success" onclick="commitTransactionById('${escapeHtml(tx.id)}')"><i class="fas fa-check"></i> Commit</button><button class="btn btn-sm btn-danger" onclick="abortTransactionById('${escapeHtml(tx.id)}')"><i class="fas fa-times"></i> Abort</button>` : ''}
</td>
</tr>
`).join('') || '<tr><td colspan="5">Нет активных транзакций</td></tr>'}
</tbody></table></div>
<div class="info-message" style="margin-top:20px;padding:12px;background:var(--bg-card);border-radius:8px;"><i class="fas fa-info-circle"></i> <strong>Информация о транзакциях:</strong><ul style="margin-top:8px;margin-left:20px;"><li>Транзакции поддерживают INSERT, UPDATE, DELETE</li><li>Используется MVCC для изоляции</li><li>WAL гарантирует персистентность</li></ul></div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки транзакций</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
// ============================== ИНДЕКСЫ ==============================
/**
* @async
* @description Загружает страницу управления индексами с выбором БД и коллекции.
*/
async function loadIndexesList() {
pageTitle.textContent = 'Управление индексами';
contentArea.innerHTML = `
<div style="margin-bottom:16px;"><div class="form-group"><label>База данных</label><select id="indexDbSelect" class="form-control" onchange="loadCollectionsForIndex()"><option value="">Выберите БД</option></select></div>
<div class="form-group"><label>Коллекция</label><select id="indexCollSelect" class="form-control" onchange="loadIndexesForCollection()"><option value="">Сначала выберите БД</option></select></div>
<button class="btn btn-primary" onclick="showCreateIndexModal()"><i class="fas fa-plus"></i> Создать индекс</button></div>
<div id="indexesContent" class="data-table"><p>Выберите базу данных и коллекцию</p></div>
`;
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('indexDbSelect');
dbSelect.innerHTML = '<option value="">Выберите БД</option>' + data.data.map(db => `<option value="${escapeHtml(db.name)}">${escapeHtml(db.name)}</option>`).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 = '<option value="">Сначала выберите БД</option>';
document.getElementById('indexesContent').innerHTML = '<p>Выберите базу данных и коллекцию</p>';
return;
}
collSelect.innerHTML = '<option value="">Загрузка...</option>';
try {
const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
const data = await response.json();
if (data.success && data.data.collections) {
collSelect.innerHTML = '<option value="">Выберите коллекцию</option>' + data.data.collections.map(coll => `<option value="${escapeHtml(coll.name)}">${escapeHtml(coll.name)}</option>`).join('');
} else {
collSelect.innerHTML = '<option value="">Нет коллекций</option>';
}
} catch (error) {
collSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
}
};
/**
* @async
* @description Загружает и отображает список индексов выбранной коллекции.
*/
async function loadIndexesForCollection() {
const dbName = document.getElementById('indexDbSelect').value;
const collName = document.getElementById('indexCollSelect').value;
if (!dbName || !collName) {
document.getElementById('indexesContent').innerHTML = '<p>Выберите базу данных и коллекцию</p>';
return;
}
document.getElementById('indexesContent').innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка индексов...</p></div>';
try {
const response = await fetch(`/api/webui/indexes/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}`);
const data = await response.json();
if (data.success) {
document.getElementById('indexesContent').innerHTML = `
<h3>Индексы коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}</h3>
<table><thead><tr><th>Имя индекса</th><th>Поля</th><th>Уникальный</th><th>Действия</th></tr></thead><tbody>
${data.data.map(idx => `<tr><td><code>${escapeHtml(idx.name)}</code></td><td>${idx.fields.join(', ')}</td><td>${idx.unique ? 'Да' : 'Нет'}</td><td><button class="btn btn-sm btn-danger" onclick="dropIndex('${escapeHtml(dbName)}', '${escapeHtml(collName)}', '${escapeHtml(idx.name)}')"><i class="fas fa-trash"></i> Удалить</button></td></tr>`).join('') || '<tr><td colspan="4">Нет индексов</td></tr>'}
</tbody></table>
`;
} else {
document.getElementById('indexesContent').innerHTML = '<div class="error-message">Ошибка загрузки индексов</div>';
}
} catch (error) {
document.getElementById('indexesContent').innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
// ============================== ТРИГГЕРЫ ==============================
/**
* @async
* @description Загружает страницу управления триггерами.
*/
async function loadTriggersList() {
pageTitle.textContent = 'Управление триггерами';
contentArea.innerHTML = `
<div style="margin-bottom:16px;"><div class="form-group"><label>База данных</label><select id="triggerDbSelect" class="form-control" onchange="loadCollectionsForTrigger()"><option value="">Выберите БД</option></select></div>
<div class="form-group"><label>Коллекция</label><select id="triggerCollSelect" class="form-control" onchange="loadTriggersForCollection()"><option value="">Сначала выберите БД</option></select></div>
<button class="btn btn-primary" onclick="showCreateTriggerModal()"><i class="fas fa-plus"></i> Создать триггер</button></div>
<div id="triggersContent" class="data-table"><p>Выберите базу данных и коллекцию</p></div>
`;
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('triggerDbSelect');
dbSelect.innerHTML = '<option value="">Выберите БД</option>' + data.data.map(db => `<option value="${escapeHtml(db.name)}">${escapeHtml(db.name)}</option>`).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 = '<option value="">Сначала выберите БД</option>';
document.getElementById('triggersContent').innerHTML = '<p>Выберите базу данных и коллекцию</p>';
return;
}
collSelect.innerHTML = '<option value="">Загрузка...</option>';
try {
const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
const data = await response.json();
if (data.success && data.data.collections) {
collSelect.innerHTML = '<option value="">Выберите коллекцию</option>' + data.data.collections.map(coll => `<option value="${escapeHtml(coll.name)}">${escapeHtml(coll.name)}</option>`).join('');
} else {
collSelect.innerHTML = '<option value="">Нет коллекций</option>';
}
} catch (error) {
collSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
}
};
/**
* @async
* @description Загружает и отображает список триггеров выбранной коллекции.
*/
async function loadTriggersForCollection() {
const dbName = document.getElementById('triggerDbSelect').value;
const collName = document.getElementById('triggerCollSelect').value;
if (!dbName || !collName) {
document.getElementById('triggersContent').innerHTML = '<p>Выберите базу данных и коллекцию</p>';
return;
}
document.getElementById('triggersContent').innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка триггеров...</p></div>';
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 = '<p>Нет триггеров для этой коллекции</p>';
return;
}
document.getElementById('triggersContent').innerHTML = `
<h3>Триггеры коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}</h3>
<table><thead><tr><th>Имя</th><th>Событие</th><th>Действие</th><th>Статус</th><th>Описание</th><th>Действия</th></tr></thead><tbody>
${data.data.map(trigger => `
<tr>
<td><code>${escapeHtml(trigger.name)}</code></td>
<td>${escapeHtml(trigger.event)}</td><td>${escapeHtml(trigger.action)}</td>
<td>${trigger.enabled ? '<span class="status-badge status-active">Включён</span>' : '<span class="status-badge status-inactive">Отключён</span>'}</td>
<td>${escapeHtml(trigger.description || '-')}</td>
<td>
${trigger.enabled ? `<button class="btn btn-sm btn-warning" onclick="toggleTrigger('${dbName}', '${collName}', '${trigger.name}', '${trigger.event}', false)">Отключить</button>` : `<button class="btn btn-sm btn-success" onclick="toggleTrigger('${dbName}', '${collName}', '${trigger.name}', '${trigger.event}', true)">Включить</button>`}
<button class="btn btn-sm btn-danger" onclick="deleteTrigger('${dbName}', '${collName}', '${trigger.name}', '${trigger.event}')">Удалить</button>
</td>
</tr>
`).join('')}
</tbody></table>
`;
} else {
document.getElementById('triggersContent').innerHTML = '<div class="error-message">Ошибка загрузки триггеров</div>';
}
} catch (error) {
document.getElementById('triggersContent').innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
/**
* @async
* @description Загружает и отображает лог выполнения триггеров.
*/
async function loadTriggerLog() {
pageTitle.textContent = 'Лог выполнения триггеров';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка лога...</p></div>';
try {
const response = await fetch('/api/webui/trigger/log');
const data = await response.json();
if (data.success) {
if (data.data.length === 0) {
contentArea.innerHTML = '<p>Лог выполнения триггеров пуст</p>';
return;
}
contentArea.innerHTML = `
<div class="data-table"><h3>Лог выполнения триггеров</h3><table><thead><tr><th>Триггер</th><th>Событие</th><th>Коллекция</th><th>БД</th><th>Документ</th><th>Время</th><th>Пользователь</th></tr></thead><tbody>
${data.data.map(entry => `<tr><td><code>${escapeHtml(entry.trigger_name)}</code></td><td>${escapeHtml(entry.event)}</td><td>${escapeHtml(entry.collection)}</td><td>${escapeHtml(entry.database)}</td><td><code>${escapeHtml(entry.document_id)}</code></td><td>${new Date(entry.timestamp).toLocaleString()}</td><td>${escapeHtml(entry.user || '-')}</td></tr>`).join('')}
</tbody></table></div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки лога</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
// ============================== ОГРАНИЧЕНИЯ (CONSTRAINTS) ==============================
/**
* @async
* @description Загружает страницу управления ограничениями коллекции.
*/
async function loadConstraintsList() {
pageTitle.textContent = 'Управление ограничениями (Constraints)';
contentArea.innerHTML = `
<div style="margin-bottom:16px;"><div class="form-group"><label>База данных</label><select id="constraintDbSelect" class="form-control" onchange="loadCollectionsForConstraints()"><option value="">Выберите БД</option></select></div>
<div class="form-group"><label>Коллекция</label><select id="constraintCollSelect" class="form-control" onchange="loadConstraintsForCollection()"><option value="">Сначала выберите БД</option></select></div></div>
<div id="constraintsContent" class="data-table"><p>Выберите базу данных и коллекцию</p></div>
`;
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('constraintDbSelect');
dbSelect.innerHTML = '<option value="">Выберите БД</option>' + data.data.map(db => `<option value="${escapeHtml(db.name)}">${escapeHtml(db.name)}</option>`).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 = '<option value="">Сначала выберите БД</option>';
document.getElementById('constraintsContent').innerHTML = '<p>Выберите базу данных и коллекцию</p>';
return;
}
collSelect.innerHTML = '<option value="">Загрузка...</option>';
try {
const response = await fetch(`/api/webui/collections/${encodeURIComponent(dbName)}`);
const data = await response.json();
if (data.success && data.data.collections) {
collSelect.innerHTML = '<option value="">Выберите коллекцию</option>' + data.data.collections.map(coll => `<option value="${escapeHtml(coll.name)}">${escapeHtml(coll.name)}</option>`).join('');
} else {
collSelect.innerHTML = '<option value="">Нет коллекций</option>';
}
} catch (error) {
collSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
}
};
/**
* @async
* @description Загружает и отображает все ограничения выбранной коллекции.
*/
async function loadConstraintsForCollection() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').value;
if (!dbName || !collName) {
document.getElementById('constraintsContent').innerHTML = '<p>Выберите базу данных и коллекцию</p>';
return;
}
document.getElementById('constraintsContent').innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка ограничений...</p></div>';
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 = '<p>Нет ограничений для этой коллекции</p>';
return;
}
// Группировка ограничений по типу
const grouped = { required: [], unique: [], min: [], max: [], enum: [], regex: [] };
constraints.forEach(c => { if (grouped[c.type]) grouped[c.type].push(c); });
let html = `<h3>Ограничения коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}</h3><div style="margin-top:20px;">`;
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 += `<div class="constraint-section"><h4><i class="fas ${icon}"></i> ${title}</h4><table class="data-table"><thead><tr><th>Поле</th><th>${type === 'enum' ? 'Допустимые значения' : (type === 'min' || type === 'max' ? 'Значение' : (type === 'regex' ? 'Шаблон' : ''))}</th><th>Действия</th></tr></thead><tbody>`;
for (const c of grouped[type]) {
html += `<tr><td><code>${escapeHtml(c.field)}</code></td><td>${type === 'enum' ? c.values.map(v => `<span class="badge">${escapeHtml(String(v))}</span>`).join(' ') : (c.value || c.pattern || '-')}</td><td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', '${type}', '${escapeHtml(c.field)}')"><i class="fas fa-trash"></i> Удалить</button></td></tr>`;
}
html += `</tbody></table></div>`;
}
}
html += `</div>`;
document.getElementById('constraintsContent').innerHTML = html;
} else {
document.getElementById('constraintsContent').innerHTML = '<div class="error-message">Ошибка загрузки ограничений</div>';
}
} catch (error) {
document.getElementById('constraintsContent').innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
// ============================== ИМПОРТ/ЭКСПОРТ ==============================
/**
* @async
* @description Загружает страницу экспорта данных.
*/
async function loadExportPage() {
pageTitle.textContent = 'Экспорт данных';
contentArea.innerHTML = `
<div class="form-group"><label>База данных для экспорта</label><select id="exportDbSelect" class="form-control"><option value="">Выберите БД</option></select></div>
<div class="form-group"><label>Имя файла (опционально)</label><input type="text" id="exportFilename" class="form-control" placeholder="database_export.msgpack"></div>
<button class="btn btn-primary" onclick="performExport()"><i class="fas fa-upload"></i> Экспортировать</button>
<div id="exportResult" style="margin-top:20px;"></div>
`;
try {
const response = await fetch('/api/webui/databases');
const data = await response.json();
if (data.success) {
const dbSelect = document.getElementById('exportDbSelect');
dbSelect.innerHTML = '<option value="">Выберите БД</option>' + data.data.map(db => `<option value="${escapeHtml(db.name)}">${escapeHtml(db.name)}</option>`).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 = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Экспорт данных...</p></div>';
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 = `<div class="success-message"><i class="fas fa-check-circle"></i><p>Экспорт завершён</p><p>БД: ${escapeHtml(dbName)}</p><p>Коллекций: ${data.data.collections}</p><p>Файл: ${data.data.filename}</p></div>`;
showNotification('Экспорт завершён', 'success');
} else {
exportResult.innerHTML = `<div class="error-message">Ошибка: ${data.error}</div>`;
}
} catch (error) {
exportResult.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
/**
* @async
* @description Загружает страницу импорта данных.
*/
async function loadImportPage() {
pageTitle.textContent = 'Импорт данных';
contentArea.innerHTML = `
<div class="form-group"><label>Целевая база данных</label><input type="text" id="importDbName" class="form-control" placeholder="target_database"></div>
<div class="form-group"><label>JSON файл с данными</label><input type="file" id="importFile" class="form-control" accept=".json"></div>
<div class="form-group"><label><input type="checkbox" id="importOverwrite"> Перезаписывать существующие документы</label></div>
<button class="btn btn-primary" onclick="performImport()"><i class="fas fa-download"></i> Импортировать</button>
<div id="importResult" style="margin-top:20px;"></div>
`;
}
/**
* @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 = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Импорт данных...</p></div>';
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 = `<div class="success-message"><i class="fas fa-check-circle"></i><p>Импорт завершён</p><p>БД: ${escapeHtml(dbName)}</p><p>Импортировано коллекций: ${data.data.collections}</p><p>Импортировано документов: ${data.data.documents}</p></div>`;
showNotification('Импорт завершён', 'success');
} else {
importResult.innerHTML = `<div class="error-message">Ошибка: ${data.error}</div>`;
}
} catch (error) {
importResult.innerHTML = `<div class="error-message">Ошибка: ${error.message}</div>`;
}
}
// ============================== CRUD ОПЕРАЦИИ (МОДАЛЬНЫЕ ОКНА) ==============================
/**
* @description Отображает модальное окно для создания базы данных.
*/
function showCreateDatabaseModal() {
modalTitle.textContent = 'Создать базу данных';
modalConfirm.textContent = 'Подтвердить';
modalBody.innerHTML = `<div class="form-group"><label for="dbName">Имя базы данных</label><input type="text" id="dbName" class="form-control" placeholder="my_database"></div>`;
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 = `<div class="form-group"><label>База данных</label><input type="text" class="form-control" value="${escapeHtml(currentDatabase)}" disabled></div><div class="form-group"><label for="collName">Имя коллекции</label><input type="text" id="collName" class="form-control" placeholder="my_collection"></div>`;
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 = `<div class="form-group"><label>База данных</label><input type="text" class="form-control" value="${escapeHtml(currentDatabase)}" disabled></div><div class="form-group"><label>Коллекция</label><input type="text" class="form-control" value="${escapeHtml(currentCollection)}" disabled></div><div class="form-group"><label for="docData">Данные документа (JSON)</label><textarea id="docData" class="form-control" rows="8" placeholder='{"_id": "doc1", "name": "Example", "value": 123}'></textarea></div>`;
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 = `<div class="form-group"><label>База данных</label><input type="text" class="form-control" value="${escapeHtml(currentDatabase)}" disabled></div><div class="form-group"><label>Коллекция</label><input type="text" class="form-control" value="${escapeHtml(currentCollection)}" disabled></div><div class="form-group"><label for="updateDocId">ID документа</label><input type="text" id="updateDocId" class="form-control" value="${escapeHtml(docId)}" ${docId ? 'disabled' : ''} placeholder="document_id"></div><div class="form-group"><label for="updateData">Обновления (JSON)</label><textarea id="updateData" class="form-control" rows="8" placeholder='{"field1": "new value", "field2": 456}'>${escapeHtml(currentFields ? JSON.stringify(currentFields, null, 2) : '')}</textarea></div>`;
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 = `<div class="form-group"><label>Имя пользователя</label><input type="text" id="username" class="form-control" placeholder="username"></div><div class="form-group"><label>Пароль</label><input type="password" id="password" class="form-control" placeholder="password"></div><div class="form-group"><label>Роли (через запятую)</label><input type="text" id="roles" class="form-control" placeholder="admin, guest"></div>`;
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 = `<div class="form-group"><label>Название роли</label><input type="text" id="roleName" class="form-control" placeholder="my_role"></div>`;
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 = `<div class="form-group"><label>База данных</label><input type="text" class="form-control" value="${escapeHtml(dbName)}" disabled></div><div class="form-group"><label>Коллекция</label><input type="text" class="form-control" value="${escapeHtml(collName)}" disabled></div><div class="form-group"><label>Имя индекса</label><input type="text" id="indexName" class="form-control" placeholder="my_index"></div><div class="form-group"><label>Поля (через запятую)</label><input type="text" id="indexFields" class="form-control" placeholder="field1, field2"></div><div class="form-group"><label><input type="checkbox" id="indexUnique"> Уникальный индекс</label></div>`;
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 = `
<div class="form-group"><label>База данных</label><input type="text" class="form-control" value="${escapeHtml(dbName)}" disabled></div>
<div class="form-group"><label>Коллекция</label><input type="text" class="form-control" value="${escapeHtml(collName)}" disabled></div>
<div class="form-group"><label>Имя триггера</label><input type="text" id="triggerName" class="form-control" placeholder="my_trigger"></div>
<div class="form-group"><label>Событие</label><select id="triggerEvent" class="form-control"><option value="BEFORE_INSERT">BEFORE_INSERT</option><option value="AFTER_INSERT">AFTER_INSERT</option><option value="BEFORE_UPDATE">BEFORE_UPDATE</option><option value="AFTER_UPDATE">AFTER_UPDATE</option><option value="BEFORE_DELETE">BEFORE_DELETE</option><option value="AFTER_DELETE">AFTER_DELETE</option></select></div>
<div class="form-group"><label>Действие</label><select id="triggerAction" class="form-control"><option value="modify">modify - Модифицировать документ</option><option value="log">log - Записать в лог</option><option value="abort">abort - Прервать операцию</option><option value="skip">skip - Пропустить операцию</option><option value="notify">notify - Отправить уведомление</option></select></div>
<div class="form-group"><label>Описание (опционально)</label><input type="text" id="triggerDescription" class="form-control" placeholder="Описание триггера"></div>
<div class="form-group"><label>Условие (опционально)</label><div style="display:flex;gap:8px;"><input type="text" id="conditionField" class="form-control" style="flex:1;" placeholder="Поле"><select id="conditionOperator" class="form-control" style="width:100px;"><option value="eq">=</option><option value="ne">≠</option><option value="gt">&gt;</option><option value="lt">&lt;</option><option value="gte">≥</option><option value="lte">≤</option><option value="exists">exists</option></select><input type="text" id="conditionValue" class="form-control" style="flex:1;" placeholder="Значение"></div></div>
<div class="form-group"><label>Операции триггера (JSON массив)</label><textarea id="triggerOperations" class="form-control" rows="6" placeholder='[{"type": "set", "field": "updated_at", "value": "$$NOW"}]'></textarea><small>Доступные операции: set, unset, inc, mul, rename, currentDate. Спецзначения: $$NOW, $$USER, $$ROLE</small></div>
`;
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
/**
* @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: '<i class="fas fa-check-circle"></i>', error: '<i class="fas fa-exclamation-circle"></i>', warning: '<i class="fas fa-exclamation-triangle"></i>', info: '<i class="fas fa-info-circle"></i>' };
notification.innerHTML = `${icons[type] || icons.info}<span>${escapeHtml(message)}</span>`;
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 = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка деталей...</p></div>';
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 = `<div><div><strong>ID:</strong> <code>${escapeHtml(tx.id)}</code></div><div><strong>Статус:</strong> <span class="status-badge status-${tx.status}">${escapeHtml(tx.status)}</span></div><div><strong>Время начала:</strong> ${new Date(tx.start_time).toLocaleString()}</div><div><strong>Количество операций:</strong> ${tx.operation_count}</div><hr><h4>Операции (${tx.operations?.length || 0})</h4>${tx.operations?.length ? `<table class="data-table"><thead><tr><th>Тип</th><th>БД</th><th>Коллекция</th><th>ID документа</th></tr></thead><tbody>${tx.operations.map(op => `<tr><td>${escapeHtml(op.type)}</td><td>${escapeHtml(op.database)}</td><td>${escapeHtml(op.collection)}</td><td><code>${escapeHtml(op.document_id)}</code></td></tr>`).join('')}</tbody></table>` : '<p>Нет операций</p>'}</div>`;
} else {
modalBody.innerHTML = `<div class="error-message">Ошибка загрузки деталей: ${data.error || 'Неизвестная ошибка'}</div>`;
}
} catch (error) {
modalBody.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
/**
* @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 = `<div class="form-group"><label>База данных</label><input type="text" class="form-control" value="${escapeHtml(dbName)}" disabled></div><div class="form-group"><label>Коллекция</label><input type="text" class="form-control" value="${escapeHtml(collName)}" disabled></div>${fields.map(f => `<div class="form-group"><label>${f.label}</label><input type="${f.type}" id="constraint_${f.name}" class="form-control" placeholder="${f.label}"></div>`).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'); }
};
}