first commit
This commit is contained in:
924
internal/api/static/app.js
Normal file
924
internal/api/static/app.js
Normal file
@@ -0,0 +1,924 @@
|
||||
/*
|
||||
* 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;
|
||||
connectionStatus.classList.add('online');
|
||||
connectionStatus.classList.remove('offline');
|
||||
loadDashboard();
|
||||
} else {
|
||||
showLoginModal();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Session check failed:', error);
|
||||
showLoginModal();
|
||||
}
|
||||
}
|
||||
|
||||
// Показать модальное окно входа
|
||||
function showLoginModal() {
|
||||
modalTitle.textContent = 'Вход в систему';
|
||||
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>
|
||||
`;
|
||||
|
||||
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');
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
// Обработка подменю CRUD
|
||||
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;
|
||||
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>';
|
||||
// TODO: Реализовать API для получения лога аудита
|
||||
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 = 'Создать базу данных';
|
||||
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 = 'Создать коллекцию';
|
||||
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 = 'Вставить документ';
|
||||
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 = 'Найти документ';
|
||||
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 = 'Обновить документ';
|
||||
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 = 'Удалить документ';
|
||||
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 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');
|
||||
}
|
||||
695
internal/api/static/style.css
Normal file
695
internal/api/static/style.css
Normal file
@@ -0,0 +1,695 @@
|
||||
/*
|
||||
* 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/style.css */
|
||||
/* Стили для веб-интерфейса Futriis DB Dashboard */
|
||||
|
||||
:root {
|
||||
--primary-color: #00bfff;
|
||||
--primary-dark: #0099cc;
|
||||
--secondary-color: #6c757d;
|
||||
--success-color: #28a745;
|
||||
--danger-color: #dc3545;
|
||||
--warning-color: #ffc107;
|
||||
--info-color: #17a2b8;
|
||||
|
||||
--bg-dark: #1a1a2e;
|
||||
--bg-sidebar: #16213e;
|
||||
--bg-card: #0f3460;
|
||||
--bg-hover: #1a1f3a;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #b8c6db;
|
||||
--border-color: #2d3a5e;
|
||||
|
||||
--shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
|
||||
--shadow-md: 0 4px 8px rgba(0,0,0,0.15);
|
||||
--shadow-lg: 0 8px 16px rgba(0,0,0,0.2);
|
||||
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: var(--bg-dark);
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Dashboard Container */
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Sidebar - Вертикальное меню */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: var(--bg-sidebar);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: var(--transition);
|
||||
box-shadow: var(--shadow-lg);
|
||||
z-index: 100;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 24px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo i {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.logo span {
|
||||
background: linear-gradient(135deg, var(--primary-color), #00ffcc);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Navigation Menu */
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
list-style: none;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 20px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: var(--transition);
|
||||
border-radius: 8px;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-link i {
|
||||
width: 24px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.nav-link span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Submenu */
|
||||
.has-submenu > .nav-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.has-submenu > .nav-link .fa-chevron-down {
|
||||
margin-left: auto;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.has-submenu.open > .nav-link .fa-chevron-down {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.submenu {
|
||||
list-style: none;
|
||||
padding-left: 56px;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.has-submenu.open .submenu {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.submenu li a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
transition: var(--transition);
|
||||
border-radius: 6px;
|
||||
margin: 2px 8px;
|
||||
}
|
||||
|
||||
.submenu li a:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.submenu li a i {
|
||||
width: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Sidebar Footer */
|
||||
.sidebar-footer {
|
||||
padding: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.user-info i {
|
||||
font-size: 1.2rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
background: var(--danger-color);
|
||||
border: none;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.logout-btn:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
background: var(--bg-sidebar);
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.top-bar h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.connection-status i {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.connection-status.online i {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.connection-status.offline i {
|
||||
color: var(--danger-color);
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Dashboard Cards */
|
||||
.dashboard-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: rgba(0, 191, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.stat-icon i {
|
||||
font-size: 2rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-info p {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-table table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: var(--bg-sidebar);
|
||||
font-weight: 600;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.data-table tr:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-dark);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(0, 191, 255, 0.2);
|
||||
}
|
||||
|
||||
textarea.form-control {
|
||||
min-height: 120px;
|
||||
font-family: monospace;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--secondary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: var(--danger-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal.show {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: modalSlideIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes modalSlideIn {
|
||||
from {
|
||||
transform: translateY(-50px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
.notification-container {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
.notification {
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px;
|
||||
padding: 12px 20px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
animation: slideInRight 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
border-left: 4px solid var(--success-color);
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
border-left: 4px solid var(--danger-color);
|
||||
}
|
||||
|
||||
.notification.warning {
|
||||
border-left: 4px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.notification.info {
|
||||
border-left: 4px solid var(--info-color);
|
||||
}
|
||||
|
||||
/* Loading Spinner */
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.loading-spinner i {
|
||||
font-size: 3rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.loading-spinner p {
|
||||
margin-top: 16px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 8px 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
position: fixed;
|
||||
left: -280px;
|
||||
height: 100%;
|
||||
transition: left 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.menu-toggle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 95%;
|
||||
margin: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.top-bar h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.stat-info h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar Styling */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
Reference in New Issue
Block a user