This commit is contained in:
2026-01-12 23:57:46 +01:00
parent b05f59e554
commit 47327a12e0
9 changed files with 421 additions and 58 deletions

View File

@@ -3,11 +3,15 @@ import { apiAction, toast } from './api.js';
const state = {
setup: null,
loading: false,
allTables: [],
selectedTables: [],
};
let form;
let tablesInput;
let tablesPreview;
let tablesAllSelect;
let tablesSelectedSelect;
let tablesAddBtn;
let tablesRemoveBtn;
let modeInputs;
let directFields;
let configFields;
@@ -15,12 +19,18 @@ let statusLabel;
let loadBtn;
let configExampleBtn;
let configExampleDialog;
let bridgeSetupDialog;
let openBridgeSetupBtn;
let adminLoadBridgeBtn;
let closeBridgeSetupBtn;
export function initBridgeSetupPage() {
form = document.getElementById('bridgeSetupForm');
if (!form) return;
tablesInput = form.elements.tables;
tablesPreview = document.getElementById('selectedTables');
tablesAllSelect = document.getElementById('bridgeTablesAll');
tablesSelectedSelect = document.getElementById('bridgeTablesSelected');
tablesAddBtn = document.getElementById('bridgeTablesAdd');
tablesRemoveBtn = document.getElementById('bridgeTablesRemove');
directFields = document.getElementById('directFields');
configFields = document.getElementById('configFields');
statusLabel = document.getElementById('setupStatus');
@@ -28,13 +38,33 @@ export function initBridgeSetupPage() {
configExampleBtn = document.getElementById('btn-config-example');
configExampleDialog = document.getElementById('configExampleDialog');
modeInputs = Array.from(form.querySelectorAll('input[name="db_mode"]'));
bridgeSetupDialog = document.getElementById('bridgeSetupDialog');
openBridgeSetupBtn = document.getElementById('btn-open-bridge-setup');
adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge');
closeBridgeSetupBtn = document.getElementById('btn-close-bridge-setup');
form.addEventListener('submit', submitBridgeSetup);
tablesInput?.addEventListener('input', () => updateTablesPreview(parseTablesInput()));
tablesAddBtn?.addEventListener('click', () => addSelectedTables(getSelectedOptions(tablesAllSelect)));
tablesRemoveBtn?.addEventListener('click', () => removeSelectedTables(getSelectedOptions(tablesSelectedSelect)));
loadBtn?.addEventListener('click', loadTablesFromBridge);
configExampleBtn?.addEventListener('click', () => {
if (configExampleDialog?.showModal) configExampleDialog.showModal();
});
openBridgeSetupBtn?.addEventListener('click', () => {
if (bridgeSetupDialog?.showModal) bridgeSetupDialog.showModal();
if (!state.allTables.length) {
loadTablesFromBridge(null, { preserveSelection: state.selectedTables.length > 0 });
}
});
closeBridgeSetupBtn?.addEventListener('click', () => {
if (bridgeSetupDialog?.open) bridgeSetupDialog.close();
});
adminLoadBridgeBtn?.addEventListener('click', (ev) => {
if (bridgeSetupDialog?.showModal && !bridgeSetupDialog.open) {
bridgeSetupDialog.showModal();
}
loadTablesFromBridge(ev, { preserveSelection: state.selectedTables.length > 0 });
});
modeInputs.forEach(input => {
input.addEventListener('change', () => applyModeVisibility(input.value));
});
@@ -82,12 +112,12 @@ async function loadBridgeSetup() {
}
}
function fillForm(setup) {
function fillForm(setup, options = {}) {
const data = normalizeSetupInput(setup);
state.setup = data;
if (tablesInput) {
tablesInput.value = data.tables.join(', ');
updateTablesPreview(data.tables);
if (!options.skipTables) {
state.selectedTables = data.tables.slice();
updateTableSelects();
}
const activeMode = (data.mode || 'direct').toLowerCase();
modeInputs.forEach(input => {
@@ -135,22 +165,50 @@ function applyModeVisibility(mode) {
configFields?.classList[config]('hidden');
}
function parseTablesInput() {
if (!tablesInput) return [];
return tablesInput.value
.split(/[\s,]+/)
.map(part => part.trim())
.filter(Boolean)
.filter((value, index, arr) => arr.indexOf(value) === index);
function updateTableSelects() {
const all = state.allTables || [];
const selected = state.selectedTables || [];
const selectedSet = new Set(selected);
const orderedSelected = all.length ? all.filter(name => selectedSet.has(name)) : selected;
const available = all.length ? all.filter(name => !selectedSet.has(name)) : [];
renderSelect(tablesAllSelect, available, 'Noch keine Tabellen geladen.');
renderSelect(tablesSelectedSelect, orderedSelected, 'Noch keine Tabellen ausgewaehlt.');
}
function updateTablesPreview(list) {
if (!tablesPreview) return;
function renderSelect(selectEl, list, emptyLabel) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!list.length) {
tablesPreview.innerHTML = '<span class="text-xs text-slate-500">Noch keine Tabellen angegeben.</span>';
const opt = document.createElement('option');
opt.textContent = emptyLabel;
opt.disabled = true;
selectEl.appendChild(opt);
return;
}
tablesPreview.innerHTML = list.map(name => `<span class="chip">${escapeHtml(name)}</span>`).join('');
list.forEach(name => {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
selectEl.appendChild(opt);
});
}
function getSelectedOptions(selectEl) {
if (!selectEl) return [];
return Array.from(selectEl.selectedOptions || []).map(opt => opt.value);
}
function addSelectedTables(list) {
if (!list.length) return;
state.selectedTables = normalizeTableNames([...state.selectedTables, ...list]);
updateTableSelects();
}
function removeSelectedTables(list) {
if (!list.length) return;
const removeSet = new Set(list);
state.selectedTables = state.selectedTables.filter(name => !removeSet.has(name));
updateTableSelects();
}
async function submitBridgeSetup(ev) {
@@ -158,7 +216,7 @@ async function submitBridgeSetup(ev) {
if (!form) return;
const mode = form.querySelector('input[name="db_mode"]:checked')?.value || 'direct';
const payload = {
tables: parseTablesInput(),
tables: normalizeTableNames(state.selectedTables),
mode,
direct_host: form.direct_host?.value.trim() || '',
direct_port: Number(form.direct_port?.value || 0) || 3306,
@@ -182,13 +240,17 @@ async function submitBridgeSetup(ev) {
fillForm(res.setup || payload);
updateStatus('Bridge-Setup gespeichert.');
toast('Bridge-Setup gespeichert', true);
if (bridgeSetupDialog?.open) {
bridgeSetupDialog.close();
}
window.dispatchEvent(new CustomEvent('bridge-setup-updated', { detail: res.setup || payload }));
} catch (err) {
console.error(err);
toast(err.message || 'Bridge-Setup konnte nicht gespeichert werden', false);
}
}
async function loadTablesFromBridge(ev) {
async function loadTablesFromBridge(ev, options = {}) {
ev?.preventDefault();
if (!loadBtn) return;
loadBtn.disabled = true;
@@ -196,18 +258,32 @@ async function loadTablesFromBridge(ev) {
const res = await apiAction('account.bridge.test', { method: 'POST', data: {} });
if (!res?.ok) throw new Error(res?.error || 'Bridge konnte nicht abgefragt werden');
const fetchedTables = normalizeTableNames(res.tables);
const allowedTables = normalizeTableNames(res.setup_hint?.tables ?? fetchedTables);
const merged = {
...(state.setup || {}),
tables: allowedTables,
...(res.setup_hint || {}),
};
state.allTables = fetchedTables;
const preserveSelection = !!options.preserveSelection;
const hasSelection = state.selectedTables.length > 0;
if (preserveSelection || hasSelection) {
state.selectedTables = normalizeTableNames(state.selectedTables);
} else {
state.selectedTables = fetchedTables.slice();
}
if (fetchedTables.length && state.selectedTables.length) {
const fetchedSet = new Set(fetchedTables);
state.selectedTables = state.selectedTables.filter(name => fetchedSet.has(name));
}
if (res.setup_hint) {
const merged = {
...(state.setup || {}),
...(res.setup_hint || {}),
};
merged.mode = res.setup_hint.mode || merged.mode;
merged.direct = { ...(merged.direct || {}), ...(res.setup_hint.direct || {}) };
merged.config = { ...(merged.config || {}), ...(res.setup_hint.config || {}) };
fillForm(merged, { skipTables: true });
}
fillForm(merged);
updateTableSelects();
updateStatus(`Tabellen geladen (${fetchedTables.length}).`);
toast('Tabellen erfolgreich geladen', true);
} catch (err) {
@@ -246,15 +322,6 @@ function updateStatus(msg) {
statusLabel.textContent = `${msg} (${time})`;
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function normalizeSetupInput(input) {
const base = defaultSetup();
if (!input || typeof input !== 'object') return base;