mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-31 05:43:41 +00:00 
			
		
		
		
	feat: get rid of jquery (#133)
This commit is contained in:
		| @ -44,28 +44,28 @@ | ||||
|                     </button> | ||||
|                     <ul class="dropdown-menu"> | ||||
|                         <li> | ||||
|                             <button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden> | ||||
|                             <button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal"> | ||||
|                                 <i class="bi bi-plus"></i> add | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden> | ||||
|                             <button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()"> | ||||
|                                 <i class="bi bi-play"></i> update | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden> | ||||
|                             <button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal"> | ||||
|                                 <i class="bi bi-arrow-clockwise"></i> rebuild | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden> | ||||
|                             <button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled> | ||||
|                                 <i class="bi bi-trash"></i> remove | ||||
|                             </button> | ||||
|                         </li> | ||||
|                     </ul> | ||||
|  | ||||
|                     <button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden> | ||||
|                     <button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal"> | ||||
|                         <i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span> | ||||
|                     </button> | ||||
|                 {% endif %} | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| <script> | ||||
|     const alertPlaceholder = $("#alert-placeholder"); | ||||
|     const alertPlaceholder = document.getElementById("alert-placeholder"); | ||||
|  | ||||
|     function createAlert(title, message, clz, action, id) { | ||||
|         if (!id) id = $.md5(title + message); // MD5 id from the content | ||||
|         if (alertPlaceholder.find(`#${id}`).length > 0) return; // check if there are duplicates | ||||
|         id ??= md5(title + message); // MD5 id from the content | ||||
|         if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates | ||||
|  | ||||
|         const wrapper = document.createElement("div"); | ||||
|         wrapper.id = id; | ||||
|         wrapper.id = `alert-${id}`; | ||||
|         wrapper.classList.add("toast", clz); | ||||
|         wrapper.role = "alert"; | ||||
|         wrapper.ariaLive = "assertive"; | ||||
| @ -23,7 +23,7 @@ | ||||
|         body.innerText = message; | ||||
|         wrapper.appendChild(body); | ||||
|  | ||||
|         alertPlaceholder.append(wrapper); | ||||
|         alertPlaceholder.appendChild(wrapper); | ||||
|         const toast = new bootstrap.Toast(wrapper); | ||||
|         wrapper.addEventListener("hidden.bs.toast", _ => { | ||||
|             wrapper.remove();  // bootstrap doesn't remove elements | ||||
| @ -32,12 +32,12 @@ | ||||
|         toast.show(); | ||||
|     } | ||||
|  | ||||
|     function showFailure(title, description, jqXHR, errorThrown) { | ||||
|     function showFailure(title, description, error) { | ||||
|         let details; | ||||
|         try { | ||||
|             details = $.parseJSON(jqXHR.responseText).error; // execution handler json error response | ||||
|             details = JSON.parse(error.text).error; // execution handler json error response | ||||
|         } catch (_) { | ||||
|             details = errorThrown; | ||||
|             details = error.text ?? error.message ?? error; | ||||
|         } | ||||
|         createAlert(title, description(details), "text-bg-danger"); | ||||
|     } | ||||
|  | ||||
| @ -36,61 +36,69 @@ | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     const keyImportModal = $("#key-import-modal"); | ||||
|     const keyImportForm = $("#key-import-form"); | ||||
|     const keyImportModal = document.getElementById("key-import-modal"); | ||||
|     const keyImportForm = document.getElementById("key-import-form"); | ||||
|  | ||||
|     const keyImportBodyInput = $("#key-import-body-input"); | ||||
|     const keyImportCopyButton = $("#key-import-copy-button"); | ||||
|     const keyImportBodyInput = document.getElementById("key-import-body-input"); | ||||
|     const keyImportCopyButton = document.getElementById("key-import-copy-button"); | ||||
|  | ||||
|     const keyImportFingerprintInput = $("#key-import-fingerprint-input"); | ||||
|     const keyImportServerInput = $("#key-import-server-input"); | ||||
|     const keyImportFingerprintInput = document.getElementById("key-import-fingerprint-input"); | ||||
|     const keyImportServerInput = document.getElementById("key-import-server-input"); | ||||
|  | ||||
|     async function copyPgpKey() { | ||||
|         const logs = keyImportBodyInput.text(); | ||||
|         await copyToClipboard(logs, keyImportCopyButton); | ||||
|         const key = keyImportBodyInput.textContent; | ||||
|         await copyToClipboard(key, keyImportCopyButton); | ||||
|     } | ||||
|  | ||||
|     function fetchPgpKey() { | ||||
|         const key = keyImportFingerprintInput.val(); | ||||
|         const server = keyImportServerInput.val(); | ||||
|         const key = keyImportFingerprintInput.value; | ||||
|         const server = keyImportServerInput.value; | ||||
|  | ||||
|         if (key && server) { | ||||
|             $.ajax({ | ||||
|                 url: "/api/v1/service/pgp", | ||||
|                 data: {"key": key, "server": server}, | ||||
|                 type: "GET", | ||||
|                 dataType: "json", | ||||
|                 success: response => { keyImportBodyInput.text(response.key); }, | ||||
|             }); | ||||
|             makeRequest( | ||||
|                 "/api/v1/service/pgp", | ||||
|                 { | ||||
|                     query: { | ||||
|                         key: key, | ||||
|                         server: server, | ||||
|                     }, | ||||
|                     convert: response => response.json(), | ||||
|                 }, | ||||
|                 data => { keyImportBodyInput.textContent = data.key; }, | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function importPgpKey() { | ||||
|         const key = keyImportFingerprintInput.val(); | ||||
|         const server = keyImportServerInput.val(); | ||||
|         const key = keyImportFingerprintInput.value; | ||||
|         const server = keyImportServerInput.value; | ||||
|  | ||||
|         if (key && server) { | ||||
|             $.ajax({ | ||||
|                 url: "/api/v1/service/pgp", | ||||
|                 data: JSON.stringify({key: key, server: server}), | ||||
|                 type: "POST", | ||||
|                 contentType: "application/json", | ||||
|                 success: _ => { | ||||
|                     keyImportModal.modal("hide"); | ||||
|             makeRequest( | ||||
|                 "/api/v1/service/pgp", | ||||
|                 { | ||||
|                     method: "POST", | ||||
|                     json: { | ||||
|                         key: key, | ||||
|                         server: server, | ||||
|                     }, | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     bootstrap.Modal.getOrCreateInstance(keyImportModal).hide(); | ||||
|                     showSuccess("Success", `Key ${key} has been imported`); | ||||
|                 }, | ||||
|                 error: (jqXHR, _, errorThrown) => { | ||||
|                 error => { | ||||
|                     const message = _ => `Could not import key ${key} from ${server}`; | ||||
|                     showFailure("Action failed", message, jqXHR, errorThrown); | ||||
|                     showFailure("Action failed", message, error); | ||||
|                 }, | ||||
|             }); | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $(_ => { | ||||
|         keyImportModal.on("hidden.bs.modal", _ => { | ||||
|             keyImportBodyInput.text(""); | ||||
|             keyImportForm.trigger("reset"); | ||||
|     ready(_ => { | ||||
|         keyImportModal.addEventListener("hidden.bs.modal", _ => { | ||||
|             keyImportBodyInput.textContent = ""; | ||||
|             keyImportForm.reset(); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| @ -34,53 +34,57 @@ | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     const loginModal = $("#login-modal"); | ||||
|     const loginForm = $("#login-form"); | ||||
|     const loginModal = document.getElementById("login-modal"); | ||||
|     const loginForm = document.getElementById("login-form"); | ||||
|  | ||||
|     const loginPasswordInput = $("#login-password"); | ||||
|     const loginUsernameInput = $("#login-username"); | ||||
|     const showHidePasswordButton = $("#login-show-hide-password-button"); | ||||
|     const loginPasswordInput = document.getElementById("login-password"); | ||||
|     const loginUsernameInput = document.getElementById("login-username"); | ||||
|     const showHidePasswordButton = document.getElementById("login-show-hide-password-button"); | ||||
|  | ||||
|     function login() { | ||||
|         const password = loginPasswordInput.val(); | ||||
|         const username = loginUsernameInput.val(); | ||||
|         const password = loginPasswordInput.value; | ||||
|         const username = loginUsernameInput.value; | ||||
|  | ||||
|         if (username && password) { | ||||
|             $.ajax({ | ||||
|                 url: "/api/v1/login", | ||||
|                 data: JSON.stringify({username: username, password: password}), | ||||
|                 type: "POST", | ||||
|                 contentType: "application/json", | ||||
|                 success: _ => { | ||||
|                     loginModal.modal("hide"); | ||||
|             makeRequest( | ||||
|                 "/api/v1/login", | ||||
|                 { | ||||
|                     method: "POST", | ||||
|                     json: { | ||||
|                         username: username, | ||||
|                         password: password, | ||||
|                     }, | ||||
|                 }, | ||||
|                 _ => { | ||||
|                     bootstrap.Modal.getOrCreateInstance(loginModal).hide(); | ||||
|                     showSuccess("Logged in", `Successfully logged in as ${username}`, _ => location.href = "/"); | ||||
|                 }, | ||||
|                 error: (jqXHR, _, errorThrown) => { | ||||
|                 error => { | ||||
|                     const message = _ => | ||||
|                         username === "admin" && password === "admin" | ||||
|                             ? "You've entered a password for user \"root\", did you make a typo in username?" | ||||
|                             : `Could not login as ${username}`; | ||||
|                     showFailure("Login error", message, jqXHR, errorThrown); | ||||
|                     showFailure("Login error", message, error); | ||||
|                 }, | ||||
|             }); | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function showPassword() { | ||||
|         if (loginPasswordInput.attr("type") === "password") { | ||||
|             loginPasswordInput.attr("type", "text"); | ||||
|             showHidePasswordButton.removeClass("bi-eye"); | ||||
|             showHidePasswordButton.addClass("bi-eye-slash"); | ||||
|         if (loginPasswordInput.getAttribute("type") === "password") { | ||||
|             loginPasswordInput.setAttribute("type", "text"); | ||||
|             showHidePasswordButton.classList.remove("bi-eye"); | ||||
|             showHidePasswordButton.classList.add("bi-eye-slash"); | ||||
|         } else { | ||||
|             loginPasswordInput.attr("type", "password"); | ||||
|             showHidePasswordButton.removeClass("bi-eye-slash"); | ||||
|             showHidePasswordButton.addClass("bi-eye"); | ||||
|             loginPasswordInput.setAttribute("type", "password"); | ||||
|             showHidePasswordButton.classList.remove("bi-eye-slash"); | ||||
|             showHidePasswordButton.classList.add("bi-eye"); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $(_ => { | ||||
|         loginModal.on("hidden.bs.modal", _ => { | ||||
|             loginForm.trigger("reset"); | ||||
|     ready(_ => { | ||||
|         loginModal.addEventListener("hidden.bs.modal", _ => { | ||||
|             loginForm.reset(); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| @ -41,14 +41,14 @@ | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     const packageAddModal = $("#package-add-modal"); | ||||
|     const packageAddForm = $("#package-add-form"); | ||||
|     const packageAddModal = document.getElementById("package-add-modal"); | ||||
|     const packageAddForm = document.getElementById("package-add-form"); | ||||
|  | ||||
|     const packageAddInput = $("#package-add-input"); | ||||
|     const packageAddRepositoryInput = $("#package-add-repository-input"); | ||||
|     const packageAddKnownPackagesList = $("#package-add-known-packages-dlist"); | ||||
|     const packageAddInput = document.getElementById("package-add-input"); | ||||
|     const packageAddRepositoryInput = document.getElementById("package-add-repository-input"); | ||||
|     const packageAddKnownPackagesList = document.getElementById("package-add-known-packages-dlist"); | ||||
|  | ||||
|     const packageAddVariablesDiv = $("#package-add-variables-div"); | ||||
|     const packageAddVariablesDiv = document.getElementById("package-add-variables-div"); | ||||
|  | ||||
|     function packageAddVariableInputCreate() { | ||||
|         const variableInput = document.createElement("div"); | ||||
| @ -78,7 +78,7 @@ | ||||
|         variableButtonRemove.classList.add("btn"); | ||||
|         variableButtonRemove.classList.add("btn-outline-danger"); | ||||
|         variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; | ||||
|         variableButtonRemove.onclick = _ => { return variableInput.remove(); }; | ||||
|         variableButtonRemove.onclick = _ => { variableInput.remove(); }; | ||||
|  | ||||
|         // bring them together | ||||
|         variableInput.appendChild(variableNameInput); | ||||
| @ -86,27 +86,26 @@ | ||||
|         variableInput.appendChild(variableValueInput); | ||||
|         variableInput.appendChild(variableButtonRemove); | ||||
|  | ||||
|         packageAddVariablesDiv.append(variableInput); | ||||
|         packageAddVariablesDiv.appendChild(variableInput); | ||||
|     } | ||||
|  | ||||
|     function patchesParse() { | ||||
|         const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => { | ||||
|             const richElement = $(element); | ||||
|         const patches = Array.from(packageAddVariablesDiv.getElementsByClassName("package-add-variable")).map(element => { | ||||
|             return { | ||||
|                 key: richElement.find(".package-add-variable-name").val(), | ||||
|                 value: richElement.find(".package-add-variable-value").val(), | ||||
|                 key: element.querySelector(".package-add-variable-name").value, | ||||
|                 value: element.querySelector(".package-add-variable-value").value, | ||||
|             }; | ||||
|         }).filter((_, patch) => patch.key).get(); | ||||
|         }).filter(patch => patch.key); | ||||
|         return {patches: patches}; | ||||
|     } | ||||
|  | ||||
|     function packagesAdd(packages, patches, repository) { | ||||
|         packages = packages ?? packageAddInput.val(); | ||||
|         packages = packages ?? packageAddInput.value; | ||||
|         patches = patches ?? patchesParse(); | ||||
|         repository = repository ?? getRepositorySelector(packageAddRepositoryInput); | ||||
|  | ||||
|         if (packages) { | ||||
|             packageAddModal.modal("hide"); | ||||
|             bootstrap.Modal.getOrCreateInstance(packageAddModal).hide(); | ||||
|             const onSuccess = update => `Packages ${update} have been added`; | ||||
|             const onFailure = error => `Package addition failed: ${error}`; | ||||
|             doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches); | ||||
| @ -114,50 +113,54 @@ | ||||
|     } | ||||
|  | ||||
|     function packagesRequest(packages, patches) { | ||||
|         packages = packages ?? packageAddInput.val(); | ||||
|         packages = packages ?? packageAddInput.value; | ||||
|         patches = patches ?? patchesParse(); | ||||
|         const repository = getRepositorySelector(packageAddRepositoryInput); | ||||
|  | ||||
|         if (packages) { | ||||
|             packageAddModal.modal("hide"); | ||||
|             bootstrap.Modal.getOrCreateInstance(packageAddModal).hide(); | ||||
|             const onSuccess = update => `Packages ${update} have been requested`; | ||||
|             const onFailure = error => `Package request failed: ${error}`; | ||||
|             doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $(_ => { | ||||
|         packageAddModal.on("shown.bs.modal", _ => { | ||||
|             $(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true); | ||||
|     ready(_ => { | ||||
|         packageAddModal.addEventListener("shown.bs.modal", _ => { | ||||
|             const option = packageAddRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`); | ||||
|             option.selected = "selected"; | ||||
|         }); | ||||
|         packageAddModal.on("hidden.bs.modal", _ => { | ||||
|             packageAddVariablesDiv.empty(); | ||||
|             packageAddForm.trigger("reset"); | ||||
|         packageAddModal.addEventListener("hidden.bs.modal", _ => { | ||||
|             packageAddVariablesDiv.replaceChildren(); | ||||
|             packageAddForm.reset(); | ||||
|         }); | ||||
|  | ||||
|         packageAddInput.keyup(_ => { | ||||
|             clearTimeout(packageAddInput.data("timeout")); | ||||
|             packageAddInput.data("timeout", setTimeout($.proxy(_ => { | ||||
|                 const value = packageAddInput.val(); | ||||
|         packageAddInput.addEventListener("keyup", _ => { | ||||
|             clearTimeout(packageAddInput.requestTimeout); | ||||
|             packageAddInput.requestTimeout = setTimeout(_ => { | ||||
|                 const value = packageAddInput.value; | ||||
|  | ||||
|                 if (value.length >= 3) { | ||||
|                     $.ajax({ | ||||
|                         url: "/api/v1/service/search", | ||||
|                         data: {"for": value}, | ||||
|                         type: "GET", | ||||
|                         dataType: "json", | ||||
|                         success: response => { | ||||
|                             const options = response.map(pkg => { | ||||
|                     makeRequest( | ||||
|                         "/api/v1/service/search", | ||||
|                         { | ||||
|                             query: { | ||||
|                                 for: value, | ||||
|                             }, | ||||
|                             convert: response => response.json(), | ||||
|                         }, | ||||
|                         data => { | ||||
|                             const options = data.map(pkg => { | ||||
|                                 const option = document.createElement("option"); | ||||
|                                 option.value = pkg.package; | ||||
|                                 option.innerText = `${pkg.package} (${pkg.description})`; | ||||
|                                 return option; | ||||
|                             }); | ||||
|                             packageAddKnownPackagesList.empty().append(options); | ||||
|                             packageAddKnownPackagesList.replaceChildren(...options); | ||||
|                         }, | ||||
|                     }); | ||||
|                     ); | ||||
|                 } | ||||
|             }, this), 500)); | ||||
|             }, 500); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| @ -58,7 +58,7 @@ | ||||
|                         <pre class="language-diff"><code id="package-info-changes-input" class="pre-scrollable language-diff"></code><button id="package-info-changes-copy-button" type="button" class="btn language-diff" onclick="copyChanges()"><i class="bi bi-clipboard"></i> copy</button></pre> | ||||
|                     </div> | ||||
|                     <div id="package-info-events" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-events-button" tabindex="0"> | ||||
|                         <canvas id="package-info-events-update-chart"></canvas> | ||||
|                         <canvas id="package-info-events-update-chart" hidden></canvas> | ||||
|                         <table id="package-info-events-table" | ||||
|                                data-classes="table table-hover" | ||||
|                                data-sortable="true" | ||||
| @ -77,8 +77,10 @@ | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="modal-footer"> | ||||
|                 <button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal" hidden><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button> | ||||
|                 <button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal" hidden><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button> | ||||
|                 {% if not auth.enabled or auth.username is not none %} | ||||
|                     <button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button> | ||||
|                     <button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button> | ||||
|                 {% endif %} | ||||
|                 <button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button> | ||||
|                 <button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i><span class="d-none d-sm-inline"> close</span></button> | ||||
|             </div> | ||||
| @ -87,33 +89,35 @@ | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     const packageInfoModal = $("#package-info-modal"); | ||||
|     const packageInfoModalHeader = $("#package-info-modal-header"); | ||||
|     const packageInfo = $("#package-info"); | ||||
|     const packageInfoModal = document.getElementById("package-info-modal"); | ||||
|     const packageInfoModalHeader = document.getElementById("package-info-modal-header"); | ||||
|     const packageInfo = document.getElementById("package-info"); | ||||
|  | ||||
|     const packageInfoLogsInput = $("#package-info-logs-input"); | ||||
|     const packageInfoLogsCopyButton = $("#package-info-logs-copy-button"); | ||||
|     const packageInfoLogsInput = document.getElementById("package-info-logs-input"); | ||||
|     const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button"); | ||||
|  | ||||
|     const packageInfoChangesInput = $("#package-info-changes-input"); | ||||
|     const packageInfoChangesCopyButton = $("#package-info-changes-copy-button"); | ||||
|     const packageInfoChangesInput = document.getElementById("package-info-changes-input"); | ||||
|     const packageInfoChangesCopyButton = document.getElementById("package-info-changes-copy-button"); | ||||
|  | ||||
|     const packageInfoEventsTable = $("#package-info-events-table"); | ||||
|     // so far bootstrap-table only operates with jquery elements | ||||
|     const packageInfoEventsTable = $(document.getElementById("package-info-events-table")); | ||||
|     const packageInfoEventsUpdateChartCanvas = document.getElementById("package-info-events-update-chart"); | ||||
|     let packageInfoEventsUpdateChart = null; | ||||
|  | ||||
|     const packageInfoAurUrl = $("#package-info-aur-url"); | ||||
|     const packageInfoDepends = $("#package-info-depends"); | ||||
|     const packageInfoGroups = $("#package-info-groups"); | ||||
|     const packageInfoLicenses = $("#package-info-licenses"); | ||||
|     const packageInfoPackager = $("#package-info-packager"); | ||||
|     const packageInfoPackages = $("#package-info-packages"); | ||||
|     const packageInfoUpstreamUrl = $("#package-info-upstream-url"); | ||||
|     const packageInfoVersion = $("#package-info-version"); | ||||
|     const packageInfoAurUrl = document.getElementById("package-info-aur-url"); | ||||
|     const packageInfoDepends = document.getElementById("package-info-depends"); | ||||
|     const packageInfoGroups = document.getElementById("package-info-groups"); | ||||
|     const packageInfoLicenses = document.getElementById("package-info-licenses"); | ||||
|     const packageInfoPackager = document.getElementById("package-info-packager"); | ||||
|     const packageInfoPackages = document.getElementById("package-info-packages"); | ||||
|     const packageInfoUpstreamUrl = document.getElementById("package-info-upstream-url"); | ||||
|     const packageInfoVersion = document.getElementById("package-info-version"); | ||||
|  | ||||
|     const packageInfoVariablesBlock = $("#package-info-variables-block"); | ||||
|     const packageInfoVariablesDiv = $("#package-info-variables-div"); | ||||
|     const packageInfoVariablesBlock = document.getElementById("package-info-variables-block"); | ||||
|     const packageInfoVariablesDiv = document.getElementById("package-info-variables-div"); | ||||
|  | ||||
|     function clearChart() { | ||||
|         packageInfoEventsUpdateChartCanvas.hidden = true; | ||||
|         if (packageInfoEventsUpdateChart) { | ||||
|             packageInfoEventsUpdateChart.data = {}; | ||||
|             packageInfoEventsUpdateChart.update(); | ||||
| @ -121,20 +125,15 @@ | ||||
|     } | ||||
|  | ||||
|     async function copyChanges() { | ||||
|         const changes = packageInfoChangesInput.text(); | ||||
|         const changes = packageInfoChangesInput.textContent; | ||||
|         await copyToClipboard(changes, packageInfoChangesCopyButton); | ||||
|     } | ||||
|  | ||||
|     async function copyLogs() { | ||||
|         const logs = packageInfoLogsInput.text(); | ||||
|         const logs = packageInfoLogsInput.textContent; | ||||
|         await copyToClipboard(logs, packageInfoLogsCopyButton); | ||||
|     } | ||||
|  | ||||
|     function hideInfoControls(hidden) { | ||||
|         packageInfoRemoveButton.attr("hidden", hidden); | ||||
|         packageInfoUpdateButton.attr("hidden", hidden); | ||||
|     } | ||||
|  | ||||
|     function highlight(element) { | ||||
|         delete element.dataset.highlighted; | ||||
|         hljs.highlightElement(element); | ||||
| @ -164,12 +163,13 @@ | ||||
|         variableButtonRemove.classList.add("btn-outline-danger"); | ||||
|         variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; | ||||
|         variableButtonRemove.onclick = _ => { | ||||
|             $.ajax({ | ||||
|                 url: `/api/v1/packages/${packageBase}/patches/${variable.key}`, | ||||
|                 type: "DELETE", | ||||
|                 dataType: "json", | ||||
|                 success: _ => variableInput.remove(), | ||||
|             }); | ||||
|             makeRequest( | ||||
|                 `/api/v1/packages/${packageBase}/patches/${variable.key}`, | ||||
|                 { | ||||
|                     method: "DELETE", | ||||
|                 }, | ||||
|                 _ => variableInput.remove(), | ||||
|             ); | ||||
|         }; | ||||
|  | ||||
|         // bring them together | ||||
| @ -178,52 +178,57 @@ | ||||
|         variableInput.appendChild(variableValueInput); | ||||
|         variableInput.appendChild(variableButtonRemove); | ||||
|  | ||||
|         packageInfoVariablesDiv.append(variableInput); | ||||
|         packageInfoVariablesDiv.appendChild(variableInput); | ||||
|     } | ||||
|  | ||||
|     function loadChanges(packageBase, onFailure) { | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/packages/${packageBase}/changes`, | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|         makeRequest( | ||||
|             `/api/v1/packages/${packageBase}/changes`, | ||||
|             { | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                 }, | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const changes = response.changes; | ||||
|                 packageInfoChangesInput.text(changes || ""); | ||||
|                 packageInfoChangesInput.map((_, el) => highlight(el)); | ||||
|             data => { | ||||
|                 const changes = data.changes; | ||||
|                 packageInfoChangesInput.textContent = changes ?? ""; | ||||
|                 highlight(packageInfoChangesInput); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|             onFailure, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function loadEvents(packageBase, onFailure) { | ||||
|         packageInfoEventsTable.bootstrapTable("showLoading"); | ||||
|         clearChart(); | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/events`, | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|                 object_id: packageBase, | ||||
|                 limit: 30, | ||||
|         makeRequest( | ||||
|             "/api/v1/events", | ||||
|             { | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                     object_id: packageBase, | ||||
|                     limit: 30, | ||||
|                 }, | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const events = response.map(event => { | ||||
|             data => { | ||||
|                 const events = data.map(event => { | ||||
|                     return { | ||||
|                         timestamp: new Date(1000 * event.created).toISOStringShort(), | ||||
|                         event: event.event, | ||||
|                         message: event.message || "", | ||||
|                     }; | ||||
|                 }); | ||||
|                 const chart = data.filter(event => event.event === "package-updated"); | ||||
|  | ||||
|                 packageInfoEventsTable.bootstrapTable("load", events); | ||||
|                 packageInfoEventsTable.bootstrapTable("hideLoading"); | ||||
|  | ||||
|                 if (packageInfoEventsUpdateChart) { | ||||
|                     const chart = response.filter(event => event.event === "package-updated"); | ||||
|                     packageInfoEventsUpdateChart.config.data = { | ||||
|                         labels: chart.map(event => new Date(1000 * event.created).toISOStringShort()), | ||||
|                         datasets: [{ | ||||
| @ -235,32 +240,31 @@ | ||||
|                     }; | ||||
|                     packageInfoEventsUpdateChart.update(); | ||||
|                 } | ||||
|  | ||||
|                 packageInfoEventsTable.bootstrapTable("load", events); | ||||
|                 packageInfoEventsTable.bootstrapTable("hideLoading"); | ||||
|                 packageInfoEventsUpdateChartCanvas.hidden = !chart.length; | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|             onFailure, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function loadLogs(packageBase, onFailure) { | ||||
|         $.ajax({ | ||||
|             url: `/api/v2/packages/${packageBase}/logs`, | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|         makeRequest( | ||||
|             `/api/v2/packages/${packageBase}/logs`, | ||||
|             { | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                 }, | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const logs = response.map(log_record => { | ||||
|             data => { | ||||
|                 const logs = data.map(log_record => { | ||||
|                     return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`; | ||||
|                 }); | ||||
|                 packageInfoLogsInput.text(logs.join("\n")); | ||||
|                 packageInfoLogsInput.map((_, el) => highlight(el)); | ||||
|                 packageInfoLogsInput.textContent = logs.join("\n"); | ||||
|                 highlight(packageInfoLogsInput); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|             onFailure, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function loadPackage(packageBase, onFailure) { | ||||
| @ -272,16 +276,17 @@ | ||||
|             return ["bg-secondary", "text-white"]; | ||||
|         }; | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/packages/${packageBase}`, | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|         makeRequest( | ||||
|             `/api/v1/packages/${packageBase}`, | ||||
|             { | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                 }, | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const description = response.find(Boolean); | ||||
|             data => { | ||||
|                 const description = data.find(Boolean); | ||||
|                 const packages = Object.keys(description.package.packages); | ||||
|                 const aurUrl = description.package.remote.web_url; | ||||
|                 const upstreamUrls = Array.from( | ||||
| @ -291,72 +296,71 @@ | ||||
|                     ) | ||||
|                 ).sort(); | ||||
|  | ||||
|                 packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`); | ||||
|                 packageInfo.textContent = `${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`; | ||||
|  | ||||
|                 packageInfoModalHeader.removeClass(); | ||||
|                 packageInfoModalHeader.addClass("modal-header"); | ||||
|                 headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz)); | ||||
|                 packageInfoModalHeader.classList.remove(...packageInfoModalHeader.classList); | ||||
|                 packageInfoModalHeader.classList.add("modal-header"); | ||||
|                 headerClass(description.status.status).forEach(clz => packageInfoModalHeader.classList.add(clz)); | ||||
|  | ||||
|                 packageInfoAurUrl.html(aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : ""); | ||||
|                 packageInfoDepends.html(listToTable( | ||||
|                 packageInfoAurUrl.innerHTML = aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : ""; | ||||
|                 packageInfoDepends.innerHTML = listToTable( | ||||
|                     Object.values(description.package.packages) | ||||
|                         .reduce((accumulator, currentValue) => { | ||||
|                             return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1)) | ||||
|                                 .concat(currentValue.make_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (make)`)) | ||||
|                                 .concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`)); | ||||
|                         }, []) | ||||
|                 )); | ||||
|                 packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups"))); | ||||
|                 packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses"))); | ||||
|                 packageInfoPackager.text(description.package.packager); | ||||
|                 packageInfoPackages.html(listToTable(packages)); | ||||
|                 packageInfoUpstreamUrl.html(upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>")); | ||||
|                 packageInfoVersion.text(description.package.version); | ||||
|  | ||||
|                 hideInfoControls(false); | ||||
|                 ); | ||||
|                 packageInfoGroups.innerHTML = listToTable(extractListProperties(description.package, "groups")); | ||||
|                 packageInfoLicenses.innerHTML = listToTable(extractListProperties(description.package, "licenses")); | ||||
|                 packageInfoPackager.textContent = description.package.packager; | ||||
|                 packageInfoPackages.innerHTML = listToTable(packages); | ||||
|                 packageInfoUpstreamUrl.innerHTML = upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>"); | ||||
|                 packageInfoVersion.textContent = description.package.version; | ||||
|             }, | ||||
|             error: (jqXHR, _, errorThrown) => { | ||||
|                 hideInfoControls(true); | ||||
|                 onFailure(jqXHR, null, errorThrown); | ||||
|             }, | ||||
|         }); | ||||
|             onFailure, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function loadPatches(packageBase, onFailure) { | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/packages/${packageBase}/patches`, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 packageInfoVariablesDiv.empty(); | ||||
|                 response.map(patch => insertVariable(packageBase, patch)); | ||||
|                 packageInfoVariablesBlock.attr("hidden", response.length === 0); | ||||
|         makeRequest( | ||||
|             `/api/v1/packages/${packageBase}/patches`, | ||||
|             { | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|             data => { | ||||
|                 packageInfoVariablesDiv.replaceChildren(); | ||||
|                 data.map(patch => insertVariable(packageBase, patch)); | ||||
|                 packageInfoVariablesBlock.hidden = !data.length; | ||||
|             }, | ||||
|             onFailure, | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function packageInfoRemove() { | ||||
|         const packageBase = packageInfoModal.data("package"); | ||||
|         if (packageBase) return packagesRemove([packageBase]); | ||||
|         const packageBase = packageInfoModal.package; | ||||
|         packagesRemove([packageBase]); | ||||
|     } | ||||
|  | ||||
|     function packageInfoUpdate() { | ||||
|         const packageBase = packageInfoModal.data("package"); | ||||
|         if (packageBase) return packagesAdd(packageBase, [], repository); | ||||
|         const packageBase = packageInfoModal.package; | ||||
|         packagesAdd(packageBase, [], repository); | ||||
|     } | ||||
|  | ||||
|     function showPackageInfo(packageBase) { | ||||
|         const isPackageBaseSet = packageBase !== undefined; | ||||
|         if (isPackageBaseSet) | ||||
|             packageInfoModal.data("package", packageBase); // set package base as currently used | ||||
|         else | ||||
|             packageBase = packageInfoModal.data("package"); // read package base from the current window attribute | ||||
|         if (isPackageBaseSet) { | ||||
|             // set package base as currently used | ||||
|             packageInfoModal.package = packageBase; | ||||
|         } else { | ||||
|             // read package base from the current window attribute | ||||
|             packageBase = packageInfoModal.package; | ||||
|         } | ||||
|  | ||||
|         const onFailure = (jqXHR, _, errorThrown) => { | ||||
|         const onFailure = error => { | ||||
|             if (isPackageBaseSet) { | ||||
|                 const message = error => `Could not load package ${packageBase} info: ${error}`; | ||||
|                 showFailure("Load failure", message, jqXHR, errorThrown); | ||||
|                 const message = details => `Could not load package ${packageBase} info: ${details}`; | ||||
|                 showFailure("Load failure", message, error); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
| @ -366,10 +370,12 @@ | ||||
|         loadChanges(packageBase, onFailure); | ||||
|         loadEvents(packageBase, onFailure); | ||||
|  | ||||
|         if (isPackageBaseSet) packageInfoModal.modal("show"); | ||||
|         if (isPackageBaseSet) { | ||||
|             bootstrap.Modal.getOrCreateInstance(packageInfoModal).show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $(_ => { | ||||
|     ready(_ => { | ||||
|         packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, { | ||||
|             type: "line", | ||||
|             data: {}, | ||||
| @ -378,27 +384,23 @@ | ||||
|             }, | ||||
|         }); | ||||
|  | ||||
|         packageInfoModal.on("hidden.bs.modal", _ => { | ||||
|             packageInfoAurUrl.empty(); | ||||
|             packageInfoDepends.empty(); | ||||
|             packageInfoGroups.empty(); | ||||
|             packageInfoLicenses.empty(); | ||||
|             packageInfoPackager.empty(); | ||||
|             packageInfoPackages.empty(); | ||||
|             packageInfoUpstreamUrl.empty(); | ||||
|             packageInfoVersion.empty(); | ||||
|         packageInfoModal.addEventListener("hidden.bs.modal", _ => { | ||||
|             packageInfoAurUrl.textContent = ""; | ||||
|             packageInfoDepends.textContent = ""; | ||||
|             packageInfoGroups.textContent = ""; | ||||
|             packageInfoLicenses.textContent = ""; | ||||
|             packageInfoPackager.textContent = ""; | ||||
|             packageInfoPackages.textContent = ""; | ||||
|             packageInfoUpstreamUrl.textContent = ""; | ||||
|             packageInfoVersion.textContent = ""; | ||||
|  | ||||
|             packageInfoVariablesBlock.attr("hidden", true); | ||||
|             packageInfoVariablesDiv.empty(); | ||||
|             packageInfoVariablesBlock.hidden = true; | ||||
|             packageInfoVariablesDiv.replaceChildren(); | ||||
|  | ||||
|             packageInfoLogsInput.empty(); | ||||
|             packageInfoChangesInput.empty(); | ||||
|             packageInfoLogsInput.textContent = ""; | ||||
|             packageInfoChangesInput.textContent = ""; | ||||
|             packageInfoEventsTable.bootstrapTable("load", []); | ||||
|             clearChart(); | ||||
|  | ||||
|             packageInfoModal.trigger("reset"); | ||||
|  | ||||
|             hideInfoControls(true); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| @ -33,28 +33,31 @@ | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     const packageRebuildModal = $("#package-rebuild-modal"); | ||||
|     const packageRebuildForm = $("#package-rebuild-form"); | ||||
|     const packageRebuildModal = document.getElementById("package-rebuild-modal"); | ||||
|     const packageRebuildForm = document.getElementById("package-rebuild-form"); | ||||
|  | ||||
|     const packageRebuildDependencyInput = $("#package-rebuild-dependency-input"); | ||||
|     const packageRebuildRepositoryInput = $("#package-rebuild-repository-input"); | ||||
|     const packageRebuildDependencyInput = document.getElementById("package-rebuild-dependency-input"); | ||||
|     const packageRebuildRepositoryInput = document.getElementById("package-rebuild-repository-input"); | ||||
|  | ||||
|     function packagesRebuild() { | ||||
|         const packages = packageRebuildDependencyInput.val(); | ||||
|         const packages = packageRebuildDependencyInput.value; | ||||
|         const repository = getRepositorySelector(packageRebuildRepositoryInput); | ||||
|         if (packages) { | ||||
|             packageRebuildModal.modal("hide"); | ||||
|             bootstrap.Modal.getOrCreateInstance(packageRebuildModal).hide(); | ||||
|             const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`; | ||||
|             const onFailure = error => `Repository rebuild failed: ${error}`; | ||||
|             doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $(_ => { | ||||
|         packageRebuildModal.on("shown.bs.modal", _ => { | ||||
|             $(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true); | ||||
|     ready(_ => { | ||||
|         packageRebuildModal.addEventListener("shown.bs.modal", _ => { | ||||
|             const option = packageRebuildRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`); | ||||
|             option.selected = "selected"; | ||||
|  | ||||
|         }); | ||||
|         packageRebuildModal.on("hidden.bs.modal", _ => { packageRebuildForm.trigger("reset"); }); | ||||
|         packageRebuildModal.addEventListener("hidden.bs.modal", _ => { | ||||
|             packageRebuildForm.reset(); | ||||
|         }); | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| @ -1,39 +1,34 @@ | ||||
| <script> | ||||
|     const keyImportButton = $("#key-import-button"); | ||||
|     const packageAddButton = $("#package-add-button"); | ||||
|     const packageRebuildButton = $("#package-rebuild-button"); | ||||
|     const packageRemoveButton = $("#package-remove-button"); | ||||
|     const packageUpdateButton = $("#package-update-button"); | ||||
|  | ||||
|     const packageInfoRemoveButton = $("#package-info-remove-button"); | ||||
|     const packageInfoUpdateButton = $("#package-info-update-button"); | ||||
|     const packageRemoveButton = document.getElementById("package-remove-button"); | ||||
|     const packageUpdateButton = document.getElementById("package-update-button"); | ||||
|  | ||||
|     let repository = null; | ||||
|  | ||||
|     const table = $("#packages"); | ||||
|     // so far bootstrap-table only operates with jquery elements | ||||
|     const table = $(document.getElementById("packages")); | ||||
|  | ||||
|     const statusBadge = $("#badge-status"); | ||||
|     const versionBadge = $("#badge-version"); | ||||
|     const statusBadge = document.getElementById("badge-status"); | ||||
|     const versionBadge = document.getElementById("badge-version"); | ||||
|  | ||||
|     function doPackageAction(uri, packages, repository, successText, failureText, data) { | ||||
|         const queryParams = $.param({ | ||||
|             architecture: repository.architecture, | ||||
|             repository: repository.repository, | ||||
|         }); // it will never be empty btw | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `${uri}?${queryParams}`, | ||||
|             data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})), | ||||
|             type: "POST", | ||||
|             contentType: "application/json", | ||||
|             success: _ => { | ||||
|         makeRequest( | ||||
|             uri, | ||||
|             { | ||||
|                 method: "POST", | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                 }, | ||||
|                 json: Object.assign({}, {packages: packages}, data || {}), | ||||
|             }, | ||||
|             _ => { | ||||
|                 const message = successText(packages.join(", ")); | ||||
|                 showSuccess("Success", message); | ||||
|             }, | ||||
|             error: (jqXHR, _, errorThrown) => { | ||||
|                 showFailure("Action failed", failureText, jqXHR, errorThrown); | ||||
|             error => { | ||||
|                 showFailure("Action failed", failureText, error); | ||||
|             }, | ||||
|         }); | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function filterListGroups() { | ||||
| @ -49,10 +44,10 @@ | ||||
|     } | ||||
|  | ||||
|     function getRepositorySelector(selector) { | ||||
|         const selected = selector.find(":selected"); | ||||
|         const selected = selector.options[selector.selectedIndex]; | ||||
|         return { | ||||
|             architecture: selected.data("architecture"), | ||||
|             repository: selected.data("repository"), | ||||
|             architecture: selected.getAttribute("data-architecture"), | ||||
|             repository: selected.getAttribute("data-repository"), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @ -60,14 +55,6 @@ | ||||
|         return table.bootstrapTable("getSelections").map(row => row.id); | ||||
|     } | ||||
|  | ||||
|     function hideControls(hidden) { | ||||
|         keyImportButton.attr("hidden", hidden); | ||||
|         packageAddButton.attr("hidden", hidden); | ||||
|         packageRebuildButton.attr("hidden", hidden); | ||||
|         packageRemoveButton.attr("hidden", hidden); | ||||
|         packageUpdateButton.attr("hidden", hidden); | ||||
|     } | ||||
|  | ||||
|     function packagesRemove(packages) { | ||||
|         packages = packages ?? getSelection(); | ||||
|         const onSuccess = update => `Packages ${update} have been removed`; | ||||
| @ -97,16 +84,17 @@ | ||||
|             return "btn-outline-secondary"; | ||||
|         }; | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: "/api/v1/packages", | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|         makeRequest( | ||||
|             "/api/v1/packages", | ||||
|             { | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                 }, | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const payload = response.map(description => { | ||||
|             data => { | ||||
|                 const payload = data.map(description => { | ||||
|                     const package_base = description.package.base; | ||||
|                     const web_url = description.package.remote.web_url; | ||||
|                     return { | ||||
| @ -125,10 +113,9 @@ | ||||
|                 table.bootstrapTable("load", payload); | ||||
|                 table.bootstrapTable("uncheckAll"); | ||||
|                 table.bootstrapTable("hideLoading"); | ||||
|                 hideControls(false); | ||||
|             }, | ||||
|             error: (jqXHR, _, errorThrown) => { | ||||
|                 if ((jqXHR.status === 401) || (jqXHR.status === 403)) { | ||||
|             error => { | ||||
|                 if ((error.status === 401) || (error.status === 403)) { | ||||
|                     // authorization error | ||||
|                     const text = "In order to see statuses you must login first."; | ||||
|                     table.find("tr.unauthorized").remove(); | ||||
| @ -136,39 +123,39 @@ | ||||
|                     table.bootstrapTable("hideLoading"); | ||||
|                 } else { | ||||
|                     // other errors | ||||
|                     const message = error => `Could not load list of packages: ${error}`; | ||||
|                     showFailure("Load failure", message, jqXHR, errorThrown); | ||||
|                     const message = details => `Could not load list of packages: ${details}`; | ||||
|                     showFailure("Load failure", message, error); | ||||
|                 } | ||||
|                 hideControls(true); | ||||
|             }, | ||||
|         }); | ||||
|         ); | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: "/api/v1/status", | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|         makeRequest( | ||||
|             "/api/v1/status", | ||||
|             { | ||||
|                 query: { | ||||
|                     architecture: repository.architecture, | ||||
|                     repository: repository.repository, | ||||
|                 }, | ||||
|                 convert: response => response.json(), | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 versionBadge.html(`<i class="bi bi-github"></i> ahriman ${safe(response.version)}`); | ||||
|             data => { | ||||
|                 versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`; | ||||
|  | ||||
|                 statusBadge | ||||
|                     .popover("dispose") | ||||
|                     .attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`) | ||||
|                     .popover(); | ||||
|                 statusBadge.removeClass(); | ||||
|                 statusBadge.addClass("btn"); | ||||
|                 statusBadge.addClass(badgeClass(response.status.status)); | ||||
|                 statusBadge.classList.remove(...statusBadge.classList); | ||||
|                 statusBadge.classList.add("btn"); | ||||
|                 statusBadge.classList.add(badgeClass(data.status.status)); | ||||
|  | ||||
|                 const popover = bootstrap.Popover.getOrCreateInstance(statusBadge); | ||||
|                 popover.dispose(); | ||||
|                 statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`; | ||||
|                 bootstrap.Popover.getOrCreateInstance(statusBadge); | ||||
|             }, | ||||
|         }); | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     function selectRepository() { | ||||
|         const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; | ||||
|         const element = $(`#${fragment}-link`); | ||||
|         element.click(); | ||||
|         document.getElementById(`${fragment}-link`).click(); | ||||
|     } | ||||
|  | ||||
|     function statusFormat(value) { | ||||
| @ -182,20 +169,25 @@ | ||||
|         return {classes: cellClass(value)}; | ||||
|     } | ||||
|  | ||||
|     $(_ => { | ||||
|         $("#repositories a").on("click", event => { | ||||
|             const element = event.target; | ||||
|             repository = { | ||||
|                 architecture: element.dataset.architecture, | ||||
|                 repository: element.dataset.repository, | ||||
|     ready(_ => { | ||||
|         document.querySelectorAll("#repositories a").forEach(element => { | ||||
|             element.onclick = _ => { | ||||
|                 repository = { | ||||
|                     architecture: element.dataset.architecture, | ||||
|                     repository: element.dataset.repository, | ||||
|                 }; | ||||
|                 if (packageUpdateButton) { | ||||
|                     packageUpdateButton.innerHTML = `<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`; | ||||
|                 } | ||||
|                 bootstrap.Tab.getOrCreateInstance(document.getElementById(element.id)).show(); | ||||
|                 reload(); | ||||
|             }; | ||||
|             packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`); | ||||
|             $(`#${element.id}`).tab("show"); | ||||
|             reload(); | ||||
|         }); | ||||
|  | ||||
|         table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => { | ||||
|             packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length); | ||||
|             if (packageRemoveButton) { | ||||
|                 packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length; | ||||
|             } | ||||
|         }); | ||||
|         table.on("click-row.bs.table", (self, data, row, cell) => { | ||||
|             if (0 === cell || "base" === cell) { | ||||
| @ -204,26 +196,38 @@ | ||||
|             } else showPackageInfo(data.id); | ||||
|         }); | ||||
|         table.on("created-controls.bs.table", _ => { | ||||
|             const pickerInput = $(".bootstrap-table-filter-control-timestamp"); | ||||
|             pickerInput.daterangepicker({ | ||||
|                 autoUpdateInput: false, | ||||
|             new easepick.create({ | ||||
|                 element: document.querySelector(".bootstrap-table-filter-control-timestamp"), | ||||
|                 css: [ | ||||
|                     "https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css", | ||||
|                 ], | ||||
|                 grid: 2, | ||||
|                 calendars: 2, | ||||
|                 autoApply: false, | ||||
|                 locale: { | ||||
|                     cancelLabel: "Clear", | ||||
|                     cancel: "Clear", | ||||
|                 }, | ||||
|                 RangePlugin: { | ||||
|                     tooltip: false, | ||||
|                 }, | ||||
|                 plugins: [ | ||||
|                     "RangePlugin", | ||||
|                 ], | ||||
|                 setup: picker => { | ||||
|                     picker.on("select", _ => { table.bootstrapTable("triggerSearch"); }); | ||||
|                     // replace "Cancel" behaviour to "Clear" | ||||
|                     picker.onClickCancelButton = element => { | ||||
|                         if (picker.isCancelButton(element)) { | ||||
|                             picker.clear(); | ||||
|                             picker.hide(); | ||||
|                             table.bootstrapTable("triggerSearch"); | ||||
|                         } | ||||
|                     }; | ||||
|                 }, | ||||
|             }); | ||||
|  | ||||
|             pickerInput.on("apply.daterangepicker", (event, picker) => { | ||||
|                 pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`); | ||||
|                 table.bootstrapTable("triggerSearch"); | ||||
|             }); | ||||
|  | ||||
|             pickerInput.on("cancel.daterangepicker", _ => { | ||||
|                 pickerInput.val(""); | ||||
|                 table.bootstrapTable("triggerSearch"); | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         statusBadge.popover(); | ||||
|         bootstrap.Popover.getOrCreateInstance(statusBadge); | ||||
|         selectRepository(); | ||||
|     }); | ||||
| </script> | ||||
|  | ||||
| @ -105,13 +105,13 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa | ||||
|         </div> | ||||
|  | ||||
|         <script> | ||||
|             const table = $("#packages"); | ||||
|             const table = $(document.getElementById("packages")); | ||||
|  | ||||
|             const pacmanConf = $("#pacman-conf"); | ||||
|             const pacmanConfCopyButton = $("#copy-btn"); | ||||
|             const pacmanConf = document.getElementById("pacman-conf"); | ||||
|             const pacmanConfCopyButton = document.getElementById("copy-btn"); | ||||
|  | ||||
|             async function copyPacmanConf() { | ||||
|                 const conf = pacmanConf.text(); | ||||
|                 const conf = pacmanConf.textContent; | ||||
|                 await copyToClipboard(conf, pacmanConfCopyButton); | ||||
|             } | ||||
|  | ||||
| @ -127,24 +127,36 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa | ||||
|                 return extractDataList(table.bootstrapTable("getData"), "licenses"); | ||||
|             } | ||||
|  | ||||
|             $(_ => { | ||||
|             ready(_ => { | ||||
|                 table.on("created-controls.bs.table", _ => { | ||||
|                     const pickerInput = $(".bootstrap-table-filter-control-timestamp"); | ||||
|                     pickerInput.daterangepicker({ | ||||
|                         autoUpdateInput: false, | ||||
|                     new easepick.create({ | ||||
|                         element: document.querySelector(".bootstrap-table-filter-control-timestamp"), | ||||
|                         css: [ | ||||
|                             "https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css", | ||||
|                         ], | ||||
|                         grid: 2, | ||||
|                         calendars: 2, | ||||
|                         autoApply: false, | ||||
|                         locale: { | ||||
|                             cancelLabel: "Clear", | ||||
|                             cancel: "Clear", | ||||
|                         }, | ||||
|                         RangePlugin: { | ||||
|                             tooltip: false, | ||||
|                         }, | ||||
|                         plugins: [ | ||||
|                             "RangePlugin", | ||||
|                         ], | ||||
|                         setup: picker => { | ||||
|                             picker.on("select", _ => { table.bootstrapTable("triggerSearch"); }); | ||||
|                             // replace "Cancel" behaviour to "Clear" | ||||
|                             picker.onClickCancelButton = element => { | ||||
|                                 if (picker.isCancelButton(element)) { | ||||
|                                     picker.clear(); | ||||
|                                     picker.hide(); | ||||
|                                     table.bootstrapTable("triggerSearch"); | ||||
|                                 } | ||||
|                             }; | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
|                     pickerInput.on("apply.daterangepicker", (event, picker) => { | ||||
|                         pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`); | ||||
|                         table.bootstrapTable("triggerSearch"); | ||||
|                     }); | ||||
|  | ||||
|                     pickerInput.on("cancel.daterangepicker", _ => { | ||||
|                         pickerInput.val(""); | ||||
|                         table.bootstrapTable("triggerSearch"); | ||||
|                     }); | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
| @ -1,41 +1,30 @@ | ||||
| <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.md5@1.0.2/index.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/moment@2.29.4/moment.min.js" crossorigin="anonymous" type="application/javascript"></script> | ||||
| <script src="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.min.js" crossorigin="anonymous" type="application/javascript"></script> | ||||
|  | ||||
| <script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.28.0/tableExport.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/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/bootstrap@5.3.2/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script> | ||||
| <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.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-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.22.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.22.1/dist/extensions/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script> | ||||
| <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.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.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.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.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script> | ||||
|  | ||||
| <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.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/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script> | ||||
|  | ||||
| <script> | ||||
|     async function copyToClipboard(text, button) { | ||||
|         if (navigator.clipboard === undefined) { | ||||
|             const input = document.createElement("textarea"); | ||||
|             input.innerHTML = text; | ||||
|             document.body.appendChild(input); | ||||
|             input.select(); | ||||
|             document.execCommand("copy"); | ||||
|             document.body.removeChild(input); | ||||
|         } else { | ||||
|             await navigator.clipboard.writeText(text); | ||||
|         } | ||||
|  | ||||
|         button.html("<i class=\"bi bi-clipboard-check\"></i> copied"); | ||||
|         setTimeout(()=> { | ||||
|             button.html("<i class=\"bi bi-clipboard\"></i> copy"); | ||||
|         await navigator.clipboard.writeText(text); | ||||
|         button.innerHTML = "<i class=\"bi bi-clipboard-check\"></i> copied"; | ||||
|         setTimeout(_ => { | ||||
|             button.innerHTML = "<i class=\"bi bi-clipboard\"></i> copy"; | ||||
|         }, 2000); | ||||
|     } | ||||
|  | ||||
| @ -76,6 +65,47 @@ | ||||
|             .join("<br>"); | ||||
|     } | ||||
|  | ||||
|     function makeRequest(url, params, onSuccess, onFailure) { | ||||
|         const requestParams = { | ||||
|             method: params.method, | ||||
|             body: params.json ? JSON.stringify(params.json) : params.json, | ||||
|             headers: { | ||||
|                 "Accept": "application/json", | ||||
|                 "Content-Type": "application/json", | ||||
|             }, | ||||
|         }; | ||||
|         if (params.query) { | ||||
|             const query = new URLSearchParams(params.query); | ||||
|             url += `?${query.toString()}`; | ||||
|         } | ||||
|         const convert = params.convert ?? (response => response.text()); | ||||
|  | ||||
|         return fetch(url, requestParams) | ||||
|             .then(response => { | ||||
|                 if (response.ok) { | ||||
|                     return convert(response); | ||||
|                 } else { | ||||
|                     const error = new Error("Network request error"); | ||||
|                     error.status = response.status; | ||||
|                     error.statusText = response.statusText; | ||||
|                     return response.text().then(text => { | ||||
|                         error.text = text; | ||||
|                         throw error; | ||||
|                     }); | ||||
|                 } | ||||
|             }) | ||||
|             .then(data => onSuccess && onSuccess(data)) | ||||
|             .catch(error => onFailure && onFailure(error)); | ||||
|     } | ||||
|  | ||||
|     function ready(fn) { | ||||
|         if (document.readyState === "complete" || document.readyState === "interactive") { | ||||
|             setTimeout(fn, 1); | ||||
|         } else { | ||||
|             document.addEventListener("DOMContentLoaded", fn); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function safe(string) { | ||||
|         return String(string) | ||||
|             .replace(/&/g, "&") | ||||
| @ -89,7 +119,9 @@ | ||||
|         const element = document.createElement("a"); | ||||
|         element.href = url; | ||||
|         element.innerText = text; | ||||
|         if (title) element.title = title; | ||||
|         if (title) { | ||||
|             element.title = title; | ||||
|         } | ||||
|         return element; | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,17 +1,15 @@ | ||||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css"> | ||||
| <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-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css"> | ||||
|  | ||||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.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/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.22.1/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.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/bootswatch@5.3.2/dist/cosmo/bootstrap.min.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/daterangepicker@3.1.0/daterangepicker.css" crossorigin="anonymous" type="text/css"> | ||||
|  | ||||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github.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"> | ||||
|  | ||||
| <style> | ||||
|     .pre-scrollable { | ||||
|  | ||||
		Reference in New Issue
	
	Block a user