1414 lines
62 KiB
JavaScript
1414 lines
62 KiB
JavaScript
/*
|
||
* Copyright 2026 Safronov Grigorii
|
||
*
|
||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
*
|
||
* You may obtain a copy of the License at
|
||
* https://opensource.org/licenses/CDDL-1.0
|
||
*/
|
||
|
||
/**
|
||
* Futriis DB Dashboard - Web Interface JavaScript для веб-интерфейса Futriis DB Dashboard
|
||
* @version 1.0.0
|
||
* @description Обеспечивает полное управление СУБД: CRUD операции, ACL, индексы,
|
||
* транзакции, триггеры, ограничения (constraints), импорт/экспорт,
|
||
* управление кластером и аудит. Использует async/await, Fetch API,
|
||
* динамическую отрисовку DOM и модальные окна.
|
||
* @author Futriis Team
|
||
* @license CDDL-1.0
|
||
*/
|
||
|
||
// ======================== СОСТОЯНИЕ ПРИЛОЖЕНИЯ ========================
|
||
|
||
/** @type {string|null} ID текущей сессии */
|
||
let currentSession = null;
|
||
|
||
/** @type {string|null} Имя текущей базы данных */
|
||
let currentDatabase = null;
|
||
|
||
/** @type {string|null} Имя текущей коллекции */
|
||
let currentCollection = null;
|
||
|
||
/** @type {string|null} Имя текущего пользователя */
|
||
let currentUser = null;
|
||
|
||
/** @type {boolean} Флаг администратора */
|
||
let isAdmin = false;
|
||
|
||
// ======================== DOM ЭЛЕМЕНТЫ ========================
|
||
|
||
const DOM = {
|
||
content: document.getElementById('contentArea'),
|
||
title: document.getElementById('pageTitle'),
|
||
connection: document.getElementById('connectionStatus'),
|
||
userName: document.querySelector('#userName'),
|
||
userRole: document.getElementById('userRole'),
|
||
logoutBtn: document.getElementById('logoutBtn'),
|
||
menuToggle: document.getElementById('menuToggle'),
|
||
sidebar: document.querySelector('.sidebar'),
|
||
modal: document.getElementById('modal'),
|
||
modalTitle: document.getElementById('modalTitle'),
|
||
modalBody: document.getElementById('modalBody'),
|
||
modalConfirm: document.getElementById('modalConfirm'),
|
||
modalCloseBtns: document.querySelectorAll('.modal-close'),
|
||
changePasswordIcon: document.getElementById('changePasswordIcon'),
|
||
userAvatar: document.getElementById('userAvatar'),
|
||
notificationContainer: document.getElementById('notificationContainer')
|
||
};
|
||
|
||
// ======================== ИНИЦИАЛИЗАЦИЯ ========================
|
||
|
||
/**
|
||
* Главная точка входа. Выполняется после загрузки DOM.
|
||
*/
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
await checkSession();
|
||
initNavigation();
|
||
initEventListeners();
|
||
initAvatarUpload();
|
||
initChangePassword();
|
||
});
|
||
|
||
// ======================== АУТЕНТИФИКАЦИЯ ========================
|
||
|
||
/**
|
||
* Проверяет активность сессии на сервере
|
||
*/
|
||
async function checkSession() {
|
||
try {
|
||
const response = await fetch('/api/webui/session');
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.data.authenticated) {
|
||
currentUser = data.data.username;
|
||
isAdmin = data.data.is_admin || false;
|
||
DOM.userName.textContent = currentUser;
|
||
DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь';
|
||
|
||
if (data.data.avatar) {
|
||
updateAvatarDisplay(data.data.avatar);
|
||
} else {
|
||
loadUserAvatar();
|
||
}
|
||
|
||
updateConnectionStatus(data.data.connection_status);
|
||
loadDashboard();
|
||
startConnectionStatusMonitor();
|
||
} else {
|
||
showLoginModal();
|
||
}
|
||
} catch (error) {
|
||
console.error('Session check failed:', error);
|
||
showLoginModal();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Запускает периодическую проверку статуса подключения
|
||
*/
|
||
function startConnectionStatusMonitor() {
|
||
setInterval(async () => {
|
||
if (currentUser) {
|
||
try {
|
||
const response = await fetch('/api/webui/session');
|
||
const data = await response.json();
|
||
updateConnectionStatus(data.data?.connection_status || 'disconnected');
|
||
} catch (error) {
|
||
updateConnectionStatus('disconnected');
|
||
}
|
||
}
|
||
}, 5000);
|
||
}
|
||
|
||
/**
|
||
* Обновляет индикатор подключения
|
||
* @param {string} status - Статус подключения ('connected' или 'disconnected')
|
||
*/
|
||
function updateConnectionStatus(status) {
|
||
if (status === 'connected') {
|
||
DOM.connection.className = 'connection-status online';
|
||
DOM.connection.innerHTML = '<span>СУБД подключена</span>';
|
||
} else {
|
||
DOM.connection.className = 'connection-status offline';
|
||
DOM.connection.innerHTML = '<span>СУБД не подключена</span>';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отображает модальное окно для входа в систему
|
||
*/
|
||
function showLoginModal() {
|
||
DOM.modalTitle.textContent = 'Вход в систему СУБД Futriis';
|
||
DOM.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>
|
||
`;
|
||
DOM.modalConfirm.textContent = 'Войти';
|
||
DOM.modal.classList.add('show');
|
||
|
||
const confirmHandler = async () => {
|
||
const username = document.getElementById('username').value;
|
||
const password = document.getElementById('password').value;
|
||
|
||
if (!username || !password) {
|
||
showNotification('Пожалуйста, заполните все поля', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/webui/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password })
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
currentUser = username;
|
||
isAdmin = data.data.is_admin || false;
|
||
DOM.userName.textContent = username;
|
||
DOM.userRole.textContent = isAdmin ? 'Администратор' : 'Пользователь';
|
||
|
||
if (data.data.avatar) {
|
||
updateAvatarDisplay(data.data.avatar);
|
||
}
|
||
|
||
DOM.modal.classList.remove('show');
|
||
showNotification('Вход выполнен успешно', 'success');
|
||
updateConnectionStatus('connected');
|
||
startConnectionStatusMonitor();
|
||
loadDashboard();
|
||
} else {
|
||
showNotification(data.error || 'Неверный логин и/или пароль', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения к серверу', 'error');
|
||
}
|
||
};
|
||
|
||
DOM.modalConfirm.onclick = confirmHandler;
|
||
|
||
const handleEnter = (e) => {
|
||
if (e.key === 'Enter') {
|
||
confirmHandler();
|
||
document.removeEventListener('keydown', handleEnter);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', handleEnter);
|
||
}
|
||
|
||
// ======================== АВАТАР ПОЛЬЗОВАТЕЛЯ ========================
|
||
|
||
/**
|
||
* Загружает аватар пользователя с сервера
|
||
*/
|
||
async function loadUserAvatar() {
|
||
try {
|
||
const response = await fetch('/api/webui/user/info');
|
||
const data = await response.json();
|
||
|
||
if (data.success && data.data.avatar) {
|
||
updateAvatarDisplay(data.data.avatar);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to load avatar:', error);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Обновляет отображение аватара
|
||
* @param {string} avatarBase64 - Аватар в формате base64
|
||
*/
|
||
function updateAvatarDisplay(avatarBase64) {
|
||
if (!DOM.userAvatar) return;
|
||
DOM.userAvatar.innerHTML = `<img src="${avatarBase64}" alt="Avatar" style="width:40px;height:40px;border-radius:50%;object-fit:cover;">`;
|
||
}
|
||
|
||
/**
|
||
* Инициализирует загрузку аватара
|
||
*/
|
||
function initAvatarUpload() {
|
||
if (!DOM.userAvatar) return;
|
||
|
||
DOM.userAvatar.style.cursor = 'pointer';
|
||
DOM.userAvatar.addEventListener('click', () => {
|
||
showAvatarUploadModal();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Показывает модальное окно загрузки аватара
|
||
*/
|
||
function showAvatarUploadModal() {
|
||
const avatarModal = document.getElementById('avatarUploadModal');
|
||
const fileInput = document.getElementById('avatarFile');
|
||
const preview = document.getElementById('avatarPreview');
|
||
const uploadBtn = document.getElementById('uploadAvatarBtn');
|
||
|
||
if (!avatarModal) return;
|
||
|
||
if (fileInput) fileInput.value = '';
|
||
if (preview) preview.innerHTML = '';
|
||
|
||
avatarModal.classList.add('show');
|
||
|
||
if (fileInput) {
|
||
fileInput.onchange = function() {
|
||
if (this.files && this.files[0]) {
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
if (preview) {
|
||
preview.innerHTML = `<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');
|
||
};
|
||
});
|
||
}
|
||
|
||
// ======================== СМЕНА ПАРОЛЯ ========================
|
||
|
||
/**
|
||
* Инициализирует смену пароля
|
||
*/
|
||
function initChangePassword() {
|
||
if (DOM.changePasswordIcon) {
|
||
DOM.changePasswordIcon.addEventListener('click', () => {
|
||
showChangePasswordModal();
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Показывает модальное окно смены пароля
|
||
*/
|
||
function showChangePasswordModal() {
|
||
const passwordModal = document.getElementById('changePasswordModal');
|
||
const currentPasswordInput = document.getElementById('currentPassword');
|
||
const newPasswordInput = document.getElementById('newPassword');
|
||
const confirmPasswordInput = document.getElementById('confirmPassword');
|
||
const changeBtn = document.getElementById('changePasswordBtn');
|
||
|
||
if (!passwordModal) return;
|
||
|
||
if (currentPasswordInput) currentPasswordInput.value = '';
|
||
if (newPasswordInput) newPasswordInput.value = '';
|
||
if (confirmPasswordInput) confirmPasswordInput.value = '';
|
||
|
||
passwordModal.classList.add('show');
|
||
|
||
if (changeBtn) {
|
||
changeBtn.onclick = async () => {
|
||
const currentPassword = currentPasswordInput?.value || '';
|
||
const newPassword = newPasswordInput?.value || '';
|
||
const confirmPassword = confirmPasswordInput?.value || '';
|
||
|
||
if (!currentPassword) {
|
||
showNotification('Введите текущий пароль', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (!newPassword) {
|
||
showNotification('Введите новый пароль', 'warning');
|
||
return;
|
||
}
|
||
|
||
if (newPassword !== confirmPassword) {
|
||
showNotification('Новый пароль и подтверждение не совпадают', 'error');
|
||
return;
|
||
}
|
||
|
||
if (newPassword.length < 4) {
|
||
showNotification('Новый пароль должен содержать минимум 4 символа', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/api/webui/change-password', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
current_password: currentPassword,
|
||
new_password: newPassword
|
||
})
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
passwordModal.classList.remove('show');
|
||
showNotification('Пароль успешно изменён', 'success');
|
||
} else {
|
||
showNotification(data.error || 'Ошибка смены пароля', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
}
|
||
|
||
const closeButtons = passwordModal.querySelectorAll('.modal-close');
|
||
closeButtons.forEach(btn => {
|
||
btn.onclick = () => {
|
||
passwordModal.classList.remove('show');
|
||
};
|
||
});
|
||
}
|
||
|
||
// ======================== НАВИГАЦИЯ ========================
|
||
|
||
/**
|
||
* Инициализирует обработчики навигации
|
||
*/
|
||
function initNavigation() {
|
||
document.querySelectorAll('.nav-link[data-section]').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const section = link.dataset.section;
|
||
loadSection(section);
|
||
setActiveNav(link);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('[data-action]').forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const action = item.dataset.action;
|
||
handleAction(action);
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('.has-submenu > .nav-link').forEach(link => {
|
||
link.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const parent = link.closest('.has-submenu');
|
||
parent.classList.toggle('open');
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Инициализирует глобальные обработчики событий
|
||
*/
|
||
function initEventListeners() {
|
||
DOM.logoutBtn.addEventListener('click', async () => {
|
||
await fetch('/api/webui/logout', { method: 'POST' });
|
||
currentUser = null;
|
||
isAdmin = false;
|
||
updateConnectionStatus('disconnected');
|
||
if (DOM.userAvatar) {
|
||
DOM.userAvatar.innerHTML = '<i class="fas fa-user-circle" style="font-size: 40px;"></i>';
|
||
}
|
||
showLoginModal();
|
||
});
|
||
|
||
if (DOM.menuToggle) {
|
||
DOM.menuToggle.addEventListener('click', () => {
|
||
DOM.sidebar.classList.toggle('open');
|
||
});
|
||
}
|
||
|
||
DOM.modalCloseBtns.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
DOM.modal.classList.remove('show');
|
||
});
|
||
});
|
||
|
||
DOM.modal.addEventListener('click', (e) => {
|
||
if (e.target === DOM.modal) {
|
||
DOM.modal.classList.remove('show');
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Загружает соответствующую секцию интерфейса
|
||
* @param {string} section - Идентификатор секции
|
||
*/
|
||
async function loadSection(section) {
|
||
const sections = {
|
||
dashboard: loadDashboard,
|
||
cluster: loadClusterManagement,
|
||
logs: loadLogs,
|
||
'plugins-list': loadPluginsList,
|
||
settings: loadSettings,
|
||
'acl-users': loadACLUsers,
|
||
'acl-roles': loadACLRoles,
|
||
'acl-permissions': loadACLPermissions,
|
||
'tx-list': loadTransactionList,
|
||
'indexes-list': loadIndexesList,
|
||
'export-data': loadExportPage,
|
||
'import-data': loadImportPage,
|
||
'constraints-list': loadConstraintsList,
|
||
'triggers-list': loadTriggersList,
|
||
'trigger-log': loadTriggerLog
|
||
};
|
||
|
||
const loader = sections[section];
|
||
if (loader) {
|
||
await loader();
|
||
} else {
|
||
DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Устанавливает активный пункт навигации
|
||
* @param {HTMLElement} activeLink - Активный элемент ссылки
|
||
*/
|
||
function setActiveNav(activeLink) {
|
||
document.querySelectorAll('.nav-link').forEach(link => link.classList.remove('active'));
|
||
activeLink.classList.add('active');
|
||
}
|
||
|
||
// ======================== ДАШБОРД ========================
|
||
|
||
/**
|
||
* Загружает и отображает главную панель управления
|
||
*/
|
||
async function loadDashboard() {
|
||
DOM.title.textContent = 'Панель управления';
|
||
DOM.content.innerHTML = '<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();
|
||
|
||
DOM.content.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) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки данных</div>';
|
||
showNotification('Ошибка загрузки дашборда', 'error');
|
||
}
|
||
}
|
||
|
||
// ======================== БАЗЫ ДАННЫХ И КОЛЛЕКЦИИ ========================
|
||
|
||
/**
|
||
* Отображает список коллекций в выбранной базе данных
|
||
* @param {string} dbName - Имя базы данных
|
||
*/
|
||
window.viewDatabase = async function(dbName) {
|
||
currentDatabase = dbName;
|
||
DOM.title.textContent = `База данных: ${dbName}`;
|
||
DOM.content.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) {
|
||
DOM.content.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 {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки коллекций</div>';
|
||
}
|
||
} catch (error) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка подключения</div>';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Отображает документы выбранной коллекции
|
||
* @param {string} dbName - Имя базы данных
|
||
* @param {string} collName - Имя коллекции
|
||
*/
|
||
window.viewCollection = async function(dbName, collName) {
|
||
currentDatabase = dbName;
|
||
currentCollection = collName;
|
||
DOM.title.textContent = `Коллекция: ${dbName}.${collName}`;
|
||
DOM.content.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) {
|
||
DOM.content.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 {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки документов</div>';
|
||
}
|
||
} catch (error) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка подключения</div>';
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Удаляет коллекцию
|
||
* @param {string} dbName - Имя БД
|
||
* @param {string} collName - Имя коллекции
|
||
*/
|
||
window.deleteCollection = async function(dbName, collName) {
|
||
if (!confirm(`Удалить коллекцию "${collName}"? Это действие необратимо.`)) return;
|
||
try {
|
||
const response = await fetch(`/api/db/${dbName}/${collName}`, { method: 'DELETE' });
|
||
if (response.ok) {
|
||
showNotification(`Коллекция "${collName}" удалена`, 'success');
|
||
viewDatabase(dbName);
|
||
} else {
|
||
const error = await response.json();
|
||
showNotification(error.error || 'Ошибка удаления коллекции', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Удаляет документ
|
||
* @param {string} dbName - Имя БД
|
||
* @param {string} collName - Имя коллекции
|
||
* @param {string} docId - ID документа
|
||
*/
|
||
window.deleteDocument = async function(dbName, collName, docId) {
|
||
if (!confirm(`Удалить документ "${docId}"?`)) return;
|
||
try {
|
||
const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, { method: 'DELETE' });
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
showNotification('Документ удалён', 'success');
|
||
viewCollection(dbName, collName);
|
||
} else {
|
||
showNotification(result.error || 'Ошибка удаления документа', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
|
||
// ======================== ЛОГИ ОПЕРАЦИЙ ========================
|
||
|
||
/**
|
||
* Загружает и отображает лог операций веб-интерфейса
|
||
*/
|
||
async function loadLogs() {
|
||
if (!isAdmin) {
|
||
DOM.content.innerHTML = '<div class="error-message">Доступ запрещён. Только для администраторов.</div>';
|
||
return;
|
||
}
|
||
|
||
DOM.title.textContent = 'Лог операций';
|
||
DOM.content.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка логов...</p></div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/webui/logs?limit=500');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const logs = data.data;
|
||
if (logs.length === 0) {
|
||
DOM.content.innerHTML = '<p>Лог операций пуст</p>';
|
||
return;
|
||
}
|
||
|
||
DOM.content.innerHTML = `
|
||
<div class="data-table">
|
||
<h3>Лог операций веб-интерфейса</h3>
|
||
<div style="margin-bottom: 16px;">
|
||
<button class="btn btn-sm btn-secondary" onclick="loadLogs()"><i class="fas fa-sync-alt"></i> Обновить</button>
|
||
</div>
|
||
<table><thead><tr>
|
||
<th>Время</th><th>Операция</th><th>Цель</th><th>Пользователь</th><th>Статус</th><th>Ошибка</th>
|
||
</tr></thead><tbody>
|
||
${logs.map(log => `
|
||
<tr>
|
||
<td>${new Date(log.timestamp).toLocaleString()}</td>
|
||
<td><code>${escapeHtml(log.operation)}</code></td>
|
||
<td>${escapeHtml(log.target)}</td>
|
||
<td>${escapeHtml(log.user)}</td>
|
||
<td><span class="status-badge status-${log.status === 'success' ? 'active' : 'inactive'}">${log.status === 'success' ? 'Успех' : 'Ошибка'}</span></td>
|
||
<td>${log.error_msg ? `<span class="error-text">${escapeHtml(log.error_msg)}</span>` : '-'}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody></table>
|
||
</div>
|
||
`;
|
||
} else {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки логов</div>';
|
||
}
|
||
} catch (error) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка подключения</div>';
|
||
}
|
||
}
|
||
|
||
// ======================== ПЛАГИНЫ ========================
|
||
|
||
/**
|
||
* Загружает и отображает список плагинов
|
||
*/
|
||
async function loadPluginsList() {
|
||
if (!isAdmin) {
|
||
DOM.content.innerHTML = '<div class="error-message">Доступ запрещён. Только для администраторов.</div>';
|
||
return;
|
||
}
|
||
|
||
DOM.title.textContent = 'Управление плагинами';
|
||
DOM.content.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка плагинов...</p></div>';
|
||
|
||
try {
|
||
const response = await fetch('/api/webui/plugins');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
const plugins = data.data;
|
||
DOM.content.innerHTML = `
|
||
<div style="margin-bottom: 16px;">
|
||
<button class="btn btn-primary" onclick="showUploadPluginModal()"><i class="fas fa-upload"></i> Загрузить плагин</button>
|
||
<button class="btn btn-secondary" onclick="loadPluginsList()"><i class="fas fa-sync-alt"></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>
|
||
${plugins.map(plugin => `
|
||
<tr>
|
||
<td><strong>${escapeHtml(plugin.name)}</strong></strong></td>
|
||
<td>${escapeHtml(plugin.version)}</td>
|
||
<td>${escapeHtml(plugin.author)}</td>
|
||
<td>${escapeHtml(plugin.description)}</td>
|
||
<td>${new Date(plugin.loaded_at).toLocaleString()}</td>
|
||
<td>
|
||
<button class="btn btn-sm btn-success" onclick="startPlugin('${escapeHtml(plugin.name)}')"><i class="fas fa-play"></i> Старт</button>
|
||
<button class="btn btn-sm btn-warning" onclick="stopPlugin('${escapeHtml(plugin.name)}')"><i class="fas fa-stop"></i> Стоп</button>
|
||
<button class="btn btn-sm btn-danger" onclick="deletePlugin('${escapeHtml(plugin.name)}')"><i class="fas fa-trash"></i> Удалить</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
${plugins.length === 0 ? '<tr><td colspan="6">Нет загруженных плагинов</td></tr>' : ''}
|
||
</tbody></table>
|
||
</div>
|
||
`;
|
||
} else {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки плагинов</div>';
|
||
}
|
||
} catch (error) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка подключения</div>';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Показывает модальное окно загрузки плагина
|
||
*/
|
||
function showUploadPluginModal() {
|
||
DOM.modalTitle.textContent = 'Загрузить плагин';
|
||
DOM.modalBody.innerHTML = `
|
||
<div class="form-group">
|
||
<label>Файл плагина (.lua)</label>
|
||
<input type="file" id="pluginFile" class="form-control" accept=".lua">
|
||
</div>
|
||
<div class="info-message">
|
||
<i class="fas fa-info-circle"></i>
|
||
Плагины должны быть написаны на Lua и содержать функции on_load, on_start, on_stop, on_unload
|
||
</div>
|
||
`;
|
||
DOM.modalConfirm.textContent = 'Загрузить';
|
||
DOM.modal.classList.add('show');
|
||
|
||
DOM.modalConfirm.onclick = async () => {
|
||
const fileInput = document.getElementById('pluginFile');
|
||
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
||
showNotification('Выберите файл плагина', 'warning');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
formData.append('plugin', fileInput.files[0]);
|
||
|
||
try {
|
||
const response = await fetch('/api/webui/plugin/upload', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
DOM.modal.classList.remove('show');
|
||
showNotification('Плагин загружен', 'success');
|
||
loadPluginsList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка загрузки плагина', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Запускает плагин
|
||
* @param {string} pluginName - Имя плагина
|
||
*/
|
||
window.startPlugin = async function(pluginName) {
|
||
try {
|
||
const response = await fetch(`/api/webui/plugin/${pluginName}/start`, { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification(`Плагин ${pluginName} запущен`, 'success');
|
||
loadPluginsList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка запуска', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Останавливает плагин
|
||
* @param {string} pluginName - Имя плагина
|
||
*/
|
||
window.stopPlugin = async function(pluginName) {
|
||
try {
|
||
const response = await fetch(`/api/webui/plugin/${pluginName}/stop`, { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification(`Плагин ${pluginName} остановлен`, 'success');
|
||
loadPluginsList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка остановки', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Удаляет плагин
|
||
* @param {string} pluginName - Имя плагина
|
||
*/
|
||
window.deletePlugin = async function(pluginName) {
|
||
if (!confirm(`Удалить плагин "${pluginName}"?`)) return;
|
||
try {
|
||
const response = await fetch(`/api/webui/plugin/${pluginName}/delete`, { method: 'DELETE' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification(`Плагин ${pluginName} удалён`, 'success');
|
||
loadPluginsList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка удаления', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
|
||
// ======================== ОСТАЛЬНЫЕ СЕКЦИИ ========================
|
||
|
||
/**
|
||
* Загружает страницу управления кластером
|
||
*/
|
||
async function loadClusterManagement() {
|
||
DOM.title.textContent = 'Управление кластером';
|
||
DOM.content.innerHTML = '<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();
|
||
|
||
DOM.content.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) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки информации о кластере</div>';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Загружает страницу настроек
|
||
*/
|
||
function loadSettings() {
|
||
DOM.title.textContent = 'Настройки';
|
||
DOM.content.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>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Сохраняет настройки интерфейса
|
||
*/
|
||
function saveSettings() {
|
||
const theme = document.getElementById('themeSelect')?.value;
|
||
if (theme) {
|
||
localStorage.setItem('theme', theme);
|
||
showNotification('Настройки сохранены', 'success');
|
||
}
|
||
}
|
||
|
||
// ======================== ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ========================
|
||
|
||
/**
|
||
* Экранирует HTML-спецсимволы для предотвращения XSS
|
||
* @param {any} str - Входная строка
|
||
* @returns {string} Экранированная строка
|
||
*/
|
||
function escapeHtml(str) {
|
||
if (!str) return '';
|
||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
/**
|
||
* Отображает всплывающее уведомление
|
||
* @param {string} message - Текст уведомления
|
||
* @param {string} type - Тип уведомления (success, error, warning, info)
|
||
*/
|
||
function showNotification(message, type = 'info') {
|
||
const container = DOM.notificationContainer || document.getElementById('notificationContainer');
|
||
if (!container) return;
|
||
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
|
||
notification.innerHTML = `${icons[type] || icons.info} ${escapeHtml(message)}`;
|
||
container.appendChild(notification);
|
||
setTimeout(() => {
|
||
notification.style.animation = 'slideOutRight 0.3s ease';
|
||
setTimeout(() => notification.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
/**
|
||
* Обрабатывает быстрые действия из меню
|
||
* @param {string} action - Идентификатор действия
|
||
*/
|
||
function handleAction(action) {
|
||
const actions = {
|
||
'create-db': showCreateDatabaseModal,
|
||
'create-collection': showCreateCollectionModal,
|
||
'insert-doc': showInsertDocumentModal,
|
||
'update-doc': () => showUpdateDocumentModal(),
|
||
'acl-create-user': showCreateUserModal,
|
||
'acl-create-role': showCreateRoleModal,
|
||
'tx-start-session': startSession,
|
||
'tx-start': startTransaction,
|
||
'tx-commit': commitTransaction,
|
||
'tx-abort': abortTransaction,
|
||
'index-create': () => {
|
||
if (document.getElementById('indexDbSelect')?.value && document.getElementById('indexCollSelect')?.value) {
|
||
showCreateIndexModal();
|
||
} else {
|
||
showNotification('Сначала выберите БД и коллекцию на странице индексов', 'warning');
|
||
}
|
||
},
|
||
'plugin-upload': showUploadPluginModal,
|
||
'constraint-add-required': () => showConstraintModal('required'),
|
||
'constraint-add-unique': () => showConstraintModal('unique'),
|
||
'constraint-add-min': () => showConstraintModal('min'),
|
||
'constraint-add-max': () => showConstraintModal('max'),
|
||
'constraint-add-enum': () => showConstraintModal('enum'),
|
||
'constraint-add-regex': () => showConstraintModal('regex')
|
||
};
|
||
|
||
const handler = actions[action];
|
||
if (handler) {
|
||
handler();
|
||
} else {
|
||
showNotification('Неизвестное действие', 'warning');
|
||
}
|
||
}
|
||
|
||
// ======================== МОДАЛЬНЫЕ ОКНА ДЛЯ CRUD ========================
|
||
|
||
/**
|
||
* Показывает модальное окно создания базы данных
|
||
*/
|
||
function showCreateDatabaseModal() {
|
||
DOM.modalTitle.textContent = 'Создать базу данных';
|
||
DOM.modalBody.innerHTML = `<div class="form-group"><label>Имя базы данных</label><input type="text" id="dbName" class="form-control" placeholder="my_database"></div>`;
|
||
DOM.modalConfirm.textContent = 'Создать';
|
||
DOM.modal.classList.add('show');
|
||
|
||
DOM.modalConfirm.onclick = async () => {
|
||
const dbName = document.getElementById('dbName').value;
|
||
if (!dbName) {
|
||
showNotification('Введите имя базы данных', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
const response = await fetch('/api/db/' + dbName, { method: 'POST' });
|
||
if (response.ok) {
|
||
DOM.modal.classList.remove('show');
|
||
showNotification(`База данных "${dbName}" создана`, 'success');
|
||
loadDashboard();
|
||
} else {
|
||
const error = await response.json();
|
||
showNotification(error.error || 'Ошибка создания БД', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Показывает модальное окно создания коллекции
|
||
*/
|
||
function showCreateCollectionModal() {
|
||
if (!currentDatabase) {
|
||
showNotification('Сначала выберите базу данных', 'warning');
|
||
return;
|
||
}
|
||
DOM.modalTitle.textContent = 'Создать коллекцию';
|
||
DOM.modalBody.innerHTML = `
|
||
<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" id="collName" class="form-control" placeholder="my_collection"></div>
|
||
`;
|
||
DOM.modalConfirm.textContent = 'Создать';
|
||
DOM.modal.classList.add('show');
|
||
|
||
DOM.modalConfirm.onclick = async () => {
|
||
const collName = document.getElementById('collName').value;
|
||
if (!collName) {
|
||
showNotification('Введите имя коллекции', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
const response = await fetch(`/api/db/${currentDatabase}/${collName}`, { method: 'POST' });
|
||
if (response.ok) {
|
||
DOM.modal.classList.remove('show');
|
||
showNotification(`Коллекция "${collName}" создана`, 'success');
|
||
viewDatabase(currentDatabase);
|
||
} else {
|
||
const error = await response.json();
|
||
showNotification(error.error || 'Ошибка создания коллекции', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Показывает модальное окно вставки документа
|
||
*/
|
||
function showInsertDocumentModal() {
|
||
if (!currentDatabase || !currentCollection) {
|
||
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
|
||
return;
|
||
}
|
||
DOM.modalTitle.textContent = 'Вставить документ';
|
||
DOM.modalBody.innerHTML = `
|
||
<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>Данные документа (JSON)</label><textarea id="docData" class="form-control" rows="8" placeholder='{"name": "Example", "value": 123}'></textarea></div>
|
||
`;
|
||
DOM.modalConfirm.textContent = 'Вставить';
|
||
DOM.modal.classList.add('show');
|
||
|
||
DOM.modalConfirm.onclick = async () => {
|
||
const docData = document.getElementById('docData').value;
|
||
if (!docData) {
|
||
showNotification('Введите данные документа', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
const data = JSON.parse(docData);
|
||
const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
DOM.modal.classList.remove('show');
|
||
showNotification('Документ вставлен', 'success');
|
||
viewCollection(currentDatabase, currentCollection);
|
||
} else {
|
||
showNotification(result.error || 'Ошибка вставки документа', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Показывает модальное окно обновления документа
|
||
* @param {string} docId - ID документа
|
||
* @param {Object} currentFields - Текущие поля документа
|
||
*/
|
||
function showUpdateDocumentModal(docId = '', currentFields = null) {
|
||
if (!currentDatabase || !currentCollection) {
|
||
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
|
||
return;
|
||
}
|
||
DOM.modalTitle.textContent = 'Обновить документ';
|
||
DOM.modalBody.innerHTML = `
|
||
<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>ID документа</label><input type="text" id="updateDocId" class="form-control" value="${escapeHtml(docId)}" ${docId ? 'disabled' : ''} placeholder="document_id"></div>
|
||
<div class="form-group"><label>Обновления (JSON)</label><textarea id="updateData" class="form-control" rows="8" placeholder='{"field1": "new value"}'>${escapeHtml(currentFields ? JSON.stringify(currentFields, null, 2) : '')}</textarea></div>
|
||
`;
|
||
DOM.modalConfirm.textContent = 'Обновить';
|
||
DOM.modal.classList.add('show');
|
||
|
||
DOM.modalConfirm.onclick = async () => {
|
||
const updateDocId = document.getElementById('updateDocId').value;
|
||
const updateData = document.getElementById('updateData').value;
|
||
if (!updateDocId) {
|
||
showNotification('Введите ID документа', 'error');
|
||
return;
|
||
}
|
||
if (!updateData) {
|
||
showNotification('Введите данные для обновления', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
const data = JSON.parse(updateData);
|
||
const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(updateDocId)}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
if (result.success) {
|
||
DOM.modal.classList.remove('show');
|
||
showNotification('Документ обновлён', 'success');
|
||
viewCollection(currentDatabase, currentCollection);
|
||
} else {
|
||
showNotification(result.error || 'Ошибка обновления документа', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification(error instanceof SyntaxError ? 'Неверный формат JSON' : 'Ошибка подключения', 'error');
|
||
}
|
||
};
|
||
}
|
||
|
||
// ======================== ТРАНЗАКЦИИ ========================
|
||
|
||
/**
|
||
* Загружает и отображает список активных транзакций
|
||
*/
|
||
async function loadTransactionList() {
|
||
DOM.title.textContent = 'Активные транзакции';
|
||
DOM.content.innerHTML = '<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) {
|
||
DOM.content.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>
|
||
`;
|
||
} else {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка загрузки транзакций</div>';
|
||
}
|
||
} catch (error) {
|
||
DOM.content.innerHTML = '<div class="error-message">Ошибка подключения</div>';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Загружает детали транзакции
|
||
* @param {string} txId - ID транзакции
|
||
*/
|
||
async function loadTransactionDetails(txId) {
|
||
DOM.modalTitle.textContent = `Детали транзакции ${txId}`;
|
||
DOM.modalConfirm.textContent = 'Закрыть';
|
||
DOM.modalBody.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка деталей...</p></div>';
|
||
DOM.modal.classList.add('show');
|
||
|
||
const originalConfirmHandler = DOM.modalConfirm.onclick;
|
||
DOM.modalConfirm.onclick = () => {
|
||
DOM.modal.classList.remove('show');
|
||
DOM.modalConfirm.onclick = originalConfirmHandler;
|
||
};
|
||
|
||
try {
|
||
const response = await fetch(`/api/webui/transaction/${txId}/details`);
|
||
const data = await response.json();
|
||
if (data.success && data.data) {
|
||
const tx = data.data;
|
||
DOM.modalBody.innerHTML = `<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>Операции</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 {
|
||
DOM.modalBody.innerHTML = `<div class="error-message">Ошибка загрузки деталей</div>`;
|
||
}
|
||
} catch (error) {
|
||
DOM.modalBody.innerHTML = '<div class="error-message">Ошибка подключения</div>';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Начинает новую сессию транзакций
|
||
*/
|
||
async function startSession() {
|
||
try {
|
||
const response = await fetch('/api/webui/transactions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'start_session' })
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification('Сессия начата', 'success');
|
||
loadTransactionList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Начинает новую транзакцию
|
||
*/
|
||
async function startTransaction() {
|
||
try {
|
||
const response = await fetch('/api/webui/transactions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ action: 'start_transaction' })
|
||
});
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification('Транзакция начата', 'success');
|
||
loadTransactionList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Фиксирует текущую транзакцию
|
||
*/
|
||
async function commitTransaction() {
|
||
try {
|
||
const response = await fetch('/api/webui/transaction/commit', { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification('Транзакция зафиксирована', 'success');
|
||
loadTransactionList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отменяет текущую транзакцию
|
||
*/
|
||
async function abortTransaction() {
|
||
try {
|
||
const response = await fetch('/api/webui/transaction/abort', { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification('Транзакция отменена', 'success');
|
||
loadTransactionList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Фиксирует транзакцию по ID
|
||
* @param {string} txId - ID транзакции
|
||
*/
|
||
async function commitTransactionById(txId) {
|
||
if (!confirm(`Зафиксировать транзакцию ${txId}?`)) return;
|
||
try {
|
||
const response = await fetch(`/api/webui/transaction/${txId}/commit`, { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification(`Транзакция ${txId} зафиксирована`, 'success');
|
||
loadTransactionList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка фиксации', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Отменяет транзакцию по ID
|
||
* @param {string} txId - ID транзакции
|
||
*/
|
||
async function abortTransactionById(txId) {
|
||
if (!confirm(`Отменить транзакцию ${txId}?`)) return;
|
||
try {
|
||
const response = await fetch(`/api/webui/transaction/${txId}/abort`, { method: 'POST' });
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
showNotification(`Транзакция ${txId} отменена`, 'success');
|
||
loadTransactionList();
|
||
} else {
|
||
showNotification(data.error || 'Ошибка отмены', 'error');
|
||
}
|
||
} catch (error) {
|
||
showNotification('Ошибка подключения', 'error');
|
||
}
|
||
}
|
||
|
||
// ======================== ОСТАЛЬНЫЕ ФУНКЦИИ (ЗАГЛУШКИ ДЛЯ ПОЛНОТЫ) ========================
|
||
|
||
// ACL функции
|
||
async function loadACLUsers() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
async function loadACLRoles() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
async function loadACLPermissions() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
|
||
// Индексы
|
||
async function loadIndexesList() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
|
||
// Импорт/Экспорт
|
||
async function loadExportPage() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
async function loadImportPage() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
|
||
// Ограничения
|
||
async function loadConstraintsList() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
|
||
// Триггеры
|
||
async function loadTriggersList() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
async function loadTriggerLog() { DOM.content.innerHTML = '<div class="info-message">Раздел в разработке</div>'; }
|
||
|
||
// Модальные окна для ACL и ограничений
|
||
function showCreateUserModal() { showNotification('Функция в разработке', 'info'); }
|
||
function showCreateRoleModal() { showNotification('Функция в разработке', 'info'); }
|
||
function showCreateIndexModal() { showNotification('Функция в разработке', 'info'); }
|
||
function showConstraintModal(type) { showNotification(`Добавление ограничения "${type}" в разработке`, 'info'); }
|