Some checks failed
Build and Push Docker Image / build (push) Failing after 6m59s
616 lines
18 KiB
JavaScript
616 lines
18 KiB
JavaScript
|
|
// region Init Table
|
|
|
|
function initializeDataTable(tableId, columns) {
|
|
$(tableId).DataTable({
|
|
info: false,
|
|
paging: false,
|
|
ordering: false,
|
|
searching: false,
|
|
autoWidth: false,
|
|
order: [],
|
|
language: {
|
|
emptyTable: "No results found"
|
|
},
|
|
select: {
|
|
style: "multi+shift",
|
|
selector: 'th:first-child input[type="checkbox"]'
|
|
},
|
|
columnDefs: [
|
|
{ orderable: false, targets: "no-sort" },
|
|
{
|
|
targets: 0,
|
|
checkboxes: { selectRow: true }
|
|
},
|
|
],
|
|
columns: [
|
|
{
|
|
// Select row checkbox column
|
|
title: '<input type="checkbox" class="form-check-input table-select-all" />',
|
|
data: null,
|
|
orderable: false,
|
|
className: "col-checkbox text-center",
|
|
render: function() {
|
|
return '<input type="checkbox" class="form-check-input table-select-row" />'
|
|
}
|
|
},
|
|
{ data: "id", visible: false },
|
|
...columns
|
|
]
|
|
});
|
|
bindTablePagination(tableId);
|
|
bindTablePageSizer(tableId);
|
|
bindTableSearch(tableId);
|
|
bindRefreshButton(tableId);
|
|
bindTableSelectColumn(tableId);
|
|
}
|
|
|
|
|
|
// region Filters & Ordering
|
|
|
|
// Filter methods
|
|
var _tableFilters = {};
|
|
function getTableFilters(tableId) {
|
|
return _tableFilters[tableId];
|
|
}
|
|
|
|
function setTableFilter(tableId, key, value) {
|
|
if (!_tableFilters[tableId]) {
|
|
_tableFilters[tableId] = {};
|
|
}
|
|
|
|
_tableFilters[tableId][key] = value;
|
|
}
|
|
|
|
// Sort methods
|
|
var _tableOrdering = {};
|
|
function getTableOrdering(tableId) {
|
|
return _tableOrdering[tableId];
|
|
}
|
|
|
|
function setTableOrdering(tableId, value) {
|
|
_tableOrdering[tableId] = value;
|
|
}
|
|
|
|
// Clear all kinds of sorting and filtering when changing servers
|
|
$(document).on("selectedServerChange", function() {
|
|
_tableFilters = {};
|
|
_tableOrdering = {};
|
|
$(".table-search-input").val("");
|
|
});
|
|
|
|
|
|
// region Load & Clear Data
|
|
|
|
function wipeTable(tableId) {
|
|
$(`${tableId} thead .table-select-all`).prop("checked", false).prop("indeterminate", false);
|
|
$(tableId).DataTable().clear().draw(false)
|
|
}
|
|
|
|
function populateTable(tableId, data) {
|
|
$(tableId).DataTable().rows.add(data.results).draw(false);
|
|
updateTablePagination(tableId, data);
|
|
updateTableTotalCount(tableId, data);
|
|
}
|
|
|
|
async function loadTableData(tableId, url, method) {
|
|
fixTablePagination(tableId);
|
|
disableTableControls(tableId);
|
|
|
|
// Create querystring for filtering against the API
|
|
const filters = getTableFilters(tableId);
|
|
const ordering = getTableOrdering(tableId);
|
|
const querystring = makeQuerystring(filters, ordering);
|
|
|
|
// API request
|
|
const data = await ajaxRequest(url + querystring, method);
|
|
|
|
// Update table with new data
|
|
wipeTable(tableId);
|
|
populateTable(tableId, data);
|
|
enableTableControls(tableId);
|
|
}
|
|
|
|
|
|
// region Pagination
|
|
|
|
function bindTablePagination(tableId) {
|
|
let $paginationArea = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find('.pagination');
|
|
$paginationArea.on("click", ".page-link", function() {
|
|
let currentPage = parseInt($paginationArea.data("page"));
|
|
let wantedPage;
|
|
|
|
if ($(this).hasClass("page-prev")) {
|
|
wantedPage = currentPage - 1;
|
|
}
|
|
else if ($(this).hasClass("page-next")) {
|
|
wantedPage = currentPage + 1;
|
|
}
|
|
else {
|
|
wantedPage = $(this).attr("data-page")
|
|
}
|
|
|
|
setTableFilter(tableId, "page", wantedPage);
|
|
$(tableId).trigger("doDataLoad");
|
|
});
|
|
}
|
|
|
|
function fixTablePagination(tableId) {
|
|
let filters = getTableFilters(tableId);
|
|
let $pageSizer = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find(".table-page-sizer");
|
|
|
|
if (!("page" in filters)) {
|
|
setTableFilter(tableId, "page", 1);
|
|
}
|
|
if (!("page_size" in filters)) {
|
|
setTableFilter(tableId, "page_size", $($pageSizer).val())
|
|
}
|
|
}
|
|
|
|
function updateTablePagination(tableId, data) {
|
|
let filters = getTableFilters(tableId);
|
|
|
|
let $paginationArea = $(tableId).closest('.js-tableBody').siblings('.js-tableControls').find('.pagination');
|
|
$paginationArea.data("page", filters.page); // store the page for later
|
|
|
|
// Remove existing buttons for specific pages
|
|
$paginationArea.find(".page-pick").remove();
|
|
|
|
// Enable/disable 'next' and 'prev' buttons
|
|
$paginationArea.find(".page-prev").toggleClass("disabled", !data.previous);
|
|
$paginationArea.find(".page-next").toggleClass("disabled", !data.next);
|
|
|
|
const pagesToShow = Math.max(Math.ceil(data.count / filters.page_size), 1);
|
|
const maxPagesToShow = 10;
|
|
|
|
let startPage = 1;
|
|
let endPage;
|
|
let page = parseInt(filters.page);
|
|
|
|
// Determine the start and end page
|
|
if (pagesToShow <= maxPagesToShow) {
|
|
endPage = pagesToShow;
|
|
}
|
|
else {
|
|
const halfVisible = Math.floor(maxPagesToShow / 2);
|
|
if (page <= halfVisible) {
|
|
endPage = maxPagesToShow;
|
|
}
|
|
else if (page + halfVisible >= pagesToShow) {
|
|
startPage = pagesToShow - maxPagesToShow + 1;
|
|
endPage = pagesToShow;
|
|
}
|
|
else {
|
|
startPage = page - halfVisible;
|
|
endPage = page + halfVisible;
|
|
}
|
|
}
|
|
|
|
// Add buttons
|
|
if (startPage > 1) {
|
|
AddTablePageButton($paginationArea, 1);
|
|
if (startPage > 2) {
|
|
AddTablePageEllipsis($paginationArea);
|
|
}
|
|
}
|
|
for (i = startPage; i <= endPage; i++) {
|
|
AddTablePageButton($paginationArea, i, page);
|
|
}
|
|
if (endPage < pagesToShow) {
|
|
if (endPage < pagesToShow - 1) {
|
|
AddTablePageEllipsis($paginationArea);
|
|
}
|
|
AddTablePageButton($paginationArea, pagesToShow);
|
|
}
|
|
}
|
|
|
|
function AddTablePageButton($paginationArea, number, currentPage) {
|
|
let pageItem = $("<li>").addClass("page-item");
|
|
let pageLink = $("<button>")
|
|
.attr("type", "button")
|
|
.attr("data-page", number)
|
|
.addClass("page-link page-pick")
|
|
.text(number);
|
|
|
|
if (number === parseInt(currentPage)) {
|
|
pageLink.addClass("disabled").attr("tabindex", -1);
|
|
}
|
|
|
|
pageItem.append(pageLink);
|
|
$paginationArea.find(".page-next").parent().before(pageItem);
|
|
}
|
|
|
|
function AddTablePageEllipsis($paginationArea) {
|
|
let ellipsisItem = $("<li>").addClass("page-item disabled");
|
|
let ellipsisLink = $("<span>").addClass("page-link page-pick").text("...");
|
|
ellipsisItem.append(ellipsisLink);
|
|
$paginationArea.find(".page-next").parent().before(ellipsisItem);
|
|
}
|
|
|
|
|
|
// region Page Sizer
|
|
|
|
function updateTableTotalCount(tableId, data) {
|
|
let $tableControls = $(tableId).closest('.js-tableBody').siblings('.js-tableControls');
|
|
$tableControls.find('.pageinfo-total').text(data.count);
|
|
}
|
|
|
|
function bindTablePageSizer(tableId) {
|
|
let $tableControls = $(tableId).closest('.js-tableBody').siblings('.js-tableControls');
|
|
$tableControls.on("change", ".table-page-sizer", function() {
|
|
setTableFilter(tableId, "page", "1");
|
|
setTableFilter(tableId, "page_size", $(this).val());
|
|
$(tableId).trigger("doDataLoad");
|
|
});
|
|
}
|
|
|
|
|
|
// region Search Filters
|
|
|
|
_searchTimeouts = {};
|
|
function bindTableSearch(tableId) {
|
|
const $tableFilters = $(tableId).closest('.js-tableBody').siblings('.js-tableFilters');
|
|
$tableFilters.on("input", ".table-search-input", function() {
|
|
$(this).data("was-focused", true);
|
|
clearTimeout(_searchTimeouts[tableId]);
|
|
|
|
const searchString = $(this).val();
|
|
setTableFilter(tableId, "search", searchString);
|
|
setTableFilter(tableId, "page", 1); // back to first page, as desired page no. might not exist after filtering
|
|
|
|
_searchTimeouts[tableId] = setTimeout(function() {
|
|
$(tableId).trigger("doDataLoad");
|
|
}, 300);
|
|
});
|
|
}
|
|
|
|
|
|
// region Button Controls
|
|
|
|
function bindRefreshButton(tableId) {
|
|
$controls = getTableFiltersComponent(tableId);
|
|
$controls.on("click", ".js-tableRefreshBtn", function() {
|
|
$controls.find(".js-tableDeleteBtn").prop("disabled", true);
|
|
$controls.find(".js-tableShareBtn").prop("disabled", true);
|
|
$(tableId).trigger("doDataLoad");
|
|
})
|
|
}
|
|
|
|
|
|
// region Select Checkboxes
|
|
|
|
function bindTableSelectColumn(tableId) {
|
|
$(tableId).on("change", "tbody tr .table-select-row", function() {
|
|
let selected = $(this).prop("checked");
|
|
let rowIndex = $(this).closest("tr").index();
|
|
let row = $(tableId).DataTable().row(rowIndex);
|
|
|
|
selected === true ? row.select() : row.deselect();
|
|
determineSelectAllState(tableId);
|
|
});
|
|
|
|
$(tableId).on("change", "thead .table-select-all", function() {
|
|
let selected = $(this).prop("checked");
|
|
let table = $(tableId).DataTable();
|
|
$(tableId).find("tbody tr").each(function(rowIndex) {
|
|
let row = table.row(rowIndex);
|
|
selected === true ? row.select() : row.deselect();
|
|
$(this).find(".table-select-row").prop("checked", selected);
|
|
});
|
|
|
|
determineSelectAllState(tableId);
|
|
});
|
|
}
|
|
|
|
function determineSelectAllState(tableId) {
|
|
let table = $(tableId).DataTable();
|
|
let selectedRowsCount = table.rows(".selected").data().toArray().length;
|
|
let allRowsCount = table.rows().data().toArray().length;
|
|
|
|
let doCheck = selectedRowsCount === allRowsCount;
|
|
let doIndeterminate = !doCheck && selectedRowsCount > 0;
|
|
|
|
$checkbox = $(tableId).find("thead .table-select-all");
|
|
$checkbox.prop("checked", doCheck);
|
|
$checkbox.prop("indeterminate", doIndeterminate);
|
|
|
|
const selectionExists = doCheck || doIndeterminate;
|
|
const $controls = getTableFiltersComponent(tableId);
|
|
$controls.find(".js-tableShareBtn").prop("disabled", !selectionExists);
|
|
$controls.find(".js-tableDeleteBtn").prop("disabled", !selectionExists);
|
|
}
|
|
|
|
|
|
// region On/Off Controls
|
|
|
|
function enableTableControls(tableId) {
|
|
setTableControlsUsability(tableId, false);
|
|
}
|
|
|
|
function disableTableControls(tableId) {
|
|
setTableControlsUsability(tableId, true);
|
|
}
|
|
|
|
function setTableControlsUsability(tableId, disabled) {
|
|
const $table = $(tableId);
|
|
const $tableBody = $table.closest(".js-tableBody");
|
|
const $tableFilters = $tableBody.siblings(".js-tableFilters");
|
|
const $tableControls = $tableBody.siblings(".tableControls");
|
|
|
|
$tableBody.find(".disable-while-loading").prop("disabled", disabled);
|
|
$tableFilters.find(".disable-while-loading").prop("disabled", disabled);
|
|
$tableControls.find(".disable-while-loading").prop("disabled", disabled);
|
|
|
|
// Re-focus search bar if used
|
|
const $search = $tableFilters.find(".disable-while-loading[type=search]");
|
|
$search.data("was-focused") ? $search.focus() : null;
|
|
}
|
|
|
|
|
|
// region Modals
|
|
|
|
|
|
async function openDataModal(modalId, pk, url) {
|
|
$modal = $(modalId);
|
|
$modal.data("primary-key", pk);
|
|
clearValidation($modal);
|
|
|
|
if (parseInt(pk) === -1) {
|
|
$modal.find(".form-create").show();
|
|
$modal.find(".form-edit").hide();
|
|
setDefaultModalData($modal);
|
|
}
|
|
else {
|
|
$modal.find(".form-create").hide();
|
|
$modal.find(".form-edit").show();
|
|
await loadModalData($modal, url);
|
|
}
|
|
|
|
$modal.modal("show");
|
|
}
|
|
|
|
function setDefaultModalData($modal) {
|
|
$modal.find("[data-field]").each(function() {
|
|
const type = $(this).attr("type");
|
|
const defaultVal = $(this).attr("data-default") || "";
|
|
|
|
if (type === "checkbox") {
|
|
$(this).prop("checked", defaultVal === "true");
|
|
}
|
|
else if (type === "datetime-local") {
|
|
$(this).val(getCurrentDateTime());
|
|
}
|
|
else if ($(this).is("select") && defaultVal === "firstOption") {
|
|
$(this).val($(this).find("option:first").val()).change();
|
|
}
|
|
else {
|
|
$(this).val(defaultVal).change();
|
|
}
|
|
});
|
|
}
|
|
|
|
function clearValidation($modal) {
|
|
$modal.find(".invalid-feedback").remove();
|
|
$modal.find(".is-invalid").removeClass("is-invalid");
|
|
}
|
|
|
|
async function loadModalData($modal, url) {
|
|
const data = await ajaxRequest(url, "GET");
|
|
|
|
$modal.find("[data-field]").each(function() {
|
|
const key = $(this).attr("data-field");
|
|
const value = data[key];
|
|
|
|
if (typeof value === "boolean") {
|
|
$(this).prop("checked", value);
|
|
}
|
|
else if (isISODateTimeString(value)) {
|
|
$(this).val(value.split('+')[0].substring(0, 16));
|
|
}
|
|
else if ($(this).attr("type") === "color") {
|
|
$(this).val(`#${value}`);
|
|
}
|
|
else {
|
|
$(this).val(value).change();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function onModalSubmit($modal, $table, url) {
|
|
if (!selectedServer) {
|
|
return;
|
|
}
|
|
|
|
clearValidation($modal);
|
|
let data = { server: selectedServer.id };
|
|
|
|
$modal.find("[data-field]").each(function() {
|
|
const type = $(this).attr("type");
|
|
const key = $(this).attr("data-field");
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
let value;
|
|
switch (type) {
|
|
case "checkbox":
|
|
value = $(this).prop("checked");
|
|
break;
|
|
case "color":
|
|
value = $(this).val();
|
|
value = value ? value.replace("#", "") : value;
|
|
break;
|
|
default:
|
|
value = $(this).val();
|
|
break;
|
|
}
|
|
|
|
data[key] = value;
|
|
});
|
|
|
|
const formData = objectToFormData(data);
|
|
const id = $modal.data("primary-key");
|
|
const isNewItem = parseInt(id) !== -1;
|
|
const method = isNewItem ? "PUT" : "POST";
|
|
url = isNewItem ? url + `${id}/` : url;
|
|
|
|
ajaxRequest(url, method, formData)
|
|
.then(response => {
|
|
$table.trigger("doDataLoad");
|
|
$modal.modal("hide");
|
|
})
|
|
.catch(error => {
|
|
logError(error);
|
|
if (typeof error === "object" && "responseJSON" in error) {
|
|
renderErrorMessages($modal, error.responseJSON);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// region Modal Error Msgs
|
|
|
|
function renderErrorMessages($modal, errorObj) {
|
|
for (const key in errorObj) {
|
|
const value = errorObj[key];
|
|
const $input = $modal.find(`[data-field="${key}"]`);
|
|
$input.addClass("is-invalid");
|
|
$input.nextAll(".form-text").last().after(
|
|
`<div class="invalid-feedback">${value}</div>`
|
|
)
|
|
}
|
|
}
|
|
|
|
|
|
// region Table Col Types
|
|
|
|
function renderEditColumn(data) {
|
|
const name = sanitise(data);
|
|
return `<span class="act-as-link edit-modal" role="button">${name}</span>`
|
|
}
|
|
|
|
function renderAnchorColumn(name, href) {
|
|
name = sanitise(name);
|
|
href = sanitise(href);
|
|
|
|
return `<a href="${href}" class="act-as-link">${name}</a>`;
|
|
}
|
|
|
|
function renderBooleanColumn(data) {
|
|
const iconClass = data ? "bi-check-circle-fill text-success" : "bi-x-circle-fill text-danger";
|
|
return `<i class="bi ${iconClass}"></i>`;
|
|
}
|
|
|
|
function renderBadgeColumn(data, colour=null) {
|
|
let badge = $(`<span class="badge text-bg-secondary rounded-1 border border-1">${data}</span>`)
|
|
if (colour) {
|
|
badge[0].style.setProperty("border-color", `#${colour}`, "important");
|
|
}
|
|
|
|
return badge.prop("outerHTML");
|
|
}
|
|
|
|
function renderArrayBadgesColumn(data) {
|
|
let badges = $("<div>");
|
|
|
|
data.forEach((item, index) => {
|
|
let badge = $(`<span class="badge text-bg-secondary rounded-1">${item}</span>`);
|
|
if (index > 0) { badge.addClass("ms-2") }
|
|
badges.append(badge);
|
|
});
|
|
|
|
return badges.html();
|
|
}
|
|
|
|
function renderArrayDropdownColumn(data, icon, headerText) {
|
|
if (!data.length) {
|
|
return "";
|
|
}
|
|
|
|
let $dropdown = $(`
|
|
<div class="dropdown">
|
|
<button type="button" class="dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fs-5 bi ${icon}"></i>
|
|
</button>
|
|
<div class="dropdown-menu">
|
|
<li><h6 class="dropdown-header">${headerText} (${data.length})</h6></li>
|
|
</div>
|
|
</div>
|
|
`);
|
|
let $dropdownItems = $dropdown.find(".dropdown-menu")
|
|
|
|
data.forEach(item => {
|
|
let $itemBtn = $(`<li><button type="button" class="dropdown-item">${item}</button></li>`);
|
|
$dropdownItems.append($itemBtn);
|
|
});
|
|
|
|
return $dropdown.prop("outerHTML");
|
|
}
|
|
|
|
const renderHexColourColumn = data => {
|
|
const hexWithHashtag = `#${data}`.toUpperCase();
|
|
|
|
let icon = $("<div>");
|
|
icon.addClass("col-hex-icon");
|
|
icon.css("background-color", hexWithHashtag);
|
|
|
|
return $(`<span data-bs-toggle="tooltip" data-bs-title="${hexWithHashtag}">${icon.prop("outerHTML")}</span>`).tooltip()[0];
|
|
}
|
|
|
|
function renderMutatorColumn(data) {
|
|
if (!("id" in data)) {
|
|
return "";
|
|
}
|
|
|
|
return $(`<span class="badge text-bg-secondary rounded-1">${data.name}</span>`).prop("outerHTML");
|
|
}
|
|
|
|
const renderLinkToStyleColumn = style => {
|
|
const hexWithHashtag = `#${style.colour}`.toUpperCase();
|
|
|
|
let icon = $("<div>");
|
|
icon.addClass("col-hex-icon js-openSubStyle");
|
|
icon.css("background-color", hexWithHashtag);
|
|
|
|
return $(`<span data-bs-toggle="tooltip" data-bs-title="${hexWithHashtag}" role="button">${icon.prop("outerHTML")}</span>`).tooltip()[0];
|
|
}
|
|
|
|
const renderPopoverBadgesColumn = (items, iconClass) => {
|
|
if (!items.length) {
|
|
return "";
|
|
}
|
|
|
|
let $span = $("<span>");
|
|
$span.attr("data-bs-toggle", "popover");
|
|
$span.attr("data-bs-trigger", "hover focus");
|
|
$span.attr("data-bs-custom-class", "table-badge-popover")
|
|
$span.attr("data-bs-html", "true");
|
|
|
|
let $placeholderContainer = $("<div>");
|
|
|
|
items.forEach(item => {
|
|
let $badge = $("<div>");
|
|
$badge.addClass("badge text-bg-secondary rounded-1 me-2 mb-2 text-wrap mw-100 text-start");
|
|
$badge.text(item);
|
|
$placeholderContainer.append($badge);
|
|
});
|
|
|
|
$span.attr("data-bs-content", $placeholderContainer.html());
|
|
$span.html(`<i class="bi ${iconClass} fs-5"></i>`)
|
|
return $span.popover()[0];
|
|
}
|
|
|
|
|
|
// region Get Table Parts
|
|
|
|
function getTableFiltersComponent(tableId) {
|
|
return $(tableId).closest(".js-tableBody").siblings(".js-tableFilters");
|
|
}
|
|
|
|
function getTableControlsComponent(tableId) {
|
|
return $(tableId).closest(".js-tableBody").siblings(".js-tableControls");
|
|
}
|
|
|
|
function getSelectedTableRows(tableId) {
|
|
return $(tableId).DataTable().rows(".selected").data().toArray();
|
|
} |