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

3252 lines
140 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/api/static/app.js
// JavaScript для веб-интерфейса Futriis DB Dashboard
// Глобальное состояние
let currentSession = null;
let currentDatabase = null;
let currentCollection = null;
let currentUser = null;
// DOM элементы
const contentArea = document.getElementById('contentArea');
const pageTitle = document.getElementById('pageTitle');
const connectionStatus = document.getElementById('connectionStatus');
const userInfoSpan = document.querySelector('#userInfo span');
const logoutBtn = document.getElementById('logoutBtn');
const menuToggle = document.getElementById('menuToggle');
const sidebar = document.querySelector('.sidebar');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalConfirm = document.getElementById('modalConfirm');
const modalCloseBtns = document.querySelectorAll('.modal-close');
// Инициализация приложения
document.addEventListener('DOMContentLoaded', () => {
checkSession();
initNavigation();
initEventListeners();
});
// Проверка сессии
async function checkSession() {
try {
const response = await fetch('/api/webui/session');
const data = await response.json();
if (data.success && data.data.authenticated) {
currentUser = data.data.username;
userInfoSpan.textContent = currentUser;
if (data.data.connection_status === 'connected') {
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = '<span>СУБД подключена</span>';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
loadDashboard();
} else {
showLoginModal();
}
} catch (error) {
console.error('Session check failed:', error);
showLoginModal();
}
}
// Функция для проверки статуса подключения
function startConnectionStatusMonitor() {
setInterval(async () => {
if (currentUser) {
try {
const response = await fetch('/api/webui/session');
const data = await response.json();
if (data.success && data.data.connection_status === 'connected') {
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = '<span>СУБД подключена</span>';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
} catch (error) {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
}
}, 5000);
}
// Показать модальное окно входа
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;
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);
}
// Инициализация навигации
function initNavigation() {
document.querySelectorAll('.nav-link[data-section]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const section = link.dataset.section;
loadSection(section);
setActiveNav(link);
});
});
document.querySelectorAll('[data-action]').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const action = item.dataset.action;
handleCrudAction(action);
});
});
document.querySelectorAll('.has-submenu > .nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const parent = link.closest('.has-submenu');
parent.classList.toggle('open');
});
});
}
// Инициализация обработчиков событий
function initEventListeners() {
logoutBtn.addEventListener('click', async () => {
await fetch('/api/webui/logout', { method: 'POST' });
currentSession = null;
currentUser = null;
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
showLoginModal();
});
if (menuToggle) {
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
}
modalCloseBtns.forEach(btn => {
btn.addEventListener('click', () => {
modal.classList.remove('show');
});
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
}
});
}
// Загрузка секции
async function loadSection(section) {
switch(section) {
case 'dashboard':
loadDashboard();
break;
case 'cluster':
loadClusterManagement();
break;
case 'audit':
loadAuditLog();
break;
case 'settings':
loadSettings();
break;
case 'acl-users':
loadACLUsers();
break;
case 'acl-roles':
loadACLRoles();
break;
case 'acl-permissions':
loadACLPermissions();
break;
case 'tx-list':
loadTransactionList();
break;
case 'indexes-list':
loadIndexesList();
break;
case 'export-data':
loadExportPage();
break;
case 'import-data':
loadImportPage();
break;
default:
loadDashboard();
}
}
// Загрузка дашборда
async function loadDashboard() {
pageTitle.textContent = 'Панель управления';
contentArea.innerHTML = '<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 style="margin-bottom: 16px;">Базы данных</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');
}
}
// Просмотр базы данных
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>';
}
};
// Просмотр коллекции
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; flex-wrap: wrap;">
<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 style="margin-bottom: 16px;">Документы (${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 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 style="margin-bottom: 16px;">Узлы кластера</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>';
}
}
// Загрузка лога аудита
async function loadAuditLog() {
pageTitle.textContent = 'Лог аудита';
contentArea.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-pulse"></i><p>Загрузка лога аудита...</p></div>';
contentArea.innerHTML = '<div class="info-message">Функция в разработке</div>';
}
// Загрузка настроек
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 Functions ====================
// Загрузка списка пользователей
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 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 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>';
}
}
// Показать модальное окно создания пользователя
function showCreateUserModal() {
modalTitle.textContent = 'Создать пользователя';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" id="username" class="form-control" placeholder="username">
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" id="password" class="form-control" placeholder="password">
</div>
<div class="form-group">
<label for="roles">Роли (через запятую)</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');
}
};
}
// Показать модальное окно редактирования пользователя
function showEditUserModal(username, currentRoles) {
modalTitle.textContent = `Редактировать пользователя: ${username}`;
modalConfirm.textContent = 'Сохранить';
modalBody.innerHTML = `
<div class="form-group">
<label>Новый пароль (оставьте пустым для сохранения текущего)</label>
<input type="password" id="newPassword" class="form-control" placeholder="Новый пароль">
</div>
<div class="form-group">
<label>Добавить роль</label>
<input type="text" id="addRole" class="form-control" placeholder="admin">
</div>
<div class="form-group">
<label>Удалить роль</label>
<input type="text" id="removeRole" class="form-control" placeholder="guest">
</div>
<div class="form-group">
<label>Текущие роли: ${currentRoles.join(', ') || 'нет'}</label>
</div>
<div class="form-group">
<button class="btn btn-sm btn-warning" onclick="disableUser('${username}')" style="margin-right: 8px;">Отключить пользователя</button>
<button class="btn btn-sm btn-success" onclick="enableUser('${username}')">Включить пользователя</button>
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const newPassword = document.getElementById('newPassword').value;
const addRole = document.getElementById('addRole').value;
const removeRole = document.getElementById('removeRole').value;
const updates = {};
if (newPassword) updates.password = newPassword;
if (addRole) updates.add_role = addRole;
if (removeRole) updates.remove_role = removeRole;
if (Object.keys(updates).length === 0) {
showNotification('Нет изменений для сохранения', 'warning');
return;
}
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Пользователь ${username} обновлён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка обновления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно создания роли
function showCreateRoleModal() {
modalTitle.textContent = 'Создать роль';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
<div class="form-group">
<label for="roleName">Название роли</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');
}
};
}
// Показать модальное окно редактирования роли
function showEditRoleModal(roleName, currentPermissions) {
modalTitle.textContent = `Редактировать роль: ${roleName}`;
modalConfirm.textContent = 'Добавить разрешение';
modalBody.innerHTML = `
<div class="form-group">
<label>Текущие разрешения:</label>
<div style="background: var(--bg-dark); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
${currentPermissions.map(p => `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<code>${escapeHtml(p)}</code>
<button class="btn btn-sm btn-danger" onclick="revokePermission('${roleName}', '${escapeHtml(p)}')">Отозвать</button>
</div>
`).join('') || '<em>Нет разрешений</em>'}
</div>
</div>
<div class="form-group">
<label>Добавить разрешение</label>
<input type="text" id="newPermission" class="form-control" placeholder="database.collection:read">
<small style="color: var(--text-secondary);">Формат: database.collection:read|write|delete|admin (можно использовать * как wildcard)</small>
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const permission = document.getElementById('newPermission').value;
if (!permission) {
showNotification('Введите разрешение', 'error');
return;
}
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/grant/${encodeURIComponent(permission)}`, {
method: 'PUT'
});
const data = await response.json();
if (data.success) {
showNotification(`Разрешение ${permission} добавлено`, 'success');
modal.classList.remove('show');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка добавления разрешения', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// ==================== Transaction Functions ====================
// Загрузка списка транзакций
async function loadTransactionList() {
pageTitle.textContent = 'Активные транзакции';
contentArea.innerHTML = '<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>
</div>
<div class="data-table">
<h3>Активные транзакции</h3>
<table>
<thead>
<tr><th>ID транзакции</th><th>Статус</th><th>Начало</th><th>Операций</th></tr>
</thead>
<tbody>
${data.data.map(tx => `
<tr>
<td><code>${escapeHtml(tx.id)}</code></td>
<td>${escapeHtml(tx.status)}</td>
<td>${new Date(tx.start_time).toLocaleString()}</td>
<td>${tx.operation_count || 0}</td>
</tr>
`).join('') || '<tr><td colspan="4">Нет активных транзакций</td></tr>'}
</tbody>
</table>
</div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки транзакций</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
}
// ==================== Index Functions ====================
// Загрузка списка индексов
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');
}
}
// Загрузка коллекций для выбранной БД
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 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>';
}
}
// ==================== Additional features for transactions ====================
// Загрузка деталей транзакции
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');
try {
const response = await fetch(`/api/webui/transaction/${txId}/details`);
const data = await response.json();
if (data.success && data.data) {
const tx = data.data;
let operationsHtml = '';
if (tx.operations && tx.operations.length > 0) {
operationsHtml = `
<h4>Операции (${tx.operations.length})</h4>
<table class="data-table" style="width: 100%; margin-top: 12px;">
<thead>
<tr><th>Тип</th><th>БД</th><th>Коллекция</th><th>ID документа</th></tr>
</thead>
<tbody>
${tx.operations.map(op => `
<tr>
<td><span class="badge badge-${op.type}">${escapeHtml(op.type)}</span></td>
<td>${escapeHtml(op.database)}</td>
<td>${escapeHtml(op.collection)}</td>
<td><code>${escapeHtml(op.document_id)}</code></td>
</tr>
`).join('')}
</tbody>
</table>
`;
} else {
operationsHtml = '<p>Нет операций в этой транзакции</p>';
}
modalBody.innerHTML = `
<div class="transaction-details">
<div class="info-row">
<strong>ID:</strong> <code>${escapeHtml(tx.id)}</code>
</div>
<div class="info-row">
<strong>Статус:</strong>
<span class="status-badge status-${tx.status}">${escapeHtml(tx.status)}</span>
</div>
<div class="info-row">
<strong>Время начала:</strong> ${new Date(tx.start_time).toLocaleString()}
</div>
<div class="info-row">
<strong>Количество операций:</strong> ${tx.operation_count}
</div>
<hr>
${operationsHtml}
</div>
`;
} else {
modalBody.innerHTML = `<div class="error-message">Ошибка загрузки деталей: ${data.error || 'Неизвестная ошибка'}</div>`;
}
} catch (error) {
modalBody.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
// Переопределяем обработчик кнопки подтверждения для закрытия
const originalConfirmHandler = modalConfirm.onclick;
modalConfirm.onclick = () => {
modal.classList.remove('show');
modalConfirm.onclick = originalConfirmHandler;
};
}
// Обновлённая функция loadTransactionList с детализацией
const originalLoadTransactionList = window.loadTransactionList;
window.loadTransactionList = async function() {
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="refreshTransactionList()">
<i class="fas fa-sync-alt"></i> Обновить
</button>
</div>
<div class="data-table">
<h3>Транзакции</h3>
<table class="data-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 (Multi-Version Concurrency Control) для изоляции</li>
<li>WAL (Write-Ahead Logging) гарантирует持久化 операций</li>
<li>При сбое системы незавершённые транзакции автоматически откатываются</li>
</ul>
</div>
`;
} else {
contentArea.innerHTML = '<div class="error-message">Ошибка загрузки транзакций</div>';
}
} catch (error) {
contentArea.innerHTML = '<div class="error-message">Ошибка подключения</div>';
}
};
// Функция обновления списка транзакций
function refreshTransactionList() {
loadTransactionList();
}
// Функция коммита транзакции по ID
async function commitTransactionById(txId) {
if (confirm(`Зафиксировать транзакцию ${txId}?`)) {
try {
const response = await fetch(`/api/webui/transaction/${txId}/commit`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification(`Транзакция ${txId} зафиксирована`, 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка фиксации', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
}
// Функция отката транзакции по ID
async function abortTransactionById(txId) {
if (confirm(`Отменить транзакцию ${txId}?`)) {
try {
const response = await fetch(`/api/webui/transaction/${txId}/abort`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification(`Транзакция ${txId} отменена`, 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка отмены', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
}
// ==================== Import/Export Functions ====================
// Загрузка страницы экспорта
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 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) {
// Создаём JSON файл для скачивания
const exportData = data.data.data;
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = data.data.filename.replace('.msgpack', '.json');
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
exportResult.innerHTML = `
<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 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 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 Action Handlers ====================
// Обработка CRUD действий
function handleCrudAction(action) {
switch(action) {
case 'create-db':
showCreateDatabaseModal();
break;
case 'create-collection':
showCreateCollectionModal();
break;
case 'insert-doc':
showInsertDocumentModal();
break;
case 'find-doc':
showFindDocumentModal();
break;
case 'update-doc':
showUpdateDocumentModal();
break;
case 'delete-doc':
showDeleteDocumentModal();
break;
case 'acl-create-user':
showCreateUserModal();
break;
case 'acl-create-role':
showCreateRoleModal();
break;
case 'tx-start-session':
startSession();
break;
case 'tx-start':
startTransaction();
break;
case 'tx-commit':
commitTransaction();
break;
case 'tx-abort':
abortTransaction();
break;
case 'index-create':
showCreateIndexModal();
break;
}
}
// Показать модальное окно создания БД
function showCreateDatabaseModal() {
modalTitle.textContent = 'Создать базу данных';
modalConfirm.textContent = 'Подтвердить';
modalBody.innerHTML = `
<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');
}
};
}
// Показать модальное окно создания коллекции
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');
}
};
}
// Показать модальное окно вставки документа
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) {
if (error instanceof SyntaxError) {
showNotification('Неверный формат JSON', 'error');
} else {
showNotification('Ошибка подключения', 'error');
}
}
};
}
// Показать модальное окно поиска документа
function showFindDocumentModal() {
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="docId">ID документа</label>
<input type="text" id="docId" class="form-control" placeholder="document_id">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const docId = document.getElementById('docId').value;
if (!docId) {
showNotification('Введите ID документа', 'error');
return;
}
try {
const response = await fetch(`/api/db/${currentDatabase}/${currentCollection}/${docId}`);
if (response.ok) {
const data = await response.json();
modal.classList.remove('show');
contentArea.innerHTML = `
<div class="data-table">
<h3>Результат поиска</h3>
<pre style="background: var(--bg-dark); padding: 16px; border-radius: 8px; overflow-x: auto;">
${escapeHtml(JSON.stringify(data.data, null, 2))}
</pre>
<button class="btn btn-secondary" onclick="viewCollection('${escapeHtml(currentDatabase)}', '${escapeHtml(currentCollection)}')">
<i class="fas fa-arrow-left"></i> Назад
</button>
</div>
`;
} else {
const error = await response.json();
showNotification(error.error || 'Документ не найден', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно обновления документа
function showUpdateDocumentModal(docId, currentFields = null) {
if (!currentDatabase || !currentCollection) {
showNotification('Сначала выберите базу данных и коллекцию', 'warning');
return;
}
const fieldsJson = currentFields ? JSON.stringify(currentFields, null, 2) : '';
modalTitle.textContent = 'Обновить документ';
modalConfirm.textContent = 'Обновить';
modalBody.innerHTML = `
<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(fieldsJson)}</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) {
if (error instanceof SyntaxError) {
showNotification('Неверный формат JSON', 'error');
} else {
showNotification('Ошибка подключения', 'error');
}
}
};
}
// Показать модальное окно удаления документа
function showDeleteDocumentModal() {
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="deleteDocId">ID документа</label>
<input type="text" id="deleteDocId" class="form-control" placeholder="document_id">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const docId = document.getElementById('deleteDocId').value;
if (!docId) {
showNotification('Введите ID документа', 'error');
return;
}
try {
const response = await fetch(`/api/webui/documents/${currentDatabase}/${currentCollection}?id=${encodeURIComponent(docId)}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
modal.classList.remove('show');
showNotification('Документ удалён', 'success');
viewCollection(currentDatabase, currentCollection);
} else {
showNotification(result.error || 'Ошибка удаления документа', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно создания индекса
function showCreateIndexModal() {
const dbName = document.getElementById('indexDbSelect')?.value;
const collName = document.getElementById('indexCollSelect')?.value;
if (!dbName || !collName) {
showNotification('Сначала выберите базу данных и коллекцию на странице "Список индексов"', 'warning');
return;
}
modalTitle.textContent = 'Создать индекс';
modalConfirm.textContent = 'Создать';
modalBody.innerHTML = `
<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 for="indexName">Имя индекса</label>
<input type="text" id="indexName" class="form-control" placeholder="my_index">
</div>
<div class="form-group">
<label for="indexFields">Поля (через запятую)</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');
}
};
}
// ==================== Вспомогательные функции ====================
// Удаление коллекции
window.deleteCollection = async function(dbName, collName) {
if (confirm(`Вы уверены, что хотите удалить коллекцию "${collName}"? Это действие необратимо.`)) {
try {
const response = await fetch(`/api/db/${dbName}/${collName}`, {
method: 'DELETE'
});
if (response.ok) {
showNotification(`Коллекция "${collName}" удалена`, 'success');
viewDatabase(dbName);
} else {
const error = await response.json();
showNotification(error.error || 'Ошибка удаления коллекции', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Удаление документа
window.deleteDocument = async function(dbName, collName, docId) {
if (confirm(`Вы уверены, что хотите удалить документ "${docId}"?`)) {
try {
const response = await fetch(`/api/webui/documents/${dbName}/${collName}?id=${encodeURIComponent(docId)}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showNotification('Документ удалён', 'success');
viewCollection(dbName, collName);
} else {
showNotification(result.error || 'Ошибка удаления документа', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Удаление пользователя
window.deleteUser = async function(username) {
if (confirm(`Удалить пользователя "${username}"?`)) {
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Пользователь ${username} удалён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Удаление роли
window.deleteRole = async function(roleName) {
if (confirm(`Удалить роль "${roleName}"?`)) {
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Роль ${roleName} удалена`, 'success');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Отключение пользователя
window.disableUser = async function(username) {
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ disable: true })
});
const data = await response.json();
if (data.success) {
showNotification(`Пользователь ${username} отключён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Включение пользователя
window.enableUser = async function(username) {
try {
const response = await fetch(`/api/webui/acl/user/${encodeURIComponent(username)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enable: true })
});
const data = await response.json();
if (data.success) {
showNotification(`Пользователь ${username} включён`, 'success');
loadACLUsers();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Отзыв разрешения у роли
window.revokePermission = async function(roleName, permission) {
try {
const response = await fetch(`/api/webui/acl/role/${encodeURIComponent(roleName)}/revoke/${encodeURIComponent(permission)}`, {
method: 'PUT'
});
const data = await response.json();
if (data.success) {
showNotification(`Разрешение отозвано`, 'success');
loadACLRoles();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Удаление индекса
window.dropIndex = async function(dbName, collName, indexName) {
if (confirm(`Удалить индекс "${indexName}"?`)) {
try {
const response = await fetch(`/api/webui/index/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/drop/${encodeURIComponent(indexName)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification(`Индекс ${indexName} удалён`, 'success');
loadIndexesForCollection();
} else {
showNotification(data.error || 'Ошибка удаления индекса', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Функции для транзакций
window.startSession = async function() {
try {
const response = await fetch('/api/webui/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'start_session' })
});
const data = await response.json();
if (data.success) {
showNotification('Сессия начата', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
window.startTransaction = async function() {
try {
const response = await fetch('/api/webui/transactions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'start_transaction' })
});
const data = await response.json();
if (data.success) {
showNotification('Транзакция начата', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
window.commitTransaction = async function() {
try {
const response = await fetch('/api/webui/transaction/commit', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Транзакция зафиксирована', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
window.abortTransaction = async function() {
try {
const response = await fetch('/api/webui/transaction/abort', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Транзакция отменена', 'success');
loadTransactionList();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Сохранение настроек
function saveSettings() {
const theme = document.getElementById('themeSelect')?.value;
if (theme) {
localStorage.setItem('theme', theme);
showNotification('Настройки сохранены', 'success');
}
}
// Утилиты
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
let icon = '';
switch(type) {
case 'success': icon = '<i class="fas fa-check-circle"></i>'; break;
case 'error': icon = '<i class="fas fa-exclamation-circle"></i>'; break;
case 'warning': icon = '<i class="fas fa-exclamation-triangle"></i>'; break;
default: icon = '<i class="fas fa-info-circle"></i>';
}
notification.innerHTML = `${icon}<span>${escapeHtml(message)}</span>`;
container.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
function setActiveNav(activeLink) {
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
activeLink.classList.add('active');
}
// ==================== Trigger Functions ====================
// Загрузка списка триггеров
async function loadTriggersList() {
pageTitle.textContent = 'Управление триггерами';
contentArea.innerHTML = `
<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');
}
}
// Загрузка коллекций для выбранной БД (для триггеров)
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 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>';
}
}
// Показать модальное окно создания триггера
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 for="triggerName">Имя триггера</label>
<input type="text" id="triggerName" class="form-control" placeholder="my_trigger">
</div>
<div class="form-group">
<label for="triggerEvent">Событие</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 for="triggerAction">Действие</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 for="triggerDescription">Описание (опционально)</label>
<input type="text" id="triggerDescription" class="form-control" placeholder="Описание триггера">
</div>
<div class="form-group">
<label>Условие (опционально)</label>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<input type="text" id="conditionField" class="form-control" style="flex: 1;" placeholder="Поле">
<select id="conditionOperator" class="form-control" style="width: 100px;">
<option value="eq">=</option>
<option value="ne">≠</option>
<option value="gt">&gt;</option>
<option value="lt">&lt;</option>
<option value="gte">≥</option>
<option value="lte">≤</option>
<option value="exists">exists</option>
</select>
<input type="text" id="conditionValue" class="form-control" style="flex: 1;" placeholder="Значение">
</div>
</div>
<div class="form-group">
<label>Операции триггера (JSON массив)</label>
<textarea id="triggerOperations" class="form-control" rows="6" placeholder='[
{"type": "set", "field": "updated_at", "value": "$$NOW"},
{"type": "set", "field": "modified_by", "value": "$$USER"}
]'></textarea>
<small style="color: var(--text-secondary);">
Доступные операции: set, unset, inc, mul, rename, currentDate<br>
Специальные значения: $$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: operations
};
if (condition) {
requestBody.condition = condition;
}
try {
const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/create`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Триггер ${triggerName} создан`, 'success');
loadTriggersForCollection();
} else {
showNotification(data.error || 'Ошибка создания триггера', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Включение/отключение триггера
window.toggleTrigger = async function(dbName, collName, triggerName, triggerEvent, enable) {
const action = enable ? 'enable' : 'disable';
try {
const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${action}/${encodeURIComponent(triggerName)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification(`Триггер ${triggerName} ${enable ? 'включён' : 'отключён'}`, 'success');
loadTriggersForCollection();
} else {
showNotification(data.error || 'Ошибка', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
// Удаление триггера
window.deleteTrigger = async function(dbName, collName, triggerName, triggerEvent) {
if (confirm(`Удалить триггер "${triggerName}"?`)) {
try {
const response = await fetch(`/api/webui/trigger/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/delete/${encodeURIComponent(triggerName)}/${encodeURIComponent(triggerEvent)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Триггер ${triggerName} удалён`, 'success');
loadTriggersForCollection();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Загрузка лога выполнения триггеров
async function loadTriggerLog() {
pageTitle.textContent = 'Лог выполнения триггеров';
contentArea.innerHTML = '<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>';
}
}
// Обновляем loadSection для секции триггеров
const originalLoadSectionTriggers = window.loadSection;
window.loadSection = function(section) {
switch(section) {
case 'triggers-list':
loadTriggersList();
break;
case 'trigger-log':
loadTriggerLog();
break;
default:
if (originalLoadSectionTriggers) originalLoadSectionTriggers(section);
}
};
// ==================== Constraints Functions ====================
// Загрузка списка ограничений
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');
}
}
// Загрузка коллекций для выбранной БД (для ограничений)
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 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: []
};
for (const c of constraints) {
if (grouped[c.type]) {
grouped[c.type].push(c);
}
}
let html = `
<h3>Ограничения коллекции ${escapeHtml(dbName)}.${escapeHtml(collName)}</h3>
<div style="margin-top: 20px;">
`;
// Required fields
if (grouped.required.length > 0) {
html += `
<div class="constraint-section">
<h4><i class="fas fa-exclamation-circle"></i> Обязательные поля</h4>
<table class="data-table">
<thead><tr><th>Поле</th><th>Действия</th></tr></thead>
<tbody>
${grouped.required.map(c => `
<tr>
<td><code>${escapeHtml(c.field)}</code></td>
<td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', 'required', '${escapeHtml(c.field)}')">Удалить</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
// Unique constraints
if (grouped.unique.length > 0) {
html += `
<div class="constraint-section">
<h4><i class="fas fa-unique"></i> Уникальные поля</h4>
<table class="data-table">
<thead><tr><th>Поле</th><th>Действия</th></tr></thead>
<tbody>
${grouped.unique.map(c => `
<tr>
<td><code>${escapeHtml(c.field)}</code></td>
<td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', 'unique', '${escapeHtml(c.field)}')">Удалить</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
// Min constraints
if (grouped.min.length > 0) {
html += `
<div class="constraint-section">
<h4><i class="fas fa-greater-than"></i> Минимальные значения</h4>
<table class="data-table">
<thead><tr><th>Поле</th><th>Минимум</th><th>Действия</th></tr></thead>
<tbody>
${grouped.min.map(c => `
<tr>
<td><code>${escapeHtml(c.field)}</code></td>
<td>${c.value}</td>
<td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', 'min', '${escapeHtml(c.field)}')">Удалить</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
// Max constraints
if (grouped.max.length > 0) {
html += `
<div class="constraint-section">
<h4><i class="fas fa-less-than"></i> Максимальные значения</h4>
<table class="data-table">
<thead><tr><th>Поле</th><th>Максимум</th><th>Действия</th></tr></thead>
<tbody>
${grouped.max.map(c => `
<tr>
<td><code>${escapeHtml(c.field)}</code></td>
<td>${c.value}</td>
<td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', 'max', '${escapeHtml(c.field)}')">Удалить</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
// Enum constraints
if (grouped.enum.length > 0) {
html += `
<div class="constraint-section">
<h4><i class="fas fa-list-ul"></i> Перечисления (Enum)</h4>
<table class="data-table">
<thead><tr><th>Поле</th><th>Допустимые значения</th><th>Действия</th></tr></thead>
<tbody>
${grouped.enum.map(c => `
<tr>
<td><code>${escapeHtml(c.field)}</code></td>
<td>${c.values.map(v => `<span class="badge">${escapeHtml(String(v))}</span>`).join(' ')}</td>
<td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', 'enum', '${escapeHtml(c.field)}')">Удалить</button></td>
</tr>
`).join('')}
</tbody>
</table>
</div>
`;
}
// Regex constraints
if (grouped.regex.length > 0) {
html += `
<div class="constraint-section">
<h4><i class="fas fa-code"></i> Регулярные выражения</h4>
<table class="data-table">
<thead><tr><th>Поле</th><th>Шаблон</th><th>Действия</th></tr></thead>
<tbody>
${grouped.regex.map(c => `
<tr>
<td><code>${escapeHtml(c.field)}</code></td>
<td><code>${escapeHtml(c.pattern)}</code></td>
<td><button class="btn btn-sm btn-danger" onclick="removeConstraint('${dbName}', '${collName}', 'regex', '${escapeHtml(c.field)}')">Удалить</button></td>
</tr>
`).join('')}
</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>';
}
}
// Показать модальное окно добавления обязательного поля
function showAddRequiredConstraintModal() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').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 for="requiredField">Имя поля</label>
<input type="text" id="requiredField" class="form-control" placeholder="field_name">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const field = document.getElementById('requiredField').value;
if (!field) {
showNotification('Введите имя поля', 'error');
return;
}
try {
const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/required/${encodeURIComponent(field)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Обязательное поле "${field}" добавлено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка добавления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно добавления уникального поля
function showAddUniqueConstraintModal() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').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 for="uniqueField">Имя поля</label>
<input type="text" id="uniqueField" class="form-control" placeholder="field_name">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const field = document.getElementById('uniqueField').value;
if (!field) {
showNotification('Введите имя поля', 'error');
return;
}
try {
const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/unique/${encodeURIComponent(field)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Уникальное поле "${field}" добавлено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка добавления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно добавления минимального значения
function showAddMinConstraintModal() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').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 for="minField">Имя поля</label>
<input type="text" id="minField" class="form-control" placeholder="field_name">
</div>
<div class="form-group">
<label for="minValue">Минимальное значение</label>
<input type="number" id="minValue" class="form-control" placeholder="0" step="any">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const field = document.getElementById('minField').value;
const value = document.getElementById('minValue').value;
if (!field) {
showNotification('Введите имя поля', 'error');
return;
}
if (value === '') {
showNotification('Введите минимальное значение', 'error');
return;
}
try {
const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/min/${encodeURIComponent(field)}/${encodeURIComponent(value)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Минимальное значение для поля "${field}" добавлено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка добавления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно добавления максимального значения
function showAddMaxConstraintModal() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').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 for="maxField">Имя поля</label>
<input type="text" id="maxField" class="form-control" placeholder="field_name">
</div>
<div class="form-group">
<label for="maxValue">Максимальное значение</label>
<input type="number" id="maxValue" class="form-control" placeholder="100" step="any">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const field = document.getElementById('maxField').value;
const value = document.getElementById('maxValue').value;
if (!field) {
showNotification('Введите имя поля', 'error');
return;
}
if (value === '') {
showNotification('Введите максимальное значение', 'error');
return;
}
try {
const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/max/${encodeURIComponent(field)}/${encodeURIComponent(value)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Максимальное значение для поля "${field}" добавлено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка добавления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно добавления enum ограничения
function showAddEnumConstraintModal() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').value;
if (!dbName || !collName) {
showNotification('Сначала выберите базу данных и коллекцию на странице "Список ограничений"', 'warning');
return;
}
modalTitle.textContent = 'Добавить перечисление (Enum)';
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 for="enumField">Имя поля</label>
<input type="text" id="enumField" class="form-control" placeholder="field_name">
</div>
<div class="form-group">
<label for="enumValues">Допустимые значения (через запятую)</label>
<input type="text" id="enumValues" class="form-control" placeholder="value1, value2, value3">
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const field = document.getElementById('enumField').value;
const valuesStr = document.getElementById('enumValues').value;
if (!field) {
showNotification('Введите имя поля', 'error');
return;
}
if (!valuesStr) {
showNotification('Введите допустимые значения', 'error');
return;
}
const values = valuesStr.split(',').map(v => v.trim());
const url = `/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/enum/${encodeURIComponent(field)}/${values.map(v => encodeURIComponent(v)).join('/')}`;
try {
const response = await fetch(url, { method: 'POST' });
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Перечисление для поля "${field}" добавлено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка добавления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Показать модальное окно добавления regex ограничения
function showAddRegexConstraintModal() {
const dbName = document.getElementById('constraintDbSelect').value;
const collName = document.getElementById('constraintCollSelect').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 for="regexField">Имя поля</label>
<input type="text" id="regexField" class="form-control" placeholder="field_name">
</div>
<div class="form-group">
<label for="regexPattern">Регулярное выражение</label>
<input type="text" id="regexPattern" class="form-control" placeholder="^[a-zA-Z0-9]+$">
<small style="color: var(--text-secondary);">Пример: ^[a-zA-Z]+$ - только буквы, ^\\d+$ - только цифры</small>
</div>
`;
modal.classList.add('show');
modalConfirm.onclick = async () => {
const field = document.getElementById('regexField').value;
const pattern = document.getElementById('regexPattern').value;
if (!field) {
showNotification('Введите имя поля', 'error');
return;
}
if (!pattern) {
showNotification('Введите регулярное выражение', 'error');
return;
}
try {
const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/regex/${encodeURIComponent(field)}/${encodeURIComponent(pattern)}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
modal.classList.remove('show');
showNotification(`Регулярное выражение для поля "${field}" добавлено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка добавления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
};
}
// Удаление ограничения
window.removeConstraint = async function(dbName, collName, constraintType, field) {
let confirmMsg = '';
switch(constraintType) {
case 'required': confirmMsg = `Удалить обязательное поле "${field}"?`; break;
case 'unique': confirmMsg = `Удалить уникальное ограничение для поля "${field}"?`; break;
case 'min': confirmMsg = `Удалить минимальное значение для поля "${field}"?`; break;
case 'max': confirmMsg = `Удалить максимальное значение для поля "${field}"?`; break;
case 'enum': confirmMsg = `Удалить перечисление для поля "${field}"?`; break;
case 'regex': confirmMsg = `Удалить регулярное выражение для поля "${field}"?`; break;
default: confirmMsg = `Удалить ограничение "${field}"?`;
}
if (confirm(confirmMsg)) {
try {
const response = await fetch(`/api/webui/constraint/${encodeURIComponent(dbName)}/${encodeURIComponent(collName)}/${constraintType}/${encodeURIComponent(field)}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
showNotification(`Ограничение удалено`, 'success');
loadConstraintsForCollection();
} else {
showNotification(data.error || 'Ошибка удаления', 'error');
}
} catch (error) {
showNotification('Ошибка подключения', 'error');
}
}
};
// Обновляем функцию handleCrudAction для обработки действий с ограничениями
const originalHandleCrudAction = window.handleCrudAction;
window.handleCrudAction = function(action) {
switch(action) {
case 'constraint-add-required':
showAddRequiredConstraintModal();
break;
case 'constraint-add-unique':
showAddUniqueConstraintModal();
break;
case 'constraint-add-min':
showAddMinConstraintModal();
break;
case 'constraint-add-max':
showAddMaxConstraintModal();
break;
case 'constraint-add-enum':
showAddEnumConstraintModal();
break;
case 'constraint-add-regex':
showAddRegexConstraintModal();
break;
default:
if (originalHandleCrudAction) originalHandleCrudAction(action);
}
};
// Обновляем функцию loadSection для секции ограничений
const originalLoadSectionConstraints = window.loadSection;
window.loadSection = function(section) {
switch(section) {
case 'constraints-list':
loadConstraintsList();
break;
default:
if (originalLoadSectionConstraints) originalLoadSectionConstraints(section);
}
};