feat: Implement multi-tenancy with dynamic data source routing and introduce a database management UI.
This commit is contained in:
@@ -8,6 +8,7 @@ import { initEquipments } from './views/equipments.js';
|
||||
import { initClassrooms } from './views/classrooms.js';
|
||||
import { initSubjects } from './views/subjects.js';
|
||||
import {initSchedule} from "./views/schedule.js";
|
||||
import {initDatabase} from "./views/database.js";
|
||||
|
||||
// Configuration
|
||||
const ROUTES = {
|
||||
@@ -17,8 +18,8 @@ const ROUTES = {
|
||||
equipments: { title: 'Оборудование', file: 'views/equipments.html', init: initEquipments },
|
||||
classrooms: { title: 'Аудитории', file: 'views/classrooms.html', init: initClassrooms },
|
||||
subjects: { title: 'Дисциплины и преподаватели', file: 'views/subjects.html', init: initSubjects },
|
||||
// Новая вкладка
|
||||
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
||||
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
||||
};
|
||||
|
||||
let currentTab = null;
|
||||
|
||||
157
frontend/admin/js/views/database.js
Normal file
157
frontend/admin/js/views/database.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
|
||||
export async function initDatabase() {
|
||||
const tenantsTbody = document.getElementById('tenants-tbody');
|
||||
const addTenantForm = document.getElementById('add-tenant-form');
|
||||
const statusInfo = document.getElementById('db-status-info');
|
||||
const btnTest = document.getElementById('btn-test-connection');
|
||||
|
||||
// === Загрузка статуса текущего подключения ===
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const data = await api.get('/api/database/status');
|
||||
const statusBadge = data.connected
|
||||
? '<span class="badge badge-available">Online</span>'
|
||||
: '<span class="badge badge-unavailable">Offline</span>';
|
||||
|
||||
statusInfo.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">Тенант:</span>
|
||||
<strong>${escapeHtml(data.tenant || '—')}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">Название:</span>
|
||||
<strong>${escapeHtml(data.name || '—')}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">Статус:</span>
|
||||
${statusBadge}
|
||||
</div>
|
||||
${data.url ? `<div>
|
||||
<span style="color: var(--text-secondary); font-size: 0.85rem;">URL:</span>
|
||||
<code style="font-size: 0.85rem;">${escapeHtml(data.url)}</code>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
} catch (e) {
|
||||
statusInfo.innerHTML = `<div class="form-alert error" style="display:block">Ошибка загрузки статуса: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// === Загрузка списка тенантов ===
|
||||
async function loadTenants() {
|
||||
try {
|
||||
const tenants = await api.get('/api/database/tenants');
|
||||
renderTenantsTable(tenants);
|
||||
} catch (e) {
|
||||
tenantsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${e.message}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTenantsTable(tenants) {
|
||||
if (!tenants || !tenants.length) {
|
||||
tenantsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Нет подключённых тенантов</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tenantsTbody.innerHTML = tenants.map(t => {
|
||||
const statusBadge = t.connected
|
||||
? '<span class="badge badge-available">Online</span>'
|
||||
: '<span class="badge badge-unavailable">Offline</span>';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${escapeHtml(t.name || '—')}</td>
|
||||
<td><code>${escapeHtml(t.domain)}</code></td>
|
||||
<td><code style="font-size: 0.82rem;">${escapeHtml(t.url)}</code></td>
|
||||
<td>${escapeHtml(t.username || '—')}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td><button class="btn-delete" data-domain="${escapeHtml(t.domain)}">Удалить</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// === Тест подключения ===
|
||||
btnTest.addEventListener('click', async () => {
|
||||
hideAlert('add-tenant-alert');
|
||||
const url = document.getElementById('tenant-url').value.trim();
|
||||
const username = document.getElementById('tenant-username').value.trim();
|
||||
const password = document.getElementById('tenant-password').value;
|
||||
|
||||
if (!url) {
|
||||
showAlert('add-tenant-alert', 'Введите JDBC URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
btnTest.textContent = '...';
|
||||
btnTest.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await api.post('/api/database/test', { url, username, password });
|
||||
if (result.success) {
|
||||
showAlert('add-tenant-alert', '✓ Подключение успешно!', 'success');
|
||||
} else {
|
||||
showAlert('add-tenant-alert', `✗ ${result.message}`, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('add-tenant-alert', `Ошибка: ${e.message}`, 'error');
|
||||
} finally {
|
||||
btnTest.textContent = 'Тест';
|
||||
btnTest.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// === Добавление тенанта ===
|
||||
addTenantForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('add-tenant-alert');
|
||||
|
||||
const name = document.getElementById('tenant-name').value.trim();
|
||||
const domain = document.getElementById('tenant-domain').value.trim().toLowerCase();
|
||||
const url = document.getElementById('tenant-url').value.trim();
|
||||
const username = document.getElementById('tenant-username').value.trim();
|
||||
const password = document.getElementById('tenant-password').value;
|
||||
|
||||
if (!name || !domain || !url) {
|
||||
showAlert('add-tenant-alert', 'Заполните все обязательные поля', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.post('/api/database/tenants', { name, domain, url, username, password });
|
||||
if (result.success) {
|
||||
showAlert('add-tenant-alert', `Тенант "${escapeHtml(domain)}" добавлен!`, 'success');
|
||||
addTenantForm.reset();
|
||||
loadTenants();
|
||||
loadStatus();
|
||||
} else {
|
||||
showAlert('add-tenant-alert', result.message, 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showAlert('add-tenant-alert', `Ошибка: ${e.message}`, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// === Удаление тенанта ===
|
||||
tenantsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
|
||||
const domain = btn.dataset.domain;
|
||||
if (!confirm(`Удалить тенант "${domain}"? Пул соединений будет закрыт.`)) return;
|
||||
|
||||
try {
|
||||
await api.delete(`/api/database/tenants/${domain}`);
|
||||
loadTenants();
|
||||
loadStatus();
|
||||
} catch (e) {
|
||||
alert(`Ошибка: ${e.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// === Init ===
|
||||
loadStatus();
|
||||
loadTenants();
|
||||
}
|
||||
Reference in New Issue
Block a user