3252 lines
140 KiB
JavaScript
3252 lines
140 KiB
JavaScript
/*
|
||
* Copyright 2026 Safronov Grigorii
|
||
*
|
||
* Licensed under the CDDL, Version 1.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
*
|
||
* You may obtain a copy of the License at
|
||
* https://opensource.org/licenses/CDDL-1.0
|
||
*/
|
||
|
||
// Файл: 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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function showNotification(message, type = 'info') {
|
||
const container = document.getElementById('notificationContainer');
|
||
const notification = document.createElement('div');
|
||
notification.className = `notification ${type}`;
|
||
|
||
let icon = '';
|
||
switch(type) {
|
||
case 'success': icon = '<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">></option>
|
||
<option value="lt"><</option>
|
||
<option value="gte">≥</option>
|
||
<option value="lte">≤</option>
|
||
<option value="exists">exists</option>
|
||
</select>
|
||
<input type="text" id="conditionValue" class="form-control" style="flex: 1;" placeholder="Значение">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Операции триггера (JSON массив)</label>
|
||
<textarea id="triggerOperations" class="form-control" rows="6" placeholder='[
|
||
{"type": "set", "field": "updated_at", "value": "$$NOW"},
|
||
{"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);
|
||
}
|
||
};
|