mirror of
				https://github.com/arcan1s/ahriman.git
				synced 2025-10-31 05:43:41 +00:00 
			
		
		
		
	feat: add patch controls to web, review web, enrich info tab (#115)
* add ability to specify one-time patch on package addition * support vars in interface
This commit is contained in:
		| @ -51,7 +51,7 @@ | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-update-button" class="btn dropdown-item" onclick="updatePackages()" hidden> | ||||
|                             <button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden> | ||||
|                                 <i class="bi bi-play"></i> update | ||||
|                             </button> | ||||
|                         </li> | ||||
| @ -61,7 +61,7 @@ | ||||
|                             </button> | ||||
|                         </li> | ||||
|                         <li> | ||||
|                             <button id="package-remove-button" class="btn dropdown-item" onclick="removePackages()" disabled hidden> | ||||
|                             <button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden> | ||||
|                                 <i class="bi bi-trash"></i> remove | ||||
|                             </button> | ||||
|                         </li> | ||||
| @ -77,7 +77,8 @@ | ||||
|                 </button> | ||||
|             </div> | ||||
|  | ||||
|             <table id="packages" class="table table-striped table-hover" | ||||
|             <table id="packages" | ||||
|                    data-classes="table table-hover" | ||||
|                    data-export-options='{"fileName": "packages"}' | ||||
|                    data-filter-control="true" | ||||
|                    data-filter-control-visible="false" | ||||
| @ -102,13 +103,13 @@ | ||||
|                     <tr> | ||||
|                         <th data-checkbox="true"></th> | ||||
|                         <th data-sortable="true" data-switchable="false" data-field="base" data-filter-control="input" data-filter-control-placeholder="(any base)">package base</th> | ||||
|                         <th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-field="packages" data-filter-control="input" data-filter-control-placeholder="(any package)">packages</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="packager" data-filter-control="select" data-filter-custom-search="filterContains" data-filter-control-placeholder="(any packager)">packager</th> | ||||
|                         <th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th> | ||||
|                         <th data-sortable="true" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">last update</th> | ||||
|                         <th data-sortable="true" data-align="center" data-cell-style="statusFormat" data-field="status" data-filter-control="select" data-filter-control-placeholder="(any status)">status</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|             </table> | ||||
|  | ||||
| @ -8,20 +8,20 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="key-import-fingerprint-input" class="col-sm-2 col-form-label">fingerprint</label> | ||||
|                         <div class="col-sm-10"> | ||||
|                         <label for="key-import-fingerprint-input" class="col-2 col-form-label">fingerprint</label> | ||||
|                         <div class="col-10"> | ||||
|                             <input id="key-import-fingerprint-input" type="text" class="form-control" placeholder="PGP key fingerprint" name="key" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="key-import-server-input" class="col-sm-2 col-form-label">key server</label> | ||||
|                         <div class="col-sm-10"> | ||||
|                         <label for="key-import-server-input" class="col-2 col-form-label">key server</label> | ||||
|                         <div class="col-10"> | ||||
|                             <input id="key-import-server-input" type="text" class="form-control" placeholder="PGP key server" name="server" value="keyserver.ubuntu.com" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <div class="col-sm-2"></div> | ||||
|                         <div class="col-sm-10"> | ||||
|                         <div class="col-2"></div> | ||||
|                         <div class="col-10"> | ||||
|                             <pre class="language-less"><samp id="key-import-body-input" class="pre-scrollable language-less"></samp><button id="key-import-copy-button" type="button" class="btn language-less" onclick="copyPgpKey()"><i class="bi bi-clipboard"></i> copy</button></pre> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
| @ -8,14 +8,14 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="login-username" class="col-sm-4 col-form-label">username</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="login-username" class="col-4 col-form-label">username</label> | ||||
|                         <div class="col-8"> | ||||
|                             <input id="login-username" type="text" class="form-control" placeholder="enter username" name="username" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="login-password" class="col-sm-4 col-form-label">password</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="login-password" class="col-4 col-form-label">password</label> | ||||
|                         <div class="col-8"> | ||||
|                             <div class="input-group"> | ||||
|                                 <input id="login-password" type="password" class="form-control" placeholder="enter password" name="password" required> | ||||
|                                 <div class="input-group-append"> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <div id="package-add-modal" tabindex="-1" role="dialog" class="modal fade"> | ||||
|     <div class="modal-dialog" role="document"> | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <form id="package-add-form" onsubmit="return false"> | ||||
|                 <div class="modal-header"> | ||||
| @ -8,9 +8,9 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-add-repository-input" class="col-sm-4 col-form-label">repository</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                             <select id="package-add-repository-input" class="form-control" name="repository" required> | ||||
|                         <label for="package-add-repository-input" class="col-3 col-form-label">repository</label> | ||||
|                         <div class="col-9"> | ||||
|                             <select id="package-add-repository-input" class="form-control" required> | ||||
|                                 {% for repository in repositories %} | ||||
|                                     <option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option> | ||||
|                                 {% endfor %} | ||||
| @ -18,12 +18,18 @@ | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-add-input" class="col-sm-4 col-form-label">package</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                             <input id="package-add-input" type="text" list="known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" name="package" required> | ||||
|                         <label for="package-add-input" class="col-3 col-form-label">package</label> | ||||
|                         <div class="col-9"> | ||||
|                             <input id="package-add-input" type="text" list="package-add-known-packages-dlist" autocomplete="off" class="form-control" placeholder="AUR package" required> | ||||
|                             <datalist id="package-add-known-packages-dlist"></datalist> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <div class="col-12"> | ||||
|                             <button id="package-add-variable-button" type="button" class="form-control btn btn-light rounded" onclick="packageAddVariableInputCreate()"><i class="bi bi-plus"></i> add environment variable </button> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div id="package-add-variables-div" class="form-group row"></div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button type="submit" class="btn btn-primary" onclick="packagesAdd()"><i class="bi bi-play"></i> add</button> | ||||
| @ -39,9 +45,11 @@ | ||||
|     const packageAddForm = $("#package-add-form"); | ||||
|     packageAddModal.on("shown.bs.modal", () => { | ||||
|         $(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true); | ||||
|  | ||||
|     }); | ||||
|     packageAddModal.on("hidden.bs.modal", () => { packageAddForm.trigger("reset"); }); | ||||
|     packageAddModal.on("hidden.bs.modal", () => { | ||||
|         packageAddVariablesDiv.empty(); | ||||
|         packageAddForm.trigger("reset"); | ||||
|     }); | ||||
|  | ||||
|     const packageAddInput = $("#package-add-input"); | ||||
|     const packageAddRepositoryInput = $("#package-add-repository-input"); | ||||
| @ -71,25 +79,81 @@ | ||||
|         }, this), 500)); | ||||
|     }); | ||||
|  | ||||
|     function packagesAdd() { | ||||
|         const packages = packageAddInput.val(); | ||||
|     const packageAddVariablesDiv = $("#package-add-variables-div"); | ||||
|  | ||||
|     function packageAddVariableInputCreate() { | ||||
|         const variableInput = document.createElement("div"); | ||||
|         variableInput.classList.add("input-group"); | ||||
|         variableInput.classList.add("package-add-variable"); | ||||
|  | ||||
|         const variableNameInput = document.createElement("input"); | ||||
|         variableNameInput.type = "text"; | ||||
|         variableNameInput.classList.add("form-control"); | ||||
|         variableNameInput.classList.add("package-add-variable-name"); | ||||
|         variableNameInput.placeholder = "name"; | ||||
|         variableNameInput.ariaLabel = "variable name"; | ||||
|  | ||||
|         const variableSeparator = document.createElement("span"); | ||||
|         variableSeparator.classList.add("input-group-text") | ||||
|         variableSeparator.textContent = "="; | ||||
|  | ||||
|         const variableValueInput = document.createElement("input"); | ||||
|         variableValueInput.type = "text"; | ||||
|         variableValueInput.classList.add("form-control"); | ||||
|         variableValueInput.classList.add("package-add-variable-value"); | ||||
|         variableValueInput.placeholder = "value"; | ||||
|         variableValueInput.ariaLabel = "variable value"; | ||||
|  | ||||
|         const variableButtonRemove = document.createElement("button"); | ||||
|         variableButtonRemove.type = "button"; | ||||
|         variableButtonRemove.classList.add("btn"); | ||||
|         variableButtonRemove.classList.add("btn-outline-danger"); | ||||
|         variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; | ||||
|         variableButtonRemove.onclick = _ => { return variableInput.remove(); }; | ||||
|  | ||||
|         // bring them together | ||||
|         variableInput.appendChild(variableNameInput); | ||||
|         variableInput.appendChild(variableSeparator); | ||||
|         variableInput.appendChild(variableValueInput); | ||||
|         variableInput.appendChild(variableButtonRemove); | ||||
|  | ||||
|         packageAddVariablesDiv.append(variableInput); | ||||
|     } | ||||
|  | ||||
|     function patchesParse() { | ||||
|         const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => { | ||||
|             const richElement = $(element); | ||||
|             return { | ||||
|                 key: richElement.find(".package-add-variable-name").val(), | ||||
|                 value: richElement.find(".package-add-variable-value").val(), | ||||
|             }; | ||||
|         }).filter((_, patch) => patch.key).get(); | ||||
|         return {patches: patches}; | ||||
|     } | ||||
|  | ||||
|     function packagesAdd(packages, patches) { | ||||
|         packages = packages ?? packageAddInput.val(); | ||||
|         patches = patches ?? patchesParse(); | ||||
|         const repository = getRepositorySelector(packageAddRepositoryInput); | ||||
|  | ||||
|         if (packages) { | ||||
|             packageAddModal.modal("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); | ||||
|             doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function packagesRequest() { | ||||
|         const packages = packageAddInput.val(); | ||||
|     function packagesRequest(packages, patches) { | ||||
|         packages = packages ?? packageAddInput.val(); | ||||
|         patches = patches ?? patchesParse(); | ||||
|         const repository = getRepositorySelector(packageAddRepositoryInput); | ||||
|  | ||||
|         if (packages) { | ||||
|             packageAddModal.modal("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); | ||||
|             doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches); | ||||
|         } | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| @ -6,10 +6,47 @@ | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="close"></button> | ||||
|             </div> | ||||
|             <div class="modal-body"> | ||||
|                 <div class="form-group row"> | ||||
|                     <div class="input-group"> | ||||
|                         <table class="table table-borderless"> | ||||
|                             <tbody> | ||||
|                                 <tr> | ||||
|                                     <td style="width: 10%; text-align: right">version</td> | ||||
|                                     <td id="package-info-version" style="width: 40%"></td> | ||||
|                                     <td style="width: 10%; text-align: right">packager</td> | ||||
|                                     <td id="package-info-packager" style="width: 40%"></td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td style="width: 10%; text-align: right">groups</td> | ||||
|                                     <td id="package-info-groups" style="width: 40%"></td> | ||||
|                                     <td style="width: 10%; text-align: right">licenses</td> | ||||
|                                     <td id="package-info-licenses" style="width: 40%"></td> | ||||
|                                 </tr> | ||||
|                                 <tr> | ||||
|                                     <td style="width: 10%; text-align: right">packages</td> | ||||
|                                     <td id="package-info-packages" style="width: 40%"></td> | ||||
|                                     <td style="width: 10%; text-align: right">depends</td> | ||||
|                                     <td id="package-info-depends" style="width: 40%"></td> | ||||
|                                 </tr> | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <hr class="col-12"> | ||||
|  | ||||
|                 <h3>Environment variables</h3> | ||||
|                 <div id="package-info-variables-div" class="form-group row"></div> | ||||
|  | ||||
|                 <hr class="col-12"> | ||||
|  | ||||
|                 <h3>Build logs</h3> | ||||
|                 <pre class="language-logs"><samp id="package-info-logs-input" class="pre-scrollable language-logs"></samp><button id="package-info-logs-copy-button" type="button" class="btn language-logs" onclick="copyLogs()"><i class="bi bi-clipboard"></i> copy</button></pre> | ||||
|             </div> | ||||
|             <div class="modal-footer"> | ||||
|                 <button type="button" class="btn btn-secondary" onclick="showLogs()"><i class="bi bi-arrow-clockwise"></i> reload</button> | ||||
|                 <button type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i> update</button> | ||||
|                 <button type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i> remove</button> | ||||
|                 <button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i> reload</button> | ||||
|                 <button type="button" class="btn btn-primary" data-bs-dismiss="modal"><i class="bi bi-x"></i> close</button> | ||||
|             </div> | ||||
|         </div> | ||||
| @ -20,30 +57,78 @@ | ||||
|     const packageInfoModal = $("#package-info-modal"); | ||||
|     const packageInfoModalHeader = $("#package-info-modal-header"); | ||||
|     const packageInfo = $("#package-info"); | ||||
|     packageInfoModal.on("hidden.bs.modal", () => { | ||||
|         packageInfoDepends.empty(); | ||||
|         packageInfoGroups.empty(); | ||||
|         packageInfoLicenses.empty(); | ||||
|         packageInfoPackager.empty(); | ||||
|         packageInfoPackages.empty(); | ||||
|         packageInfoVersion.empty(); | ||||
|  | ||||
|         packageInfoVariablesDiv.empty(); | ||||
|  | ||||
|         packageInfoModal.trigger("reset"); | ||||
|     }); | ||||
|  | ||||
|     const packageInfoLogsInput = $("#package-info-logs-input"); | ||||
|     const packageInfoLogsCopyButton = $("#package-info-logs-copy-button"); | ||||
|  | ||||
|     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 packageInfoVersion = $("#package-info-version"); | ||||
|  | ||||
|     const packageInfoVariablesDiv = $("#package-info-variables-div"); | ||||
|  | ||||
|     async function copyLogs() { | ||||
|         const logs = packageInfoLogsInput.text(); | ||||
|         await copyToClipboard(logs, packageInfoLogsCopyButton); | ||||
|     } | ||||
|  | ||||
|     function showLogs(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 | ||||
|     function insertVariable(packageBase, variable) { | ||||
|         const variableInput = document.createElement("div"); | ||||
|         variableInput.classList.add("input-group"); | ||||
|  | ||||
|         const headerClass = status => { | ||||
|             if (status === "pending") return ["bg-warning"]; | ||||
|             if (status === "building") return ["bg-warning"]; | ||||
|             if (status === "failed") return ["bg-danger", "text-white"]; | ||||
|             if (status === "success") return ["bg-success", "text-white"]; | ||||
|             return ["bg-secondary", "text-white"]; | ||||
|         const variableNameInput = document.createElement("input"); | ||||
|         variableNameInput.classList.add("form-control"); | ||||
|         variableNameInput.readOnly = true; | ||||
|         variableNameInput.value = variable.key; | ||||
|  | ||||
|         const variableSeparator = document.createElement("span"); | ||||
|         variableSeparator.classList.add("input-group-text") | ||||
|         variableSeparator.textContent = "="; | ||||
|  | ||||
|         const variableValueInput = document.createElement("input"); | ||||
|         variableValueInput.classList.add("form-control"); | ||||
|         variableValueInput.readOnly = true; | ||||
|         variableValueInput.value = variable.value; | ||||
|  | ||||
|         const variableButtonRemove = document.createElement("button"); | ||||
|         variableButtonRemove.type = "button"; | ||||
|         variableButtonRemove.classList.add("btn"); | ||||
|         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(), | ||||
|             }); | ||||
|         }; | ||||
|  | ||||
|         // bring them together | ||||
|         variableInput.appendChild(variableNameInput); | ||||
|         variableInput.appendChild(variableSeparator); | ||||
|         variableInput.appendChild(variableValueInput); | ||||
|         variableInput.appendChild(variableButtonRemove); | ||||
|  | ||||
|         packageInfoVariablesDiv.append(variableInput); | ||||
|     } | ||||
|  | ||||
|     function loadLogs(packageBase, onFailure) { | ||||
|         $.ajax({ | ||||
|             url: `/api/v2/packages/${packageBase}/logs`, | ||||
|             data: { | ||||
| @ -53,26 +138,101 @@ | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 packageInfo.text(`${response.package_base} ${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`); | ||||
|                 const logs = response.logs.map(log_record => { | ||||
|                     const [timestamp, record] = log_record; | ||||
|                     return `[${new Date(1000 * timestamp).toISOString()}] ${record}`; | ||||
|                 const logs = response.map(log_record => { | ||||
|                     return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`; | ||||
|                 }); | ||||
|                 packageInfoLogsInput.text(logs.join("\n")); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function loadPackage(packageBase, onFailure) { | ||||
|         const headerClass = status => { | ||||
|             if (status === "pending") return ["bg-warning"]; | ||||
|             if (status === "building") return ["bg-warning"]; | ||||
|             if (status === "failed") return ["bg-danger", "text-white"]; | ||||
|             if (status === "success") return ["bg-success", "text-white"]; | ||||
|             return ["bg-secondary", "text-white"]; | ||||
|         }; | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `/api/v1/packages/${packageBase}`, | ||||
|             data: { | ||||
|                 architecture: repository.architecture, | ||||
|                 repository: repository.repository, | ||||
|             }, | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const description = response.find(Boolean); | ||||
|                 const packages = Object.keys(description.package.packages); | ||||
|  | ||||
|                 packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`); | ||||
|  | ||||
|                 packageInfoModalHeader.removeClass(); | ||||
|                 packageInfoModalHeader.addClass("modal-header"); | ||||
|                 headerClass(response.status.status).forEach((clz) => packageInfoModalHeader.addClass(clz)); | ||||
|                 headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz)); | ||||
|  | ||||
|                 if (isPackageBaseSet) packageInfoModal.modal("show"); // we don't need to show window again | ||||
|             }, | ||||
|             error: (jqXHR, _, errorThrown) => { | ||||
|                 // show failed modal in case if first time loading | ||||
|                 if (isPackageBaseSet) { | ||||
|                     const message = error => `Could not load package ${packageBase} logs: ${error}`; | ||||
|                     showFailure("Load failure", message, jqXHR, errorThrown); | ||||
|                 } | ||||
|                 packageInfoDepends.html(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)); | ||||
|                 packageInfoVersion.text(description.package.version); | ||||
|             }, | ||||
|             error: 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)); | ||||
|             }, | ||||
|             error: onFailure, | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function packageInfoRemove() { | ||||
|         const packageBase = packageInfoModal.data("package"); | ||||
|         if (packageBase) return packagesRemove([packageBase]); | ||||
|     } | ||||
|  | ||||
|     function packageInfoUpdate() { | ||||
|         const packageBase = packageInfoModal.data("package"); | ||||
|         if (packageBase) return packagesAdd(packageBase, []); | ||||
|     } | ||||
|  | ||||
|     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 | ||||
|  | ||||
|         const onFailure = (jqXHR, _, errorThrown) => { | ||||
|             if (isPackageBaseSet) { | ||||
|                 const message = error => `Could not load package ${packageBase} info: ${error}`; | ||||
|                 showFailure("Load failure", message, jqXHR, errorThrown); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         loadPackage(packageBase, onFailure); | ||||
|         loadPatches(packageBase, onFailure); | ||||
|         loadLogs(packageBase, onFailure); | ||||
|  | ||||
|         if (isPackageBaseSet) packageInfoModal.modal("show"); | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| <div id="package-rebuild-modal" tabindex="-1" role="dialog" class="modal fade"> | ||||
|     <div class="modal-dialog" role="document"> | ||||
|     <div class="modal-dialog modal-lg" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <form id="package-rebuild-form" onsubmit="return false"> | ||||
|                 <div class="modal-header"> | ||||
| @ -8,8 +8,8 @@ | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-rebuild-repository-input" class="col-sm-4 col-form-label">repository</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="package-rebuild-repository-input" class="col-3 col-form-label">repository</label> | ||||
|                         <div class="col-9"> | ||||
|                             <select id="package-rebuild-repository-input" class="form-control" name="repository" required> | ||||
|                                 {% for repository in repositories %} | ||||
|                                     <option value="{{ repository.id }}" data-repository="{{ repository.repository }}" data-architecture="{{ repository.architecture }}">{{ repository.repository }} ({{ repository.architecture }})</option> | ||||
| @ -18,8 +18,8 @@ | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-group row"> | ||||
|                         <label for="package-rebuild-dependency-input" class="col-sm-4 col-form-label">dependency</label> | ||||
|                         <div class="col-sm-8"> | ||||
|                         <label for="package-rebuild-dependency-input" class="col-3 col-form-label">dependency</label> | ||||
|                         <div class="col-9"> | ||||
|                             <input id="package-rebuild-dependency-input" type="text" class="form-control" placeholder="packages dependency" name="package" required> | ||||
|                         </div> | ||||
|                     </div> | ||||
|  | ||||
| @ -25,7 +25,7 @@ | ||||
|         if (0 === cell || "base" === cell) { | ||||
|             const method = data[0] === true ? "uncheckBy" : "checkBy"; // fck javascript | ||||
|             table.bootstrapTable(method, {field: "id", values: [data.id]}); | ||||
|         } else showLogs(data.id); | ||||
|         } else showPackageInfo(data.id); | ||||
|     }); | ||||
|     table.on("created-controls.bs.table", () => { | ||||
|         const pickerInput = $(".bootstrap-table-filter-control-timestamp"); | ||||
| @ -50,7 +50,7 @@ | ||||
|     const statusBadge = $("#badge-status"); | ||||
|     const versionBadge = $("#badge-version"); | ||||
|  | ||||
|     function doPackageAction(uri, packages, repository, successText, failureText) { | ||||
|     function doPackageAction(uri, packages, repository, successText, failureText, data) { | ||||
|         const queryParams = $.param({ | ||||
|             architecture: repository.architecture, | ||||
|             repository: repository.repository, | ||||
| @ -58,7 +58,7 @@ | ||||
|  | ||||
|         $.ajax({ | ||||
|             url: `${uri}?${queryParams}`, | ||||
|             data: JSON.stringify({packages: packages}), | ||||
|             data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})), | ||||
|             type: "POST", | ||||
|             contentType: "application/json", | ||||
|             success: _ => { | ||||
| @ -71,31 +71,30 @@ | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function filterListGroups() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "groups"); | ||||
|     } | ||||
|  | ||||
|     function filterListLicenses() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "licenses"); | ||||
|     } | ||||
|  | ||||
|     function filterListPackagers() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "packager"); | ||||
|     } | ||||
|  | ||||
|     function getRepositorySelector(selector) { | ||||
|         const selected = selector.find(":selected"); | ||||
|         return { | ||||
|             architecture: selected.data("architecture"), | ||||
|             repository: selected.data("repository"), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     function getSelection() { | ||||
|         return table.bootstrapTable("getSelections").map(row => row.id); | ||||
|     } | ||||
|  | ||||
|     function removePackages() { | ||||
|         const onSuccess = update => `Packages ${update} have been removed`; | ||||
|         const onFailure = error => `Could not remove packages: ${error}`; | ||||
|         doPackageAction("/api/v1/service/remove", getSelection(), repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function selectRepository() { | ||||
|         const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; | ||||
|         const element = $(`#${fragment}-lnk`); | ||||
|         element.click(); | ||||
|     } | ||||
|  | ||||
|     function updatePackages() { | ||||
|         const currentSelection = getSelection(); | ||||
|         const [url, onSuccess] = currentSelection.length === 0 | ||||
|             ? ["/api/v1/service/update", _ => `Repository update has been run`] | ||||
|             : ["/api/v1/service/add", update => `Run update for packages ${update}`]; | ||||
|         const onFailure = error => `Packages update failed: ${error}`; | ||||
|         doPackageAction(url, currentSelection, repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function hideControls(hidden) { | ||||
|         keyImportButton.attr("hidden", hidden); | ||||
|         packageAddButton.attr("hidden", hidden); | ||||
| @ -104,6 +103,24 @@ | ||||
|         packageUpdateButton.attr("hidden", hidden); | ||||
|     } | ||||
|  | ||||
|     function packagesRemove(packages) { | ||||
|         packages = packages ?? getSelection(); | ||||
|         const onSuccess = update => `Packages ${update} have been removed`; | ||||
|         const onFailure = error => `Could not remove packages: ${error}`; | ||||
|  | ||||
|         doPackageAction("/api/v1/service/remove", packages, repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function packagesUpdate() { | ||||
|         const currentSelection = getSelection(); | ||||
|         const [url, onSuccess] = currentSelection.length === 0 | ||||
|             ? ["/api/v1/service/update", _ => `Repository update has been run`] | ||||
|             : ["/api/v1/service/add", update => `Run update for packages ${update}`]; | ||||
|         const onFailure = error => `Packages update failed: ${error}`; | ||||
|  | ||||
|         doPackageAction(url, currentSelection, repository, onSuccess, onFailure); | ||||
|     } | ||||
|  | ||||
|     function reload() { | ||||
|         table.bootstrapTable("showLoading"); | ||||
|  | ||||
| @ -124,18 +141,6 @@ | ||||
|             type: "GET", | ||||
|             dataType: "json", | ||||
|             success: response => { | ||||
|                 const extractListProperties = (description, property) => { | ||||
|                     return Object.values(description.packages) | ||||
|                         .map(pkg => pkg[property]) | ||||
|                         .reduce((left, right) => left.concat(right), []); | ||||
|                 }; | ||||
|                 const listToTable = data => { | ||||
|                     return Array.from(new Set(data)) | ||||
|                         .sort() | ||||
|                         .map(entry => safe(entry)) | ||||
|                         .join("<br>"); | ||||
|                 }; | ||||
|  | ||||
|                 const payload = response.map(description => { | ||||
|                     const package_base = description.package.base; | ||||
|                     const web_url = description.package.remote.web_url; | ||||
| @ -147,7 +152,7 @@ | ||||
|                         packages: listToTable(Object.keys(description.package.packages)), | ||||
|                         groups: listToTable(extractListProperties(description.package, "groups")), | ||||
|                         licenses: listToTable(extractListProperties(description.package, "licenses")), | ||||
|                         timestamp: new Date(1000 * description.status.timestamp).toISOString(), | ||||
|                         timestamp: new Date(1000 * description.status.timestamp).toISOStringShort(), | ||||
|                         status: description.status.status, | ||||
|                     }; | ||||
|                 }); | ||||
| @ -186,7 +191,7 @@ | ||||
|  | ||||
|                 statusBadge | ||||
|                     .popover("dispose") | ||||
|                     .attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOString()}`) | ||||
|                     .attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`) | ||||
|                     .popover(); | ||||
|                 statusBadge.removeClass(); | ||||
|                 statusBadge.addClass("btn"); | ||||
| @ -195,6 +200,12 @@ | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function selectRepository() { | ||||
|         const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; | ||||
|         const element = $(`#${fragment}-lnk`); | ||||
|         element.click(); | ||||
|     } | ||||
|  | ||||
|     function statusFormat(value) { | ||||
|         const cellClass = status => { | ||||
|             if (status === "pending") return "table-warning"; | ||||
| @ -206,26 +217,6 @@ | ||||
|         return {classes: cellClass(value)}; | ||||
|     } | ||||
|  | ||||
|     function filterListGroups() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "groups"); | ||||
|     } | ||||
|  | ||||
|     function filterListLicenses() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "licenses"); | ||||
|     } | ||||
|  | ||||
|     function filterListPackagers() { | ||||
|         return extractDataList(table.bootstrapTable("getData"), "packager"); | ||||
|     } | ||||
|  | ||||
|     function getRepositorySelector(selector) { | ||||
|         const selected = selector.find(":selected"); | ||||
|         return { | ||||
|             architecture: selected.data("architecture"), | ||||
|             repository: selected.data("repository"), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     $(() => { | ||||
|         table.bootstrapTable({}); | ||||
|         statusBadge.popover(); | ||||
|  | ||||
| @ -30,7 +30,8 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa | ||||
|         </div> | ||||
|  | ||||
|         <div class="container"> | ||||
|             <table id="packages" class="table table-striped table-hover" | ||||
|             <table id="packages" | ||||
|                    data-classes="table table-hover" | ||||
|                    data-export-options='{"fileName": "packages"}' | ||||
|                    data-filter-control="true" | ||||
|                    data-filter-control-visible="false" | ||||
| @ -53,16 +54,16 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa | ||||
|                 <thead class="table-primary"> | ||||
|                     <tr> | ||||
|                         <th data-sortable="true" data-switchable="false" data-field="name" data-filter-control="input" data-filter-control-placeholder="(any package)">package</th> | ||||
|                         <th data-sortable="true" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="version" data-filter-control="input" data-filter-control-placeholder="(any version)">version</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="architecture" data-filter-control="select" data-filter-control-placeholder="(any arch)">architecture</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="description" data-filter-control="input" data-filter-control-placeholder="(any description)">description</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="url">upstream url</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="licenses" data-filter-control="select" data-filter-data="func:filterListLicenses" data-filter-custom-search="filterList" data-filter-control-placeholder="(any license)">licenses</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="groups" data-filter-control="select" data-filter-data="func:filterListGroups" data-filter-custom-search="filterList" data-filter-control-placeholder="(any group)">groups</th> | ||||
|                         <th data-sortable="true" data-visible="false" data-field="depends" data-filter-control="select" data-filter-data="func:filterListDepends" data-filter-custom-search="filterList" data-filter-control-placeholder="(any depends)">depends</th> | ||||
|                         <th data-sortable="true" data-field="archive_size">archive size</th> | ||||
|                         <th data-sortable="true" data-field="installed_size">installed size</th> | ||||
|                         <th data-sortable="true" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="archive_size">archive size</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="installed_size">installed size</th> | ||||
|                         <th data-sortable="true" data-align="right" data-field="timestamp" data-filter-control="input" data-filter-custom-search="filterDateRange" data-filter-control-placeholder="(any date)">build date</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|  | ||||
|  | ||||
| @ -36,19 +36,17 @@ | ||||
|         }, 2000); | ||||
|     } | ||||
|  | ||||
|     function safe(string) { | ||||
|         return String(string) | ||||
|             .replace(/&/g, "&") | ||||
|             .replace(/</g, "<") | ||||
|             .replace(/>/g, ">") | ||||
|             .replace(/"/g, """); | ||||
|     } | ||||
|  | ||||
|     function extractDataList(data, column) { | ||||
|         const elements = data.flatMap(row => row[column].split("<br>")).filter(v => v); // remove empty elements from array | ||||
|         return Array.from(new Set(elements)).sort(); | ||||
|     } | ||||
|  | ||||
|     function extractListProperties(description, property) { | ||||
|         return Object.values(description.packages) | ||||
|             .map(pkg => pkg[property]) | ||||
|             .reduce((left, right) => left.concat(right), []); | ||||
|     } | ||||
|  | ||||
|     function filterContains(text, value) { | ||||
|         return value.includes(text.toLowerCase().trim()); | ||||
|     } | ||||
| @ -67,4 +65,24 @@ | ||||
|         // the library removes all symbols from string, so it is just string | ||||
|         return value.includes(dataList[index].toLowerCase()); | ||||
|     } | ||||
|  | ||||
|     function listToTable(data) { | ||||
|         return Array.from(new Set(data)) | ||||
|             .sort() | ||||
|             .map(entry => safe(entry)) | ||||
|             .join("<br>"); | ||||
|     } | ||||
|  | ||||
|     function safe(string) { | ||||
|         return String(string) | ||||
|             .replace(/&/g, "&") | ||||
|             .replace(/</g, "<") | ||||
|             .replace(/>/g, ">") | ||||
|             .replace(/"/g, """); | ||||
|     } | ||||
|  | ||||
|     Date.prototype.toISOStringShort = function() { | ||||
|         const pad = number => String(number).padStart(2, "0"); | ||||
|         return `${this.getFullYear()}-${pad(this.getMonth())}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`; | ||||
|     } | ||||
| </script> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user