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

960 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/api/static/app.js
// JavaScript для веб-интерфейса Futriis DB Dashboard
// Глобальное состояние
let currentSession = null;
let currentDatabase = null;
let currentCollection = null;
let currentUser = null;
// DOM элементы
const contentArea = document.getElementById('contentArea');
const pageTitle = document.getElementById('pageTitle');
const connectionStatus = document.getElementById('connectionStatus');
const userInfoSpan = document.querySelector('#userInfo span');
const logoutBtn = document.getElementById('logoutBtn');
const menuToggle = document.getElementById('menuToggle');
const sidebar = document.querySelector('.sidebar');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modalTitle');
const modalBody = document.getElementById('modalBody');
const modalConfirm = document.getElementById('modalConfirm');
const modalCloseBtns = document.querySelectorAll('.modal-close');
// Инициализация приложения
document.addEventListener('DOMContentLoaded', () => {
checkSession();
initNavigation();
initEventListeners();
});
// Проверка сессии
async function checkSession() {
try {
const response = await fetch('/api/webui/session');
const data = await response.json();
if (data.success && data.data.authenticated) {
currentUser = data.data.username;
userInfoSpan.textContent = currentUser;
if (data.data.connection_status === 'connected') {
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = '<span>СУБД подключена</span>';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
loadDashboard();
} else {
showLoginModal();
}
} catch (error) {
console.error('Session check failed:', error);
showLoginModal();
}
}
// Функция для проверки статуса подключения
function startConnectionStatusMonitor() {
setInterval(async () => {
if (currentUser) {
try {
const response = await fetch('/api/webui/session');
const data = await response.json();
if (data.success && data.data.connection_status === 'connected') {
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = '<span>СУБД подключена</span>';
} else {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
} catch (error) {
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
}
}
}, 5000);
}
// Показать модальное окно входа
function showLoginModal() {
modalTitle.textContent = 'Вход в систему субд Futriis';
modalBody.innerHTML = `
<div class="form-group">
<label for="username">Имя пользователя</label>
<input type="text" id="username" class="form-control" placeholder="Введите имя пользователя">
</div>
<div class="form-group">
<label for="password">Пароль</label>
<input type="password" id="password" class="form-control" placeholder="Введите пароль">
</div>
`;
// Меняем текст на кнопке "Подтвердить" на "Войти"
modalConfirm.textContent = 'Войти';
modal.classList.add('show');
const confirmHandler = async () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
if (!username || !password) {
showNotification('Пожалуйста, заполните все поля', 'error');
return;
}
try {
const response = await fetch('/api/webui/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
currentUser = username;
userInfoSpan.textContent = username;
modal.classList.remove('show');
showNotification('Вход выполнен успешно', 'success');
connectionStatus.className = 'connection-status online';
connectionStatus.innerHTML = '<span>СУБД подключена</span>';
startConnectionStatusMonitor();
loadDashboard();
} else {
showNotification(data.error || 'Неверный логин и/или пароль', 'error');
}
} catch (error) {
showNotification('Ошибка подключения к серверу', 'error');
}
};
modalConfirm.onclick = confirmHandler;
// Обработка Enter
const handleEnter = (e) => {
if (e.key === 'Enter') {
confirmHandler();
document.removeEventListener('keydown', handleEnter);
}
};
document.addEventListener('keydown', handleEnter);
}
// Инициализация навигации
function initNavigation() {
document.querySelectorAll('.nav-link[data-section]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const section = link.dataset.section;
loadSection(section);
setActiveNav(link);
});
});
document.querySelectorAll('[data-action]').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const action = item.dataset.action;
handleCrudAction(action);
});
});
document.querySelectorAll('.has-submenu > .nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const parent = link.closest('.has-submenu');
parent.classList.toggle('open');
});
});
}
// Инициализация обработчиков событий
function initEventListeners() {
logoutBtn.addEventListener('click', async () => {
await fetch('/api/webui/logout', { method: 'POST' });
currentSession = null;
currentUser = null;
connectionStatus.className = 'connection-status offline';
connectionStatus.innerHTML = '<span>СУБД не подключена</span>';
showLoginModal();
});
if (menuToggle) {
menuToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
}
modalCloseBtns.forEach(btn => {
btn.addEventListener('click', () => {
modal.classList.remove('show');
});
});
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('show');
}
});
}
// Загрузка секции
async function loadSection(section) {
switch(section) {
case 'dashboard':
loadDashboard();
break;
case 'cluster':
loadClusterManagement();
break;
case 'audit':
loadAuditLog();
break;
case 'settings':
loadSettings();
break;
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>
`;
}
// Обработка 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;
}
}
// Показать модальное окно создания БД
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');
}
};
}
// Удаление коллекции
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');
}
}
};
// Сохранение настроек
function saveSettings() {
const theme = document.getElementById('themeSelect')?.value;
if (theme) {
localStorage.setItem('theme', theme);
showNotification('Настройки сохранены', 'success');
}
}
// Утилиты
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function showNotification(message, type = 'info') {
const container = document.getElementById('notificationContainer');
const notification = document.createElement('div');
notification.className = `notification ${type}`;
let icon = '';
switch(type) {
case 'success': icon = '<i class="fas fa-check-circle"></i>'; break;
case 'error': icon = '<i class="fas fa-exclamation-circle"></i>'; break;
case 'warning': icon = '<i class="fas fa-exclamation-triangle"></i>'; break;
default: icon = '<i class="fas fa-info-circle"></i>';
}
notification.innerHTML = `${icon}<span>${escapeHtml(message)}</span>`;
container.appendChild(notification);
setTimeout(() => {
notification.style.animation = 'slideOutRight 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 3000);
}
function setActiveNav(activeLink) {
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
activeLink.classList.add('active');
}