Files
ddbb/web/static/app.js
2025-11-06 14:58:48 +01:00

223 lines
6.6 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
const app = document.getElementById('app');
if (!app) return;
const ionApp = document.createElement('ion-app');
const ionContent = document.createElement('ion-content');
ionContent.classList.add('ion-padding');
const mainDiv = document.createElement('div');
ionContent.appendChild(mainDiv);
ionApp.appendChild(ionContent);
app.appendChild(ionApp);
let jobs = [];
let filteredJobs = [];
let filters = {};
let sortField = null;
let sortAsc = true;
const columns = ['id', 'kind', 'host', 'database', 'user'];
const displayNames = ['ID', 'Kind', 'Host', 'Database', 'User'];
const headerGrid = document.createElement('ion-grid');
const filterRow = document.createElement('ion-row');
const headerRow = document.createElement('ion-row');
filterRow.style.alignItems = 'center';
headerRow.style.alignItems = 'center';
columns.forEach((col, idx) => {
// Filters row with ion-searchbar filter
const filterCol = document.createElement('ion-col');
filterCol.size = '2';
const filterInput = document.createElement('ion-searchbar');
filterInput.placeholder = `Filter ${displayNames[idx]}`;
filterInput.style.setProperty('--min-height', '32px');
filterInput.value = filters[col] || '';
filterInput.addEventListener('ionInput', e => {
const val = e.target.value.trim().toLowerCase();
if (val) filters[col] = val;
else delete filters[col];
applyFilterSortRender();
});
filterCol.appendChild(filterInput);
filterRow.appendChild(filterCol);
// Header row labels with sorting
const headerCol = document.createElement('ion-col');
headerCol.size = '2';
headerCol.style.fontWeight = 'bold';
headerCol.style.cursor = 'pointer';
headerCol.textContent = displayNames[idx];
headerCol.addEventListener('click', () => {
if (sortField === col) {
sortAsc = !sortAsc;
} else {
sortField = col;
sortAsc = true;
}
applyFilterSortRender();
});
if (sortField === col) {
headerCol.textContent += sortAsc ? ' ▲' : ' ▼';
}
headerCol.classList.add('ion-text-center');
headerRow.appendChild(headerCol);
});
// Actions column header and filter column (empty)
const actionsFilterCol = document.createElement('ion-col');
actionsFilterCol.size = '2';
filterRow.appendChild(actionsFilterCol);
const actionsHeaderCol = document.createElement('ion-col');
actionsHeaderCol.size = '2';
actionsHeaderCol.style.fontWeight = 'bold';
actionsHeaderCol.textContent = 'Actions';
actionsHeaderCol.classList.add('ion-text-center'); // Center the header text
headerRow.appendChild(actionsHeaderCol);
headerGrid.appendChild(filterRow);
headerGrid.appendChild(headerRow);
const ionList = document.createElement('ion-list');
mainDiv.appendChild(headerGrid);
mainDiv.appendChild(ionList);
async function loadJobs() {
try {
const res = await fetch('/list');
jobs = await res.json();
applyFilterSortRender();
} catch {
await presentAlert('Failed to load jobs.');
}
}
function applyFilterSortRender() {
filteredJobs = jobs.filter(job =>
Object.entries(filters).every(([k, v]) =>
(job[k] || '').toLowerCase().includes(v)
)
);
if (sortField) {
filteredJobs.sort((a, b) => {
if (a[sortField] < b[sortField]) return sortAsc ? -1 : 1;
if (a[sortField] > b[sortField]) return sortAsc ? 1 : -1;
return 0;
});
}
renderJobList();
}
// Show an Ionic alert with OK button
async function presentAlert(message) {
return new Promise(async (resolve) => {
const alert = document.createElement('ion-alert');
alert.header = 'Result';
alert.message = message;
alert.buttons = ['OK'];
document.body.appendChild(alert);
await alert.present();
alert.addEventListener('didDismiss', () => {
alert.remove();
resolve();
});
});
}
// Show Ionic confirmation alert, resolves boolean
async function presentConfirm(message) {
return new Promise(async (resolve) => {
const alert = document.createElement('ion-alert');
alert.header = 'Confirm';
alert.message = message;
alert.buttons = [
{
text: 'Cancel',
role: 'cancel',
handler: () => resolve(false),
},
{
text: 'OK',
handler: () => resolve(true),
},
];
document.body.appendChild(alert);
await alert.present();
alert.addEventListener('didDismiss', () => {
alert.remove();
});
});
}
function renderJobList() {
ionList.innerHTML = '';
filteredJobs.forEach(job => {
const ionItem = document.createElement('ion-item');
const grid = document.createElement('ion-grid');
const row = document.createElement('ion-row');
columns.forEach(col => {
const colElem = document.createElement('ion-col');
colElem.size = '2';
colElem.textContent = job[col] || '';
colElem.classList.add('ion-text-center'); // Center column text
row.appendChild(colElem);
});
const actionCol = document.createElement('ion-col');
actionCol.size = '2';
actionCol.classList.add('ion-text-center'); // Center alignment
const dumpBtn = document.createElement('ion-button');
dumpBtn.color = 'primary';
dumpBtn.size = 'small';
dumpBtn.textContent = 'Dump';
dumpBtn.addEventListener('click', () => triggerAction(job.id, 'dump'));
const restoreBtn = document.createElement('ion-button');
restoreBtn.color = 'danger';
restoreBtn.size = 'small';
restoreBtn.textContent = 'Restore';
restoreBtn.addEventListener('click', async () => {
const confirmed = await presentConfirm(`Confirm restore job ${job.id}?`);
if (confirmed) {
triggerAction(job.id, 'restore');
}
});
actionCol.appendChild(dumpBtn);
actionCol.appendChild(restoreBtn);
row.appendChild(actionCol);
grid.appendChild(row);
ionItem.appendChild(grid);
ionList.appendChild(ionItem);
});
}
async function triggerAction(jobId, action) {
try {
const res = await fetch(`/${action}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id: jobId}),
});
const result = await res.json();
await presentAlert(`${action.toUpperCase()} result: ${result.result || 'Unknown'}`);
loadJobs();
} catch {
await presentAlert(`Error running ${action}`);
}
}
loadJobs();
});