Compare commits

..

20 Commits

Author SHA1 Message Date
013ba3d3ab build: make cerberus dependency optional 2024-09-03 13:29:58 +03:00
6099a5957d
feat: implement stats subcommand (#132) 2024-09-03 02:42:29 +03:00
41343fd9e1 feat: allow filter events by timestamp 2024-09-01 15:07:54 +03:00
a576a0b612 chore: small contributing guide update 2024-08-31 15:33:32 +03:00
05562d2ee5 chore: add rss generation to samples 2024-08-30 15:50:03 +03:00
3098132de2 feat: add event log and update chart to package info modal 2024-08-30 15:34:11 +03:00
4e246d3a67 feat: remove duplicates from the toast 2024-08-30 11:17:32 +03:00
6577ca9db1 refactor: simplify Validator class 2024-08-30 01:59:58 +03:00
6e37a60cf0
feat: allow cross reference in the configuration (#131) 2024-08-30 01:52:43 +03:00
a23a1bc613
feat: implement rss generation (#130) 2024-08-29 16:53:40 +03:00
fc508e19b8 refactor: fix some IDE warnings 2024-08-28 18:29:38 +03:00
09c8fd945d feat: add ability to log sql statements 2024-08-28 18:18:43 +03:00
b90d93f3c0 feat: serve logs and events from the newest to oldest, but keep the
ordering

So basically initial implementation, with limit=1, would emit the oldest
record in series. New implementation will return the most recent one
instead

The response is still sorted by ascension
2024-08-28 16:53:30 +03:00
cd98b7f6e6 feat: log package update events 2024-08-28 16:42:29 +03:00
08c1b08902 refactor: allow event to receive keyword arguments
This change also replaces the dataclass implementation of the class to
custom one
2024-08-28 02:01:37 +03:00
a9003993fa feat: add timer for metrics purposes 2024-08-27 00:27:37 +03:00
54a331cc96 docs: update booleans in docs 2024-08-26 23:32:02 +03:00
5f79cbc34b
feat: implement audit log tables and methods (#129) 2024-08-26 22:13:18 +03:00
ea4193eef4 build: update pytest configuration to suppress deprecation warnings 2024-08-26 21:44:40 +03:00
40fa94afbb feat: replace scan paths options to single one
It has been found that previous system didn't allow to configure
specific cases (e.g. a whitelisted directory inside /usr/lib/cmake). The
current solution replaces two options to single one, which also allows a
regular expressions

Also PackageArchive class has been moved to core package, because it is
more about service rather than model
2024-08-25 20:39:11 +03:00
26 changed files with 5608 additions and 5701 deletions

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@ -1,7 +1,7 @@
# Maintainer: Evgeniy Alekseev # Maintainer: Evgeniy Alekseev
pkgname='ahriman' pkgname='ahriman'
pkgver=2.14.1 pkgver=2.14.0
pkgrel=1 pkgrel=1
pkgdesc="ArcH linux ReposItory MANager" pkgdesc="ArcH linux ReposItory MANager"
arch=('any') arch=('any')

View File

@ -44,28 +44,28 @@
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li> <li>
<button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal"> <button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal" hidden>
<i class="bi bi-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
</li> </li>
<li> <li>
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()"> <button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden>
<i class="bi bi-play"></i> update <i class="bi bi-play"></i> update
</button> </button>
</li> </li>
<li> <li>
<button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal"> <button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal" hidden>
<i class="bi bi-arrow-clockwise"></i> rebuild <i class="bi bi-arrow-clockwise"></i> rebuild
</button> </button>
</li> </li>
<li> <li>
<button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled> <button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled hidden>
<i class="bi bi-trash"></i> remove <i class="bi bi-trash"></i> remove
</button> </button>
</li> </li>
</ul> </ul>
<button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal"> <button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal" hidden>
<i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span> <i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span>
</button> </button>
{% endif %} {% endif %}

View File

@ -1,12 +1,12 @@
<script> <script>
const alertPlaceholder = document.getElementById("alert-placeholder"); const alertPlaceholder = $("#alert-placeholder");
function createAlert(title, message, clz, action, id) { function createAlert(title, message, clz, action, id) {
id ??= md5(title + message); // MD5 id from the content if (!id) id = $.md5(title + message); // MD5 id from the content
if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates if (alertPlaceholder.find(`#${id}`).length > 0) return; // check if there are duplicates
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.id = `alert-${id}`; wrapper.id = id;
wrapper.classList.add("toast", clz); wrapper.classList.add("toast", clz);
wrapper.role = "alert"; wrapper.role = "alert";
wrapper.ariaLive = "assertive"; wrapper.ariaLive = "assertive";
@ -23,7 +23,7 @@
body.innerText = message; body.innerText = message;
wrapper.appendChild(body); wrapper.appendChild(body);
alertPlaceholder.appendChild(wrapper); alertPlaceholder.append(wrapper);
const toast = new bootstrap.Toast(wrapper); const toast = new bootstrap.Toast(wrapper);
wrapper.addEventListener("hidden.bs.toast", _ => { wrapper.addEventListener("hidden.bs.toast", _ => {
wrapper.remove(); // bootstrap doesn't remove elements wrapper.remove(); // bootstrap doesn't remove elements
@ -32,12 +32,12 @@
toast.show(); toast.show();
} }
function showFailure(title, description, error) { function showFailure(title, description, jqXHR, errorThrown) {
let details; let details;
try { try {
details = JSON.parse(error.text).error; // execution handler json error response details = $.parseJSON(jqXHR.responseText).error; // execution handler json error response
} catch (_) { } catch (_) {
details = error.text ?? error.message ?? error; details = errorThrown;
} }
createAlert(title, description(details), "text-bg-danger"); createAlert(title, description(details), "text-bg-danger");
} }

View File

@ -36,69 +36,61 @@
</div> </div>
<script> <script>
const keyImportModal = document.getElementById("key-import-modal"); const keyImportModal = $("#key-import-modal");
const keyImportForm = document.getElementById("key-import-form"); const keyImportForm = $("#key-import-form");
const keyImportBodyInput = document.getElementById("key-import-body-input"); const keyImportBodyInput = $("#key-import-body-input");
const keyImportCopyButton = document.getElementById("key-import-copy-button"); const keyImportCopyButton = $("#key-import-copy-button");
const keyImportFingerprintInput = document.getElementById("key-import-fingerprint-input"); const keyImportFingerprintInput = $("#key-import-fingerprint-input");
const keyImportServerInput = document.getElementById("key-import-server-input"); const keyImportServerInput = $("#key-import-server-input");
async function copyPgpKey() { async function copyPgpKey() {
const key = keyImportBodyInput.textContent; const logs = keyImportBodyInput.text();
await copyToClipboard(key, keyImportCopyButton); await copyToClipboard(logs, keyImportCopyButton);
} }
function fetchPgpKey() { function fetchPgpKey() {
const key = keyImportFingerprintInput.value; const key = keyImportFingerprintInput.val();
const server = keyImportServerInput.value; const server = keyImportServerInput.val();
if (key && server) { if (key && server) {
makeRequest( $.ajax({
"/api/v1/service/pgp", url: "/api/v1/service/pgp",
{ data: {"key": key, "server": server},
query: { type: "GET",
key: key, dataType: "json",
server: server, success: response => { keyImportBodyInput.text(response.key); },
}, });
convert: response => response.json(),
},
data => { keyImportBodyInput.textContent = data.key; },
);
} }
} }
function importPgpKey() { function importPgpKey() {
const key = keyImportFingerprintInput.value; const key = keyImportFingerprintInput.val();
const server = keyImportServerInput.value; const server = keyImportServerInput.val();
if (key && server) { if (key && server) {
makeRequest( $.ajax({
"/api/v1/service/pgp", url: "/api/v1/service/pgp",
{ data: JSON.stringify({key: key, server: server}),
method: "POST", type: "POST",
json: { contentType: "application/json",
key: key, success: _ => {
server: server, keyImportModal.modal("hide");
},
},
_ => {
bootstrap.Modal.getOrCreateInstance(keyImportModal).hide();
showSuccess("Success", `Key ${key} has been imported`); showSuccess("Success", `Key ${key} has been imported`);
}, },
error => { error: (jqXHR, _, errorThrown) => {
const message = _ => `Could not import key ${key} from ${server}`; const message = _ => `Could not import key ${key} from ${server}`;
showFailure("Action failed", message, error); showFailure("Action failed", message, jqXHR, errorThrown);
}, },
); });
} }
} }
ready(_ => { $(_ => {
keyImportModal.addEventListener("hidden.bs.modal", _ => { keyImportModal.on("hidden.bs.modal", _ => {
keyImportBodyInput.textContent = ""; keyImportBodyInput.text("");
keyImportForm.reset(); keyImportForm.trigger("reset");
}); });
}); });
</script> </script>

View File

@ -34,57 +34,53 @@
</div> </div>
<script> <script>
const loginModal = document.getElementById("login-modal"); const loginModal = $("#login-modal");
const loginForm = document.getElementById("login-form"); const loginForm = $("#login-form");
const loginPasswordInput = document.getElementById("login-password"); const loginPasswordInput = $("#login-password");
const loginUsernameInput = document.getElementById("login-username"); const loginUsernameInput = $("#login-username");
const showHidePasswordButton = document.getElementById("login-show-hide-password-button"); const showHidePasswordButton = $("#login-show-hide-password-button");
function login() { function login() {
const password = loginPasswordInput.value; const password = loginPasswordInput.val();
const username = loginUsernameInput.value; const username = loginUsernameInput.val();
if (username && password) { if (username && password) {
makeRequest( $.ajax({
"/api/v1/login", url: "/api/v1/login",
{ data: JSON.stringify({username: username, password: password}),
method: "POST", type: "POST",
json: { contentType: "application/json",
username: username, success: _ => {
password: password, loginModal.modal("hide");
},
},
_ => {
bootstrap.Modal.getOrCreateInstance(loginModal).hide();
showSuccess("Logged in", `Successfully logged in as ${username}`, _ => location.href = "/"); showSuccess("Logged in", `Successfully logged in as ${username}`, _ => location.href = "/");
}, },
error => { error: (jqXHR, _, errorThrown) => {
const message = _ => const message = _ =>
username === "admin" && password === "admin" username === "admin" && password === "admin"
? "You've entered a password for user \"root\", did you make a typo in username?" ? "You've entered a password for user \"root\", did you make a typo in username?"
: `Could not login as ${username}`; : `Could not login as ${username}`;
showFailure("Login error", message, error); showFailure("Login error", message, jqXHR, errorThrown);
}, },
); });
} }
} }
function showPassword() { function showPassword() {
if (loginPasswordInput.getAttribute("type") === "password") { if (loginPasswordInput.attr("type") === "password") {
loginPasswordInput.setAttribute("type", "text"); loginPasswordInput.attr("type", "text");
showHidePasswordButton.classList.remove("bi-eye"); showHidePasswordButton.removeClass("bi-eye");
showHidePasswordButton.classList.add("bi-eye-slash"); showHidePasswordButton.addClass("bi-eye-slash");
} else { } else {
loginPasswordInput.setAttribute("type", "password"); loginPasswordInput.attr("type", "password");
showHidePasswordButton.classList.remove("bi-eye-slash"); showHidePasswordButton.removeClass("bi-eye-slash");
showHidePasswordButton.classList.add("bi-eye"); showHidePasswordButton.addClass("bi-eye");
} }
} }
ready(_ => { $(_ => {
loginModal.addEventListener("hidden.bs.modal", _ => { loginModal.on("hidden.bs.modal", _ => {
loginForm.reset(); loginForm.trigger("reset");
}); });
}); });
</script> </script>

View File

@ -41,14 +41,14 @@
</div> </div>
<script> <script>
const packageAddModal = document.getElementById("package-add-modal"); const packageAddModal = $("#package-add-modal");
const packageAddForm = document.getElementById("package-add-form"); const packageAddForm = $("#package-add-form");
const packageAddInput = document.getElementById("package-add-input"); const packageAddInput = $("#package-add-input");
const packageAddRepositoryInput = document.getElementById("package-add-repository-input"); const packageAddRepositoryInput = $("#package-add-repository-input");
const packageAddKnownPackagesList = document.getElementById("package-add-known-packages-dlist"); const packageAddKnownPackagesList = $("#package-add-known-packages-dlist");
const packageAddVariablesDiv = document.getElementById("package-add-variables-div"); const packageAddVariablesDiv = $("#package-add-variables-div");
function packageAddVariableInputCreate() { function packageAddVariableInputCreate() {
const variableInput = document.createElement("div"); const variableInput = document.createElement("div");
@ -78,7 +78,7 @@
variableButtonRemove.classList.add("btn"); variableButtonRemove.classList.add("btn");
variableButtonRemove.classList.add("btn-outline-danger"); variableButtonRemove.classList.add("btn-outline-danger");
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
variableButtonRemove.onclick = _ => { variableInput.remove(); }; variableButtonRemove.onclick = _ => { return variableInput.remove(); };
// bring them together // bring them together
variableInput.appendChild(variableNameInput); variableInput.appendChild(variableNameInput);
@ -86,26 +86,27 @@
variableInput.appendChild(variableValueInput); variableInput.appendChild(variableValueInput);
variableInput.appendChild(variableButtonRemove); variableInput.appendChild(variableButtonRemove);
packageAddVariablesDiv.appendChild(variableInput); packageAddVariablesDiv.append(variableInput);
} }
function patchesParse() { function patchesParse() {
const patches = Array.from(packageAddVariablesDiv.getElementsByClassName("package-add-variable")).map(element => { const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => {
const richElement = $(element);
return { return {
key: element.querySelector(".package-add-variable-name").value, key: richElement.find(".package-add-variable-name").val(),
value: element.querySelector(".package-add-variable-value").value, value: richElement.find(".package-add-variable-value").val(),
}; };
}).filter(patch => patch.key); }).filter((_, patch) => patch.key).get();
return {patches: patches}; return {patches: patches};
} }
function packagesAdd(packages, patches, repository) { function packagesAdd(packages, patches, repository) {
packages = packages ?? packageAddInput.value; packages = packages ?? packageAddInput.val();
patches = patches ?? patchesParse(); patches = patches ?? patchesParse();
repository = repository ?? getRepositorySelector(packageAddRepositoryInput); repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
if (packages) { if (packages) {
bootstrap.Modal.getOrCreateInstance(packageAddModal).hide(); packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been added`; const onSuccess = update => `Packages ${update} have been added`;
const onFailure = error => `Package addition failed: ${error}`; const onFailure = error => `Package addition failed: ${error}`;
doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches); doPackageAction("/api/v1/service/add", [packages], repository, onSuccess, onFailure, patches);
@ -113,54 +114,50 @@
} }
function packagesRequest(packages, patches) { function packagesRequest(packages, patches) {
packages = packages ?? packageAddInput.value; packages = packages ?? packageAddInput.val();
patches = patches ?? patchesParse(); patches = patches ?? patchesParse();
const repository = getRepositorySelector(packageAddRepositoryInput); const repository = getRepositorySelector(packageAddRepositoryInput);
if (packages) { if (packages) {
bootstrap.Modal.getOrCreateInstance(packageAddModal).hide(); packageAddModal.modal("hide");
const onSuccess = update => `Packages ${update} have been requested`; const onSuccess = update => `Packages ${update} have been requested`;
const onFailure = error => `Package request failed: ${error}`; const onFailure = error => `Package request failed: ${error}`;
doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches); doPackageAction("/api/v1/service/request", [packages], repository, onSuccess, onFailure, patches);
} }
} }
ready(_ => { $(_ => {
packageAddModal.addEventListener("shown.bs.modal", _ => { packageAddModal.on("shown.bs.modal", _ => {
const option = packageAddRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`); $(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
option.selected = "selected";
}); });
packageAddModal.addEventListener("hidden.bs.modal", _ => { packageAddModal.on("hidden.bs.modal", _ => {
packageAddVariablesDiv.replaceChildren(); packageAddVariablesDiv.empty();
packageAddForm.reset(); packageAddForm.trigger("reset");
}); });
packageAddInput.addEventListener("keyup", _ => { packageAddInput.keyup(_ => {
clearTimeout(packageAddInput.requestTimeout); clearTimeout(packageAddInput.data("timeout"));
packageAddInput.requestTimeout = setTimeout(_ => { packageAddInput.data("timeout", setTimeout($.proxy(_ => {
const value = packageAddInput.value; const value = packageAddInput.val();
if (value.length >= 3) { if (value.length >= 3) {
makeRequest( $.ajax({
"/api/v1/service/search", url: "/api/v1/service/search",
{ data: {"for": value},
query: { type: "GET",
for: value, dataType: "json",
}, success: response => {
convert: response => response.json(), const options = response.map(pkg => {
},
data => {
const options = data.map(pkg => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = pkg.package; option.value = pkg.package;
option.innerText = `${pkg.package} (${pkg.description})`; option.innerText = `${pkg.package} (${pkg.description})`;
return option; return option;
}); });
packageAddKnownPackagesList.replaceChildren(...options); packageAddKnownPackagesList.empty().append(options);
}, },
); });
} }
}, 500); }, this), 500));
}); });
}); });
</script> </script>

View File

@ -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> <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>
<div id="package-info-events" class="tab-pane fade" role="tabpanel" aria-labelledby="package-info-events-button" tabindex="0"> <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" hidden></canvas> <canvas id="package-info-events-update-chart"></canvas>
<table id="package-info-events-table" <table id="package-info-events-table"
data-classes="table table-hover" data-classes="table table-hover"
data-sortable="true" data-sortable="true"
@ -77,10 +77,8 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{% 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" hidden><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<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" hidden><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</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-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> <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> </div>
@ -89,35 +87,33 @@
</div> </div>
<script> <script>
const packageInfoModal = document.getElementById("package-info-modal"); const packageInfoModal = $("#package-info-modal");
const packageInfoModalHeader = document.getElementById("package-info-modal-header"); const packageInfoModalHeader = $("#package-info-modal-header");
const packageInfo = document.getElementById("package-info"); const packageInfo = $("#package-info");
const packageInfoLogsInput = document.getElementById("package-info-logs-input"); const packageInfoLogsInput = $("#package-info-logs-input");
const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button"); const packageInfoLogsCopyButton = $("#package-info-logs-copy-button");
const packageInfoChangesInput = document.getElementById("package-info-changes-input"); const packageInfoChangesInput = $("#package-info-changes-input");
const packageInfoChangesCopyButton = document.getElementById("package-info-changes-copy-button"); const packageInfoChangesCopyButton = $("#package-info-changes-copy-button");
// so far bootstrap-table only operates with jquery elements const packageInfoEventsTable = $("#package-info-events-table");
const packageInfoEventsTable = $(document.getElementById("package-info-events-table"));
const packageInfoEventsUpdateChartCanvas = document.getElementById("package-info-events-update-chart"); const packageInfoEventsUpdateChartCanvas = document.getElementById("package-info-events-update-chart");
let packageInfoEventsUpdateChart = null; let packageInfoEventsUpdateChart = null;
const packageInfoAurUrl = document.getElementById("package-info-aur-url"); const packageInfoAurUrl = $("#package-info-aur-url");
const packageInfoDepends = document.getElementById("package-info-depends"); const packageInfoDepends = $("#package-info-depends");
const packageInfoGroups = document.getElementById("package-info-groups"); const packageInfoGroups = $("#package-info-groups");
const packageInfoLicenses = document.getElementById("package-info-licenses"); const packageInfoLicenses = $("#package-info-licenses");
const packageInfoPackager = document.getElementById("package-info-packager"); const packageInfoPackager = $("#package-info-packager");
const packageInfoPackages = document.getElementById("package-info-packages"); const packageInfoPackages = $("#package-info-packages");
const packageInfoUpstreamUrl = document.getElementById("package-info-upstream-url"); const packageInfoUpstreamUrl = $("#package-info-upstream-url");
const packageInfoVersion = document.getElementById("package-info-version"); const packageInfoVersion = $("#package-info-version");
const packageInfoVariablesBlock = document.getElementById("package-info-variables-block"); const packageInfoVariablesBlock = $("#package-info-variables-block");
const packageInfoVariablesDiv = document.getElementById("package-info-variables-div"); const packageInfoVariablesDiv = $("#package-info-variables-div");
function clearChart() { function clearChart() {
packageInfoEventsUpdateChartCanvas.hidden = true;
if (packageInfoEventsUpdateChart) { if (packageInfoEventsUpdateChart) {
packageInfoEventsUpdateChart.data = {}; packageInfoEventsUpdateChart.data = {};
packageInfoEventsUpdateChart.update(); packageInfoEventsUpdateChart.update();
@ -125,15 +121,20 @@
} }
async function copyChanges() { async function copyChanges() {
const changes = packageInfoChangesInput.textContent; const changes = packageInfoChangesInput.text();
await copyToClipboard(changes, packageInfoChangesCopyButton); await copyToClipboard(changes, packageInfoChangesCopyButton);
} }
async function copyLogs() { async function copyLogs() {
const logs = packageInfoLogsInput.textContent; const logs = packageInfoLogsInput.text();
await copyToClipboard(logs, packageInfoLogsCopyButton); await copyToClipboard(logs, packageInfoLogsCopyButton);
} }
function hideInfoControls(hidden) {
packageInfoRemoveButton.attr("hidden", hidden);
packageInfoUpdateButton.attr("hidden", hidden);
}
function highlight(element) { function highlight(element) {
delete element.dataset.highlighted; delete element.dataset.highlighted;
hljs.highlightElement(element); hljs.highlightElement(element);
@ -163,13 +164,12 @@
variableButtonRemove.classList.add("btn-outline-danger"); variableButtonRemove.classList.add("btn-outline-danger");
variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>"; variableButtonRemove.innerHTML = "<i class=\"bi bi-trash\"></i>";
variableButtonRemove.onclick = _ => { variableButtonRemove.onclick = _ => {
makeRequest( $.ajax({
`/api/v1/packages/${packageBase}/patches/${variable.key}`, url: `/api/v1/packages/${packageBase}/patches/${variable.key}`,
{ type: "DELETE",
method: "DELETE", dataType: "json",
}, success: _ => variableInput.remove(),
_ => variableInput.remove(), });
);
}; };
// bring them together // bring them together
@ -178,57 +178,52 @@
variableInput.appendChild(variableValueInput); variableInput.appendChild(variableValueInput);
variableInput.appendChild(variableButtonRemove); variableInput.appendChild(variableButtonRemove);
packageInfoVariablesDiv.appendChild(variableInput); packageInfoVariablesDiv.append(variableInput);
} }
function loadChanges(packageBase, onFailure) { function loadChanges(packageBase, onFailure) {
makeRequest( $.ajax({
`/api/v1/packages/${packageBase}/changes`, url: `/api/v1/packages/${packageBase}/changes`,
{ data: {
query: { architecture: repository.architecture,
architecture: repository.architecture, repository: repository.repository,
repository: repository.repository,
},
convert: response => response.json(),
}, },
data => { type: "GET",
const changes = data.changes; dataType: "json",
packageInfoChangesInput.textContent = changes ?? ""; success: response => {
highlight(packageInfoChangesInput); const changes = response.changes;
packageInfoChangesInput.text(changes || "");
packageInfoChangesInput.map((_, el) => highlight(el));
}, },
onFailure, error: onFailure,
); });
} }
function loadEvents(packageBase, onFailure) { function loadEvents(packageBase, onFailure) {
packageInfoEventsTable.bootstrapTable("showLoading"); packageInfoEventsTable.bootstrapTable("showLoading");
clearChart(); clearChart();
makeRequest( $.ajax({
"/api/v1/events", url: `/api/v1/events`,
{ data: {
query: { architecture: repository.architecture,
architecture: repository.architecture, repository: repository.repository,
repository: repository.repository, object_id: packageBase,
object_id: packageBase, limit: 30,
limit: 30,
},
convert: response => response.json(),
}, },
data => { type: "GET",
const events = data.map(event => { dataType: "json",
success: response => {
const events = response.map(event => {
return { return {
timestamp: new Date(1000 * event.created).toISOStringShort(), timestamp: new Date(1000 * event.created).toISOStringShort(),
event: event.event, event: event.event,
message: event.message || "", message: event.message || "",
}; };
}); });
const chart = data.filter(event => event.event === "package-updated");
packageInfoEventsTable.bootstrapTable("load", events);
packageInfoEventsTable.bootstrapTable("hideLoading");
if (packageInfoEventsUpdateChart) { if (packageInfoEventsUpdateChart) {
const chart = response.filter(event => event.event === "package-updated");
packageInfoEventsUpdateChart.config.data = { packageInfoEventsUpdateChart.config.data = {
labels: chart.map(event => new Date(1000 * event.created).toISOStringShort()), labels: chart.map(event => new Date(1000 * event.created).toISOStringShort()),
datasets: [{ datasets: [{
@ -240,31 +235,32 @@
}; };
packageInfoEventsUpdateChart.update(); packageInfoEventsUpdateChart.update();
} }
packageInfoEventsUpdateChartCanvas.hidden = !chart.length;
packageInfoEventsTable.bootstrapTable("load", events);
packageInfoEventsTable.bootstrapTable("hideLoading");
}, },
onFailure, error: onFailure,
); });
} }
function loadLogs(packageBase, onFailure) { function loadLogs(packageBase, onFailure) {
makeRequest( $.ajax({
`/api/v2/packages/${packageBase}/logs`, url: `/api/v2/packages/${packageBase}/logs`,
{ data: {
query: { architecture: repository.architecture,
architecture: repository.architecture, repository: repository.repository,
repository: repository.repository,
},
convert: response => response.json(),
}, },
data => { type: "GET",
const logs = data.map(log_record => { dataType: "json",
success: response => {
const logs = response.map(log_record => {
return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`; return `[${new Date(1000 * log_record.created).toISOString()}] ${log_record.message}`;
}); });
packageInfoLogsInput.textContent = logs.join("\n"); packageInfoLogsInput.text(logs.join("\n"));
highlight(packageInfoLogsInput); packageInfoLogsInput.map((_, el) => highlight(el));
}, },
onFailure, error: onFailure,
); });
} }
function loadPackage(packageBase, onFailure) { function loadPackage(packageBase, onFailure) {
@ -276,17 +272,16 @@
return ["bg-secondary", "text-white"]; return ["bg-secondary", "text-white"];
}; };
makeRequest( $.ajax({
`/api/v1/packages/${packageBase}`, url: `/api/v1/packages/${packageBase}`,
{ data: {
query: { architecture: repository.architecture,
architecture: repository.architecture, repository: repository.repository,
repository: repository.repository,
},
convert: response => response.json(),
}, },
data => { type: "GET",
const description = data.find(Boolean); dataType: "json",
success: response => {
const description = response.find(Boolean);
const packages = Object.keys(description.package.packages); const packages = Object.keys(description.package.packages);
const aurUrl = description.package.remote.web_url; const aurUrl = description.package.remote.web_url;
const upstreamUrls = Array.from( const upstreamUrls = Array.from(
@ -296,71 +291,72 @@
) )
).sort(); ).sort();
packageInfo.textContent = `${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`; packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`);
packageInfoModalHeader.classList.remove(...packageInfoModalHeader.classList); packageInfoModalHeader.removeClass();
packageInfoModalHeader.classList.add("modal-header"); packageInfoModalHeader.addClass("modal-header");
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.classList.add(clz)); headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz));
packageInfoAurUrl.innerHTML = aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : ""; packageInfoAurUrl.html(aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : "");
packageInfoDepends.innerHTML = listToTable( packageInfoDepends.html(listToTable(
Object.values(description.package.packages) Object.values(description.package.packages)
.reduce((accumulator, currentValue) => { .reduce((accumulator, currentValue) => {
return accumulator.concat(currentValue.depends.filter(v => packages.indexOf(v) === -1)) 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.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)`)); .concat(currentValue.opt_depends.filter(v => packages.indexOf(v) === -1).map(v => `${v} (optional)`));
}, []) }, [])
); ));
packageInfoGroups.innerHTML = listToTable(extractListProperties(description.package, "groups")); packageInfoGroups.html(listToTable(extractListProperties(description.package, "groups")));
packageInfoLicenses.innerHTML = listToTable(extractListProperties(description.package, "licenses")); packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses")));
packageInfoPackager.textContent = description.package.packager; packageInfoPackager.text(description.package.packager);
packageInfoPackages.innerHTML = listToTable(packages); packageInfoPackages.html(listToTable(packages));
packageInfoUpstreamUrl.innerHTML = upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>"); packageInfoUpstreamUrl.html(upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>"));
packageInfoVersion.textContent = description.package.version; packageInfoVersion.text(description.package.version);
hideInfoControls(false);
}, },
onFailure, error: (jqXHR, _, errorThrown) => {
); hideInfoControls(true);
onFailure(jqXHR, null, errorThrown);
},
});
} }
function loadPatches(packageBase, onFailure) { function loadPatches(packageBase, onFailure) {
makeRequest( $.ajax({
`/api/v1/packages/${packageBase}/patches`, url: `/api/v1/packages/${packageBase}/patches`,
{ type: "GET",
convert: response => response.json(), dataType: "json",
success: response => {
packageInfoVariablesDiv.empty();
response.map(patch => insertVariable(packageBase, patch));
packageInfoVariablesBlock.attr("hidden", response.length === 0);
}, },
data => { error: onFailure,
packageInfoVariablesDiv.replaceChildren(); });
data.map(patch => insertVariable(packageBase, patch));
packageInfoVariablesBlock.hidden = !data.length;
},
onFailure,
);
} }
function packageInfoRemove() { function packageInfoRemove() {
const packageBase = packageInfoModal.package; const packageBase = packageInfoModal.data("package");
packagesRemove([packageBase]); if (packageBase) return packagesRemove([packageBase]);
} }
function packageInfoUpdate() { function packageInfoUpdate() {
const packageBase = packageInfoModal.package; const packageBase = packageInfoModal.data("package");
packagesAdd(packageBase, [], repository); if (packageBase) return packagesAdd(packageBase, [], repository);
} }
function showPackageInfo(packageBase) { function showPackageInfo(packageBase) {
const isPackageBaseSet = packageBase !== undefined; const isPackageBaseSet = packageBase !== undefined;
if (isPackageBaseSet) { if (isPackageBaseSet)
// set package base as currently used packageInfoModal.data("package", packageBase); // set package base as currently used
packageInfoModal.package = packageBase; else
} else { packageBase = packageInfoModal.data("package"); // read package base from the current window attribute
// read package base from the current window attribute
packageBase = packageInfoModal.package;
}
const onFailure = error => { const onFailure = (jqXHR, _, errorThrown) => {
if (isPackageBaseSet) { if (isPackageBaseSet) {
const message = details => `Could not load package ${packageBase} info: ${details}`; const message = error => `Could not load package ${packageBase} info: ${error}`;
showFailure("Load failure", message, error); showFailure("Load failure", message, jqXHR, errorThrown);
} }
}; };
@ -370,12 +366,10 @@
loadChanges(packageBase, onFailure); loadChanges(packageBase, onFailure);
loadEvents(packageBase, onFailure); loadEvents(packageBase, onFailure);
if (isPackageBaseSet) { if (isPackageBaseSet) packageInfoModal.modal("show");
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
}
} }
ready(_ => { $(_ => {
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, { packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
type: "line", type: "line",
data: {}, data: {},
@ -384,23 +378,27 @@
}, },
}); });
packageInfoModal.addEventListener("hidden.bs.modal", _ => { packageInfoModal.on("hidden.bs.modal", _ => {
packageInfoAurUrl.textContent = ""; packageInfoAurUrl.empty();
packageInfoDepends.textContent = ""; packageInfoDepends.empty();
packageInfoGroups.textContent = ""; packageInfoGroups.empty();
packageInfoLicenses.textContent = ""; packageInfoLicenses.empty();
packageInfoPackager.textContent = ""; packageInfoPackager.empty();
packageInfoPackages.textContent = ""; packageInfoPackages.empty();
packageInfoUpstreamUrl.textContent = ""; packageInfoUpstreamUrl.empty();
packageInfoVersion.textContent = ""; packageInfoVersion.empty();
packageInfoVariablesBlock.hidden = true; packageInfoVariablesBlock.attr("hidden", true);
packageInfoVariablesDiv.replaceChildren(); packageInfoVariablesDiv.empty();
packageInfoLogsInput.textContent = ""; packageInfoLogsInput.empty();
packageInfoChangesInput.textContent = ""; packageInfoChangesInput.empty();
packageInfoEventsTable.bootstrapTable("load", []); packageInfoEventsTable.bootstrapTable("load", []);
clearChart(); clearChart();
packageInfoModal.trigger("reset");
hideInfoControls(true);
}); });
}); });
</script> </script>

View File

@ -33,31 +33,28 @@
</div> </div>
<script> <script>
const packageRebuildModal = document.getElementById("package-rebuild-modal"); const packageRebuildModal = $("#package-rebuild-modal");
const packageRebuildForm = document.getElementById("package-rebuild-form"); const packageRebuildForm = $("#package-rebuild-form");
const packageRebuildDependencyInput = document.getElementById("package-rebuild-dependency-input"); const packageRebuildDependencyInput = $("#package-rebuild-dependency-input");
const packageRebuildRepositoryInput = document.getElementById("package-rebuild-repository-input"); const packageRebuildRepositoryInput = $("#package-rebuild-repository-input");
function packagesRebuild() { function packagesRebuild() {
const packages = packageRebuildDependencyInput.value; const packages = packageRebuildDependencyInput.val();
const repository = getRepositorySelector(packageRebuildRepositoryInput); const repository = getRepositorySelector(packageRebuildRepositoryInput);
if (packages) { if (packages) {
bootstrap.Modal.getOrCreateInstance(packageRebuildModal).hide(); packageRebuildModal.modal("hide");
const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`; const onSuccess = update => `Repository rebuild has been run for packages which depend on ${update}`;
const onFailure = error => `Repository rebuild failed: ${error}`; const onFailure = error => `Repository rebuild failed: ${error}`;
doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure); doPackageAction("/api/v1/service/rebuild", [packages], repository, onSuccess, onFailure);
} }
} }
ready(_ => { $(_ => {
packageRebuildModal.addEventListener("shown.bs.modal", _ => { packageRebuildModal.on("shown.bs.modal", _ => {
const option = packageRebuildRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`); $(`#package-rebuild-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true);
option.selected = "selected";
}); });
packageRebuildModal.addEventListener("hidden.bs.modal", _ => { packageRebuildModal.on("hidden.bs.modal", _ => { packageRebuildForm.trigger("reset"); });
packageRebuildForm.reset();
});
}); });
</script> </script>

View File

@ -1,34 +1,39 @@
<script> <script>
const packageRemoveButton = document.getElementById("package-remove-button"); const keyImportButton = $("#key-import-button");
const packageUpdateButton = document.getElementById("package-update-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");
let repository = null; let repository = null;
// so far bootstrap-table only operates with jquery elements const table = $("#packages");
const table = $(document.getElementById("packages"));
const statusBadge = document.getElementById("badge-status"); const statusBadge = $("#badge-status");
const versionBadge = document.getElementById("badge-version"); const versionBadge = $("#badge-version");
function doPackageAction(uri, packages, repository, successText, failureText, data) { function doPackageAction(uri, packages, repository, successText, failureText, data) {
makeRequest( const queryParams = $.param({
uri, architecture: repository.architecture,
{ repository: repository.repository,
method: "POST", }); // it will never be empty btw
query: {
architecture: repository.architecture, $.ajax({
repository: repository.repository, url: `${uri}?${queryParams}`,
}, data: JSON.stringify(Object.assign({}, {packages: packages}, data || {})),
json: Object.assign({}, {packages: packages}, data || {}), type: "POST",
}, contentType: "application/json",
_ => { success: _ => {
const message = successText(packages.join(", ")); const message = successText(packages.join(", "));
showSuccess("Success", message); showSuccess("Success", message);
}, },
error => { error: (jqXHR, _, errorThrown) => {
showFailure("Action failed", failureText, error); showFailure("Action failed", failureText, jqXHR, errorThrown);
}, },
); });
} }
function filterListGroups() { function filterListGroups() {
@ -44,10 +49,10 @@
} }
function getRepositorySelector(selector) { function getRepositorySelector(selector) {
const selected = selector.options[selector.selectedIndex]; const selected = selector.find(":selected");
return { return {
architecture: selected.getAttribute("data-architecture"), architecture: selected.data("architecture"),
repository: selected.getAttribute("data-repository"), repository: selected.data("repository"),
}; };
} }
@ -55,6 +60,14 @@
return table.bootstrapTable("getSelections").map(row => row.id); 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) { function packagesRemove(packages) {
packages = packages ?? getSelection(); packages = packages ?? getSelection();
const onSuccess = update => `Packages ${update} have been removed`; const onSuccess = update => `Packages ${update} have been removed`;
@ -84,17 +97,16 @@
return "btn-outline-secondary"; return "btn-outline-secondary";
}; };
makeRequest( $.ajax({
"/api/v1/packages", url: "/api/v1/packages",
{ data: {
query: { architecture: repository.architecture,
architecture: repository.architecture, repository: repository.repository,
repository: repository.repository,
},
convert: response => response.json(),
}, },
data => { type: "GET",
const payload = data.map(description => { dataType: "json",
success: response => {
const payload = response.map(description => {
const package_base = description.package.base; const package_base = description.package.base;
const web_url = description.package.remote.web_url; const web_url = description.package.remote.web_url;
return { return {
@ -113,9 +125,10 @@
table.bootstrapTable("load", payload); table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll"); table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
hideControls(false);
}, },
error => { error: (jqXHR, _, errorThrown) => {
if ((error.status === 401) || (error.status === 403)) { if ((jqXHR.status === 401) || (jqXHR.status === 403)) {
// authorization error // authorization error
const text = "In order to see statuses you must login first."; const text = "In order to see statuses you must login first.";
table.find("tr.unauthorized").remove(); table.find("tr.unauthorized").remove();
@ -123,39 +136,39 @@
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
} else { } else {
// other errors // other errors
const message = details => `Could not load list of packages: ${details}`; const message = error => `Could not load list of packages: ${error}`;
showFailure("Load failure", message, error); showFailure("Load failure", message, jqXHR, errorThrown);
} }
hideControls(true);
}, },
); });
makeRequest( $.ajax({
"/api/v1/status", url: "/api/v1/status",
{ data: {
query: { architecture: repository.architecture,
architecture: repository.architecture, repository: repository.repository,
repository: repository.repository,
},
convert: response => response.json(),
}, },
data => { type: "GET",
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`; dataType: "json",
success: response => {
versionBadge.html(`<i class="bi bi-github"></i> ahriman ${safe(response.version)}`);
statusBadge.classList.remove(...statusBadge.classList); statusBadge
statusBadge.classList.add("btn"); .popover("dispose")
statusBadge.classList.add(badgeClass(data.status.status)); .attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`)
.popover();
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge); statusBadge.removeClass();
popover.dispose(); statusBadge.addClass("btn");
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`; statusBadge.addClass(badgeClass(response.status.status));
bootstrap.Popover.getOrCreateInstance(statusBadge);
}, },
); });
} }
function selectRepository() { function selectRepository() {
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
document.getElementById(`${fragment}-link`).click(); const element = $(`#${fragment}-link`);
element.click();
} }
function statusFormat(value) { function statusFormat(value) {
@ -169,25 +182,20 @@
return {classes: cellClass(value)}; return {classes: cellClass(value)};
} }
ready(_ => { $(_ => {
document.querySelectorAll("#repositories a").forEach(element => { $("#repositories a").on("click", event => {
element.onclick = _ => { const element = event.target;
repository = { repository = {
architecture: element.dataset.architecture, architecture: element.dataset.architecture,
repository: element.dataset.repository, 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", _ => { table.on("check.bs.table uncheck.bs.table check-all.bs.table uncheck-all.bs.table", _ => {
if (packageRemoveButton) { packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length);
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length;
}
}); });
table.on("click-row.bs.table", (self, data, row, cell) => { table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) { if (0 === cell || "base" === cell) {
@ -196,38 +204,26 @@
} else showPackageInfo(data.id); } else showPackageInfo(data.id);
}); });
table.on("created-controls.bs.table", _ => { table.on("created-controls.bs.table", _ => {
new easepick.create({ const pickerInput = $(".bootstrap-table-filter-control-timestamp");
element: document.querySelector(".bootstrap-table-filter-control-timestamp"), pickerInput.daterangepicker({
css: [ autoUpdateInput: false,
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: { locale: {
cancel: "Clear", cancelLabel: "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");
});
}); });
bootstrap.Popover.getOrCreateInstance(statusBadge); statusBadge.popover();
selectRepository(); selectRepository();
}); });
</script> </script>

View File

@ -105,13 +105,13 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
</div> </div>
<script> <script>
const table = $(document.getElementById("packages")); const table = $("#packages");
const pacmanConf = document.getElementById("pacman-conf"); const pacmanConf = $("#pacman-conf");
const pacmanConfCopyButton = document.getElementById("copy-btn"); const pacmanConfCopyButton = $("#copy-btn");
async function copyPacmanConf() { async function copyPacmanConf() {
const conf = pacmanConf.textContent; const conf = pacmanConf.text();
await copyToClipboard(conf, pacmanConfCopyButton); await copyToClipboard(conf, pacmanConfCopyButton);
} }
@ -127,37 +127,25 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
return extractDataList(table.bootstrapTable("getData"), "licenses"); return extractDataList(table.bootstrapTable("getData"), "licenses");
} }
ready(_ => { $(_ => {
table.on("created-controls.bs.table", _ => { table.on("created-controls.bs.table", _ => {
new easepick.create({ const pickerInput = $(".bootstrap-table-filter-control-timestamp");
element: document.querySelector(".bootstrap-table-filter-control-timestamp"), pickerInput.daterangepicker({
css: [ autoUpdateInput: false,
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: { locale: {
cancel: "Clear", cancelLabel: "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");
});
}); });
}); });
</script> </script>

View File

@ -1,30 +1,41 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/jquery.md5@1.0.2/index.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/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/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/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/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.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.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.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.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.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.9.0/build/highlight.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 src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script> <script>
async function copyToClipboard(text, button) { async function copyToClipboard(text, button) {
await navigator.clipboard.writeText(text); if (navigator.clipboard === undefined) {
button.innerHTML = "<i class=\"bi bi-clipboard-check\"></i> copied"; const input = document.createElement("textarea");
setTimeout(_ => { input.innerHTML = text;
button.innerHTML = "<i class=\"bi bi-clipboard\"></i> copy"; 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");
}, 2000); }, 2000);
} }
@ -65,47 +76,6 @@
.join("<br>"); .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) { function safe(string) {
return String(string) return String(string)
.replace(/&/g, "&amp;") .replace(/&/g, "&amp;")
@ -119,9 +89,7 @@
const element = document.createElement("a"); const element = document.createElement("a");
element.href = url; element.href = url;
element.innerText = text; element.innerText = text;
if (title) { if (title) element.title = title;
element.title = title;
}
return element; return element;
} }

View File

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

View File

@ -1,4 +1,4 @@
.TH AHRIMAN "1" "2024\-09\-04" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2024\-08\-23" "ahriman" "Generated Python Manual"
.SH NAME .SH NAME
ahriman ahriman
.SH SYNOPSIS .SH SYNOPSIS

View File

@ -17,4 +17,4 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
__version__ = "2.14.1" __version__ = "2.14.0"

View File

@ -75,9 +75,7 @@ class Lock(LazyLogging):
""" """
self.path: Path | None = None self.path: Path | None = None
if args.lock is not None: if args.lock is not None:
self.path = args.lock self.path = args.lock.with_stem(f"{args.lock.stem}_{repository_id.id}")
if not repository_id.is_empty:
self.path = self.path.with_stem(f"{args.lock.stem}_{repository_id.id}")
if not self.path.is_absolute(): if not self.path.is_absolute():
# prepend full path to the lock file # prepend full path to the lock file
self.path = Path("/") / "run" / "ahriman" / self.path self.path = Path("/") / "run" / "ahriman" / self.path

View File

@ -27,7 +27,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.database.migrations import Migrations from ahriman.core.database.migrations import Migrations
from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \ from ahriman.core.database.operations import AuthOperations, BuildOperations, ChangesOperations, \
DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations DependenciesOperations, EventOperations, LogsOperations, PackageOperations, PatchOperations
from ahriman.models.repository_id import RepositoryId
# pylint: disable=too-many-ancestors # pylint: disable=too-many-ancestors
@ -104,26 +103,23 @@ class SQLite(
self.with_connection(lambda connection: Migrations.migrate(connection, configuration)) self.with_connection(lambda connection: Migrations.migrate(connection, configuration))
paths.chown(self.path) paths.chown(self.path)
def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> None: def package_clear(self, package_base: str) -> None:
""" """
completely remove package from all tables completely remove package from all tables
Args: Args:
package_base(str): package base to remove package_base(str): package base to remove
repository_id(RepositoryId, optional): repository unique identifier override (Default value = None)
Examples: Examples:
This method completely removes the package from all tables and must be used, e.g. on package removal:: This method completely removes the package from all tables and must be used, e.g. on package removal::
>>> database.package_clear("ahriman") >>> database.package_clear("ahriman")
""" """
self.build_queue_clear(package_base, repository_id) self.build_queue_clear(package_base)
self.patches_remove(package_base, None) self.patches_remove(package_base, [])
self.logs_remove(package_base, None, repository_id) self.logs_remove(package_base, None)
self.changes_remove(package_base, repository_id) self.changes_remove(package_base)
self.dependencies_remove(package_base, repository_id) self.dependencies_remove(package_base)
self.package_remove(package_base, repository_id)
# remove local cache too # remove local cache too
self._repository_paths.tree_clear(package_base) self._repository_paths.tree_clear(package_base)

View File

@ -213,7 +213,7 @@ class LocalClient(Client):
Args: Args:
package_base(str): package base to remove package_base(str): package base to remove
""" """
self.database.package_clear(package_base, self.repository_id) self.database.package_clear(package_base)
def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None: def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
""" """

View File

@ -145,6 +145,7 @@ class Watcher(LazyLogging):
with self._lock: with self._lock:
self._known.pop(package_base, None) self._known.pop(package_base, None)
self.client.package_remove(package_base) self.client.package_remove(package_base)
self.package_logs_remove(package_base, None)
def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None: def package_status_update(self, package_base: str, status: BuildStatusEnum) -> None:
""" """

View File

@ -41,12 +41,9 @@ class RepositoryId:
Returns: Returns:
str: unique id for this repository str: unique id for this repository
Raises:
ValueError: if repository identifier is empty
""" """
if self.is_empty: if self.is_empty:
raise ValueError("Repository ID is called on empty repository identifier") return ""
return f"{self.architecture}-{self.name}" # basically the same as used for command line return f"{self.architecture}-{self.name}" # basically the same as used for command line
@property @property

View File

@ -14,7 +14,6 @@ from ahriman.core.configuration import Configuration
from ahriman.core.exceptions import DuplicateRunError, UnsafeRunError from ahriman.core.exceptions import DuplicateRunError, UnsafeRunError
from ahriman.models.build_status import BuildStatus, BuildStatusEnum from ahriman.models.build_status import BuildStatus, BuildStatusEnum
from ahriman.models.internal_status import InternalStatus from ahriman.models.internal_status import InternalStatus
from ahriman.models.repository_id import RepositoryId
def test_path(args: argparse.Namespace, configuration: Configuration) -> None: def test_path(args: argparse.Namespace, configuration: Configuration) -> None:
@ -31,8 +30,6 @@ def test_path(args: argparse.Namespace, configuration: Configuration) -> None:
args.lock = Path("ahriman.pid") args.lock = Path("ahriman.pid")
assert Lock(args, repository_id, configuration).path == Path("/run/ahriman/ahriman_x86_64-aur-clone.pid") assert Lock(args, repository_id, configuration).path == Path("/run/ahriman/ahriman_x86_64-aur-clone.pid")
assert Lock(args, RepositoryId("", ""), configuration).path == Path("/run/ahriman/ahriman.pid")
with pytest.raises(ValueError): with pytest.raises(ValueError):
args.lock = Path("/") args.lock = Path("/")
assert Lock(args, repository_id, configuration).path # special case assert Lock(args, repository_id, configuration).path # special case

View File

@ -4,7 +4,6 @@ from pytest_mock import MockerFixture
from ahriman.core.configuration import Configuration from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite from ahriman.core.database import SQLite
from ahriman.models.repository_id import RepositoryId
def test_load(configuration: Configuration, mocker: MockerFixture) -> None: def test_load(configuration: Configuration, mocker: MockerFixture) -> None:
@ -36,7 +35,7 @@ def test_init_skip_migration(database: SQLite, configuration: Configuration, moc
migrate_schema_mock.assert_not_called() migrate_schema_mock.assert_not_called()
def test_package_clear(database: SQLite, repository_id: RepositoryId, mocker: MockerFixture) -> None: def test_package_clear(database: SQLite, mocker: MockerFixture) -> None:
""" """
must clear package data must clear package data
""" """
@ -45,14 +44,12 @@ def test_package_clear(database: SQLite, repository_id: RepositoryId, mocker: Mo
logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove") logs_mock = mocker.patch("ahriman.core.database.SQLite.logs_remove")
changes_mock = mocker.patch("ahriman.core.database.SQLite.changes_remove") changes_mock = mocker.patch("ahriman.core.database.SQLite.changes_remove")
dependencies_mock = mocker.patch("ahriman.core.database.SQLite.dependencies_remove") dependencies_mock = mocker.patch("ahriman.core.database.SQLite.dependencies_remove")
package_mock = mocker.patch("ahriman.core.database.SQLite.package_remove")
tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear") tree_clear_mock = mocker.patch("ahriman.models.repository_paths.RepositoryPaths.tree_clear")
database.package_clear("package", repository_id) database.package_clear("package")
build_queue_mock.assert_called_once_with("package", repository_id) build_queue_mock.assert_called_once_with("package")
patches_mock.assert_called_once_with("package", None) patches_mock.assert_called_once_with("package", [])
logs_mock.assert_called_once_with("package", None, repository_id) logs_mock.assert_called_once_with("package", None)
changes_mock.assert_called_once_with("package", repository_id) changes_mock.assert_called_once_with("package")
dependencies_mock.assert_called_once_with("package", repository_id) dependencies_mock.assert_called_once_with("package")
package_mock.assert_called_once_with("package", repository_id)
tree_clear_mock.assert_called_once_with("package") tree_clear_mock.assert_called_once_with("package")

View File

@ -180,7 +180,7 @@ def test_package_remove(local_client: LocalClient, package_ahriman: Package, moc
""" """
package_mock = mocker.patch("ahriman.core.database.SQLite.package_clear") package_mock = mocker.patch("ahriman.core.database.SQLite.package_clear")
local_client.package_remove(package_ahriman.base) local_client.package_remove(package_ahriman.base)
package_mock.assert_called_once_with(package_ahriman.base, local_client.repository_id) package_mock.assert_called_once_with(package_ahriman.base)
def test_package_status_update(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_status_update(local_client: LocalClient, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -101,11 +101,13 @@ def test_package_remove(watcher: Watcher, package_ahriman: Package, mocker: Mock
must remove package base must remove package base
""" """
cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove") cache_mock = mocker.patch("ahriman.core.status.local_client.LocalClient.package_remove")
logs_mock = mocker.patch("ahriman.core.status.watcher.Watcher.package_logs_remove", create=True)
watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())} watcher._known = {package_ahriman.base: (package_ahriman, BuildStatus())}
watcher.package_remove(package_ahriman.base) watcher.package_remove(package_ahriman.base)
assert not watcher._known assert not watcher._known
cache_mock.assert_called_once_with(package_ahriman.base) cache_mock.assert_called_once_with(package_ahriman.base)
logs_mock.assert_called_once_with(package_ahriman.base, None)
def test_package_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None: def test_package_remove_unknown(watcher: Watcher, package_ahriman: Package, mocker: MockerFixture) -> None:

View File

@ -7,17 +7,10 @@ def test_id() -> None:
""" """
must correctly generate id must correctly generate id
""" """
assert RepositoryId("", "").id == ""
assert RepositoryId("arch", "repo").id == "arch-repo" assert RepositoryId("arch", "repo").id == "arch-repo"
def test_id_empty() -> None:
"""
must raise exception on empty identifier
"""
with pytest.raises(ValueError):
assert RepositoryId("", "").id
def test_is_empty() -> None: def test_is_empty() -> None:
""" """
must check if repository id is empty or not must check if repository id is empty or not

View File

@ -201,7 +201,7 @@ def test_service_not_found(base: BaseView) -> None:
must raise HTTPNotFound if no repository found must raise HTTPNotFound if no repository found
""" """
with pytest.raises(HTTPNotFound): with pytest.raises(HTTPNotFound):
base.service(RepositoryId("repo", "arch")) base.service(RepositoryId("", ""))
def test_service_package(base: BaseView, repository_id: RepositoryId, mocker: MockerFixture) -> None: def test_service_package(base: BaseView, repository_id: RepositoryId, mocker: MockerFixture) -> None: