feat: add cookies support and improve autorefresh UX

This commit also includes changing of load logic to update row by row
instead of full table toggle. It also changes behaviour on openned
dropdowns blocking refresh
This commit is contained in:
2025-07-01 12:16:15 +03:00
parent bd770aac2f
commit 471b1c1331
6 changed files with 190 additions and 118 deletions

View File

@ -99,6 +99,9 @@
<table id="packages" <table id="packages"
data-classes="table table-hover" data-classes="table table-hover"
data-cookie="true"
data-cookie-id-table="ahriman-packages"
data-cookie-storage="localStorage"
data-export-options='{"fileName": "packages"}' data-export-options='{"fileName": "packages"}'
data-filter-control="true" data-filter-control="true"
data-filter-control-visible="false" data-filter-control-visible="false"
@ -117,8 +120,8 @@
data-sortable="true" data-sortable="true"
data-sort-name="base" data-sort-name="base"
data-sort-order="asc" data-sort-order="asc"
data-toggle="table" data-toolbar="#toolbar"
data-toolbar="#toolbar"> data-unique-id="id">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-checkbox="true"></th> <th data-checkbox="true"></th>

View File

@ -80,8 +80,7 @@
data-classes="table table-hover" data-classes="table table-hover"
data-sortable="true" data-sortable="true"
data-sort-name="timestamp" data-sort-name="timestamp"
data-sort-order="desc" data-sort-order="desc">
data-toggle="table">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-align="right" data-field="timestamp">date</th> <th data-align="right" data-field="timestamp">date</th>
@ -509,6 +508,8 @@
} }
ready(_ => { ready(_ => {
packageInfoEventsTable.bootstrapTable({});
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, { packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
type: "line", type: "line",
data: {}, data: {},
@ -539,5 +540,7 @@
clearInterval(packageInfoAutoReloadTask); clearInterval(packageInfoAutoReloadTask);
packageInfoAutoReloadTask = null; // not really required (?) but lets clear everything packageInfoAutoReloadTask = null; // not really required (?) but lets clear everything
}); });
restoreAutoReloadSettings(packageInfoAutoReloadButton, packageInfoAutoReloadInput);
}); });
</script> </script>

View File

@ -113,7 +113,8 @@
convert: response => response.json(), convert: response => response.json(),
}, },
data => { data => {
const payload = data.map(description => { const payload = data
.map(description => {
const package_base = description.package.base; const package_base = description.package.base;
const web_url = description.package.remote.web_url; const web_url = description.package.remote.web_url;
return { return {
@ -129,8 +130,7 @@
}; };
}); });
table.bootstrapTable("load", payload); updateTable(table, payload);
table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
}, },
error => { error => {
@ -241,15 +241,20 @@
function toggleTableAutoReload(interval) { function toggleTableAutoReload(interval) {
clearInterval(tableAutoReloadTask); clearInterval(tableAutoReloadTask);
tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => { tableAutoReloadTask = toggleAutoReload(tableAutoReloadButton, interval, tableAutoReloadInput, _ => {
if ((getSelection().length === 0) && if (!dashboardModal.classList.contains("show") &&
(table.bootstrapTable("getOptions").pageNumber === 1) && !hasActiveDropdown()) {
(!dashboardModal.classList.contains("show"))) {
reload(true); reload(true);
} }
}); });
} }
ready(_ => { ready(_ => {
const onCheckFunction = function () {
if (packageRemoveButton) {
packageRemoveButton.disabled = !getSelection().length;
}
};
document.querySelectorAll("#repositories a").forEach(element => { document.querySelectorAll("#repositories a").forEach(element => {
element.onclick = _ => { element.onclick = _ => {
repository = { repository = {
@ -264,18 +269,16 @@
}; };
}); });
table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => { table.bootstrapTable({
if (packageRemoveButton) { onCheck: onCheckFunction,
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length; onCheckAll: onCheckFunction,
} onClickRow: (data, row, cell) => {
});
table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) { if (0 === cell || "base" === cell) {
const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript
table.bootstrapTable(method, {field: "id", values: [data.id]}); table.bootstrapTable(method, {field: "id", values: [data.id]});
} else showPackageInfo(data.id); } else showPackageInfo(data.id);
}); },
table.on("created-controls.bs.table", _ => { onCreatedControls: _ => {
new easepick.create({ new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"), element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [ css: [
@ -305,8 +308,13 @@
}; };
}, },
}); });
},
onUncheck: onCheckFunction,
onUncheckAll: onCheckFunction,
}); });
restoreAutoReloadSettings(tableAutoReloadButton, tableAutoReloadInput);
selectRepository(); selectRepository();
{% if autorefresh_intervals %} {% if autorefresh_intervals %}
toggleTableAutoReload(); toggleTableAutoReload();

View File

