1922 lines
108 KiB
JavaScript
1922 lines
108 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
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @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">></option><option value="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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @description Отображает всплывающее уведомление.
|
|||
|
|
* @param {string} message - Текст уведомления
|
|||
|
|
* @param {string} type - Тип уведомления (success, error, warning, info)
|
|||
|
|
*/
|
|||
|
|
function showNotification(message, type = 'info') {
|
|||
|
|
const container = document.getElementById('notificationContainer');
|
|||
|
|
const notification = document.createElement('div');
|
|||
|
|
notification.className = `notification ${type}`;
|
|||
|
|
const icons = { success: '<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'); }
|
|||
|
|
};
|
|||
|
|
}
|