@ -53,8 +53,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
data-show-search-clear-button="true" data-show-search-clear-button="true"
data-sortable="true" data-sortable="true"
data-sort-name="base" data-sort-name="base"
data-sort-order="asc" data-sort-order="asc">
data-toggle="table">
<thead class="table-primary"> <thead class="table-primary">
<tr> <tr>
<th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th> <th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th>
@ -128,7 +127,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
} }
ready(_ => { ready(_ => {
table.on("created-controls.bs.table", _ => { table.bootstrapTable({
onCreatedControls: _ => {
new easepick.create({ new easepick.create({
element: document.querySelector(".bootstrap-table-filter-control-timestamp"), element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
css: [ css: [
@ -158,6 +158,7 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
}; };
}, },
}); });
},
}); });
}); });
</script> </script>

View File

@ -1,23 +1,24 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.30.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.33.0/tableExport.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/cookie/bootstrap-table-cookie.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script> <script>
async function copyToClipboard(text, button) { async function copyToClipboard(text, button) {
@ -62,6 +63,11 @@
return !document.getSelection().isCollapsed; // not sure if it is a valid way, but I guess so return !document.getSelection().isCollapsed; // not sure if it is a valid way, but I guess so
} }
function hasActiveDropdown() {
return Array.from(document.querySelectorAll(".dropdown-menu"))
.some(el => el.classList.contains("show"));
}
function headerClass(status) { function headerClass(status) {
if (status === "pending") return ["bg-warning"]; if (status === "pending") return ["bg-warning"];
if (status === "building") return ["bg-warning"]; if (status === "building") return ["bg-warning"];
@ -110,6 +116,12 @@
.catch(error => onFailure && onFailure(error)); .catch(error => onFailure && onFailure(error));
} }
function readOptional(extractor, callback) {
for (let value = extractor(); !!value; value = null) {
callback(value);
}
}
function ready(fn) { function ready(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") { if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1); setTimeout(fn, 1);
@ -118,6 +130,11 @@
} }
} }
function restoreAutoReloadSettings(toggle, intervalSelector) {
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-enabled`), value => toggle.checked = value === "true");
readOptional(() => localStorage.getItem(`ahriman-${toggle.id}-refresh-interval`), value => toggleActiveElement(intervalSelector, "interval", value));
}
function safe(string) { function safe(string) {
return String(string) return String(string)
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
@ -137,6 +154,18 @@
return element; return element;
} }
function toggleActiveElement(selector, dataType, value) {
const targetElement = selector.querySelector(`a[data-${dataType}="${value}"]`);
if (targetElement?.classList?.contains("active")) {
return; // element is already active, skip processing
}
Array.from(selector.children).forEach(il => {
Array.from(il.children).forEach(el => el.classList.remove("active"));
});
targetElement?.classList?.add("active");
}
function toggleAutoReload(toggle, interval, intervalSelector, callback) { function toggleAutoReload(toggle, interval, intervalSelector, callback) {
if (interval) { if (interval) {
toggle.checked = true; // toggle reload toggle.checked = true; // toggle reload
@ -144,21 +173,49 @@
interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element interval = intervalSelector.querySelector(".active")?.dataset?.interval; // find active element
} }
let intervalId = null;
if (interval) { if (interval) {
if (toggle.checked) { if (toggle.checked) {
// refresh UI // refresh UI
Array.from(intervalSelector.children).forEach(il => { toggleActiveElement(intervalSelector, "interval", interval);
Array.from(il.children).forEach(el => el.classList.remove("active"));
});
intervalSelector.querySelector(`a[data-interval="${interval}"]`)?.classList?.add("active");
// finally create timer task // finally create timer task
return setInterval(callback, interval); intervalId = setInterval(callback, interval);
} }
} else { } else {
toggle.checked = false; // no active interval found, disable toggle toggle.checked = false; // no active interval found, disable toggle
} }
return null; // return null to assign to keep method sane localStorage.setItem(`ahriman-${toggle.id}-refresh-enabled`, toggle.checked);
localStorage.setItem(`ahriman-${toggle.id}-refresh-interval`, interval);
return intervalId;
}
function updateTable(table, rows) {
// instead of using load method here, we just update rows manually to avoid table reinitialization
const currentData = table.bootstrapTable("getData").reduce((accumulator, row) => {
accumulator[row.id] = row["0"];
return accumulator;
}, {});
// insert or update rows
rows.forEach(row => {
if (Object.hasOwn(currentData, row.id)) {
row["0"] = currentData[row.id]; // copy checkbox state
table.bootstrapTable("updateByUniqueId", {
id: row.id,
row: row,
replace: true,
});
} else {
table.bootstrapTable("insertRow", {index: 0, row: row});
}
});
// remove old rows
const newData = rows.map(value => value.id);
Object.keys(currentData).forEach(id => {
if (!newData.includes(id)) {
table.bootstrapTable("removeByUniqueId", id);
}
});
} }
Date.prototype.toISOStringShort = function () { Date.prototype.toISOStringShort = function () {

View File

@ -1,15 +1,15 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.24.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.11.1/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<style> <style>
.pre-scrollable { .pre-scrollable {