Compare commits

..

23 Commits

Author SHA1 Message Date
1d85a61cc4
feat: get rid of jquery (#133) 2024-09-05 02:26:52 +03:00
689de82139 build: make cerberus dependency optional 2024-09-04 22:28:25 +03:00
5b9f35220f feat: implement stats subcommand (#132) 2024-09-04 22:28:25 +03:00
8fc4d7b4a5 feat: allow filter events by timestamp 2024-09-04 22:28:25 +03:00
cedf18ac7a chore: add rss generation to samples 2024-09-04 22:28:25 +03:00
164b6d7956 feat: add event log and update chart to package info modal 2024-09-04 22:28:25 +03:00
27e595cdf4 feat: remove duplicates from the toast 2024-09-04 22:28:25 +03:00
020560d341 refactor: simplify Validator class 2024-09-04 22:28:25 +03:00
cdef67986b feat: allow cross reference in the configuration (#131) 2024-09-04 22:28:25 +03:00
dddcd0bfce feat: implement rss generation (#130) 2024-09-04 22:28:25 +03:00
a0784b7af1 feat: add ability to log sql statements 2024-09-04 22:28:25 +03:00
4c4c9b2bfd 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-09-04 22:28:25 +03:00
5c34c051cb feat: log package update events 2024-09-04 22:28:25 +03:00
4fa44b0532 refactor: allow event to receive keyword arguments
This change also replaces the dataclass implementation of the class to
custom one
2024-09-04 22:28:25 +03:00
f167ce7d3b feat: add timer for metrics purposes 2024-09-04 22:28:25 +03:00
950b9e4289 docs: update booleans in docs 2024-09-04 22:28:25 +03:00
264aeb7150 feat: implement audit log tables and methods (#129) 2024-09-04 22:28:25 +03:00
be7169c5df 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-09-04 22:25:54 +03:00
9c1e9ecbdc Release 2.14.1 2024-09-04 22:01:04 +03:00
4b2f6bbee9 bug: fix removal of the packages
It has been broken since reporter improvements, because it effectivelly
1) didn't call remove functions in database
2) used empty repository identifier for web service

With those changes it also raises exception when you try to call id on
empty identifier
2024-09-04 21:50:33 +03:00
fd8c8a00d0 chore: small contributing guide update 2024-09-04 21:49:31 +03:00
eaf1984eb3 refactor: fix some IDE warnings 2024-09-04 21:49:31 +03:00
794dddccd9 build: update pytest configuration to suppress deprecation warnings 2024-09-04 21:49:31 +03:00
26 changed files with 5701 additions and 5608 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.0 pkgver=2.14.1
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" hidden> <button id="package-add-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-add-modal">
<i class="bi bi-plus"></i> add <i class="bi bi-plus"></i> add
</button> </button>
</li> </li>
<li> <li>
<button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()" hidden> <button id="package-update-button" class="btn dropdown-item" onclick="packagesUpdate()">
<i class="bi bi-play"></i> update <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" hidden> <button id="package-rebuild-button" class="btn dropdown-item" data-bs-toggle="modal" data-bs-target="#package-rebuild-modal">
<i class="bi bi-arrow-clockwise"></i> rebuild <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 hidden> <button id="package-remove-button" class="btn dropdown-item" onclick="packagesRemove()" disabled>
<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" hidden> <button id="key-import-button" type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#key-import-modal">
<i class="bi bi-key"></i><span class="d-none d-sm-inline"> import key</span> <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 = $("#alert-placeholder"); const alertPlaceholder = document.getElementById("alert-placeholder");
function createAlert(title, message, clz, action, id) { function createAlert(title, message, clz, action, id) {
if (!id) id = $.md5(title + message); // MD5 id from the content id ??= md5(title + message); // MD5 id from the content
if (alertPlaceholder.find(`#${id}`).length > 0) return; // check if there are duplicates if (alertPlaceholder.querySelector(`#alert-${id}`)) return; // check if there are duplicates
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.id = id; wrapper.id = `alert-${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.append(wrapper); alertPlaceholder.appendChild(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, jqXHR, errorThrown) { function showFailure(title, description, error) {
let details; let details;
try { try {
details = $.parseJSON(jqXHR.responseText).error; // execution handler json error response details = JSON.parse(error.text).error; // execution handler json error response
} catch (_) { } catch (_) {
details = errorThrown; details = error.text ?? error.message ?? error;
} }
createAlert(title, description(details), "text-bg-danger"); createAlert(title, description(details), "text-bg-danger");
} }

View File

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

View File

@ -34,53 +34,57 @@
</div> </div>
<script> <script>
const loginModal = $("#login-modal"); const loginModal = document.getElementById("login-modal");
const loginForm = $("#login-form"); const loginForm = document.getElementById("login-form");
const loginPasswordInput = $("#login-password"); const loginPasswordInput = document.getElementById("login-password");
const loginUsernameInput = $("#login-username"); const loginUsernameInput = document.getElementById("login-username");
const showHidePasswordButton = $("#login-show-hide-password-button"); const showHidePasswordButton = document.getElementById("login-show-hide-password-button");
function login() { function login() {
const password = loginPasswordInput.val(); const password = loginPasswordInput.value;
const username = loginUsernameInput.val(); const username = loginUsernameInput.value;
if (username && password) { if (username && password) {
$.ajax({ makeRequest(
url: "/api/v1/login", "/api/v1/login",
data: JSON.stringify({username: username, password: password}), {
type: "POST", method: "POST",
contentType: "application/json", json: {
success: _ => { username: username,
loginModal.modal("hide"); password: password,
},
},
_ => {
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: (jqXHR, _, errorThrown) => { error => {
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, jqXHR, errorThrown); showFailure("Login error", message, error);
}, },
}); );
} }
} }
function showPassword() { function showPassword() {
if (loginPasswordInput.attr("type") === "password") { if (loginPasswordInput.getAttribute("type") === "password") {
loginPasswordInput.attr("type", "text"); loginPasswordInput.setAttribute("type", "text");
showHidePasswordButton.removeClass("bi-eye"); showHidePasswordButton.classList.remove("bi-eye");
showHidePasswordButton.addClass("bi-eye-slash"); showHidePasswordButton.classList.add("bi-eye-slash");
} else { } else {
loginPasswordInput.attr("type", "password"); loginPasswordInput.setAttribute("type", "password");
showHidePasswordButton.removeClass("bi-eye-slash"); showHidePasswordButton.classList.remove("bi-eye-slash");
showHidePasswordButton.addClass("bi-eye"); showHidePasswordButton.classList.add("bi-eye");
} }
} }
$(_ => { ready(_ => {
loginModal.on("hidden.bs.modal", _ => { loginModal.addEventListener("hidden.bs.modal", _ => {
loginForm.trigger("reset"); loginForm.reset();
}); });
}); });
</script> </script>

View File

@ -41,14 +41,14 @@
</div> </div>
<script> <script>
const packageAddModal = $("#package-add-modal"); const packageAddModal = document.getElementById("package-add-modal");
const packageAddForm = $("#package-add-form"); const packageAddForm = document.getElementById("package-add-form");
const packageAddInput = $("#package-add-input"); const packageAddInput = document.getElementById("package-add-input");
const packageAddRepositoryInput = $("#package-add-repository-input"); const packageAddRepositoryInput = document.getElementById("package-add-repository-input");
const packageAddKnownPackagesList = $("#package-add-known-packages-dlist"); const packageAddKnownPackagesList = document.getElementById("package-add-known-packages-dlist");
const packageAddVariablesDiv = $("#package-add-variables-div"); const packageAddVariablesDiv = document.getElementById("package-add-variables-div");
function packageAddVariableInputCreate() { 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 = _ => { return variableInput.remove(); }; variableButtonRemove.onclick = _ => { variableInput.remove(); };
// bring them together // bring them together
variableInput.appendChild(variableNameInput); variableInput.appendChild(variableNameInput);
@ -86,27 +86,26 @@
variableInput.appendChild(variableValueInput); variableInput.appendChild(variableValueInput);
variableInput.appendChild(variableButtonRemove); variableInput.appendChild(variableButtonRemove);
packageAddVariablesDiv.append(variableInput); packageAddVariablesDiv.appendChild(variableInput);
} }
function patchesParse() { function patchesParse() {
const patches = packageAddVariablesDiv.find(".package-add-variable").map((_, element) => { const patches = Array.from(packageAddVariablesDiv.getElementsByClassName("package-add-variable")).map(element => {
const richElement = $(element);
return { return {
key: richElement.find(".package-add-variable-name").val(), key: element.querySelector(".package-add-variable-name").value,
value: richElement.find(".package-add-variable-value").val(), value: element.querySelector(".package-add-variable-value").value,
}; };
}).filter((_, patch) => patch.key).get(); }).filter(patch => patch.key);
return {patches: patches}; return {patches: patches};
} }
function packagesAdd(packages, patches, repository) { function packagesAdd(packages, patches, repository) {
packages = packages ?? packageAddInput.val(); packages = packages ?? packageAddInput.value;
patches = patches ?? patchesParse(); patches = patches ?? patchesParse();
repository = repository ?? getRepositorySelector(packageAddRepositoryInput); repository = repository ?? getRepositorySelector(packageAddRepositoryInput);
if (packages) { if (packages) {
packageAddModal.modal("hide"); bootstrap.Modal.getOrCreateInstance(packageAddModal).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);
@ -114,50 +113,54 @@
} }
function packagesRequest(packages, patches) { function packagesRequest(packages, patches) {
packages = packages ?? packageAddInput.val(); packages = packages ?? packageAddInput.value;
patches = patches ?? patchesParse(); patches = patches ?? patchesParse();
const repository = getRepositorySelector(packageAddRepositoryInput); const repository = getRepositorySelector(packageAddRepositoryInput);
if (packages) { if (packages) {
packageAddModal.modal("hide"); bootstrap.Modal.getOrCreateInstance(packageAddModal).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.on("shown.bs.modal", _ => { packageAddModal.addEventListener("shown.bs.modal", _ => {
$(`#package-add-repository-input option[value="${repository.architecture}-${repository.repository}"]`).prop("selected", true); const option = packageAddRepositoryInput.querySelector(`option[value="${repository.architecture}-${repository.repository}"]`);
option.selected = "selected";
}); });
packageAddModal.on("hidden.bs.modal", _ => { packageAddModal.addEventListener("hidden.bs.modal", _ => {
packageAddVariablesDiv.empty(); packageAddVariablesDiv.replaceChildren();
packageAddForm.trigger("reset"); packageAddForm.reset();
}); });
packageAddInput.keyup(_ => { packageAddInput.addEventListener("keyup", _ => {
clearTimeout(packageAddInput.data("timeout")); clearTimeout(packageAddInput.requestTimeout);
packageAddInput.data("timeout", setTimeout($.proxy(_ => { packageAddInput.requestTimeout = setTimeout(_ => {
const value = packageAddInput.val(); const value = packageAddInput.value;
if (value.length >= 3) { if (value.length >= 3) {
$.ajax({ makeRequest(
url: "/api/v1/service/search", "/api/v1/service/search",
data: {"for": value}, {
type: "GET", query: {
dataType: "json", for: value,
success: response => { },
const options = response.map(pkg => { convert: response => response.json(),
},
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.empty().append(options); packageAddKnownPackagesList.replaceChildren(...options);
}, },
}); );
} }
}, this), 500)); }, 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"></canvas> <canvas id="package-info-events-update-chart" hidden></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,8 +77,10 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button id="package-info-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal" hidden><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button> {% if not auth.enabled or auth.username is not none %}
<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-update-button" type="submit" class="btn btn-success" onclick="packageInfoUpdate()" data-bs-dismiss="modal"><i class="bi bi-play"></i><span class="d-none d-sm-inline"> update</span></button>
<button id="package-info-remove-button" type="submit" class="btn btn-danger" onclick="packageInfoRemove()" data-bs-dismiss="modal"><i class="bi bi-trash"></i><span class="d-none d-sm-inline"> remove</span></button>
{% endif %}
<button type="button" class="btn btn-secondary" onclick="showPackageInfo()"><i class="bi bi-arrow-clockwise"></i><span class="d-none d-sm-inline"> reload</span></button> <button type="button" class="btn btn-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>
@ -87,33 +89,35 @@
</div> </div>
<script> <script>
const packageInfoModal = $("#package-info-modal"); const packageInfoModal = document.getElementById("package-info-modal");
const packageInfoModalHeader = $("#package-info-modal-header"); const packageInfoModalHeader = document.getElementById("package-info-modal-header");
const packageInfo = $("#package-info"); const packageInfo = document.getElementById("package-info");
const packageInfoLogsInput = $("#package-info-logs-input"); const packageInfoLogsInput = document.getElementById("package-info-logs-input");
const packageInfoLogsCopyButton = $("#package-info-logs-copy-button"); const packageInfoLogsCopyButton = document.getElementById("package-info-logs-copy-button");
const packageInfoChangesInput = $("#package-info-changes-input"); const packageInfoChangesInput = document.getElementById("package-info-changes-input");
const packageInfoChangesCopyButton = $("#package-info-changes-copy-button"); const packageInfoChangesCopyButton = document.getElementById("package-info-changes-copy-button");
const packageInfoEventsTable = $("#package-info-events-table"); // so far bootstrap-table only operates with jquery elements
const packageInfoEventsTable = $(document.getElementById("package-info-events-table"));
const packageInfoEventsUpdateChartCanvas = document.getElementById("package-info-events-update-chart"); const packageInfoEventsUpdateChartCanvas = document.getElementById("package-info-events-update-chart");
let packageInfoEventsUpdateChart = null; let packageInfoEventsUpdateChart = null;
const packageInfoAurUrl = $("#package-info-aur-url"); const packageInfoAurUrl = document.getElementById("package-info-aur-url");
const packageInfoDepends = $("#package-info-depends"); const packageInfoDepends = document.getElementById("package-info-depends");
const packageInfoGroups = $("#package-info-groups"); const packageInfoGroups = document.getElementById("package-info-groups");
const packageInfoLicenses = $("#package-info-licenses"); const packageInfoLicenses = document.getElementById("package-info-licenses");
const packageInfoPackager = $("#package-info-packager"); const packageInfoPackager = document.getElementById("package-info-packager");
const packageInfoPackages = $("#package-info-packages"); const packageInfoPackages = document.getElementById("package-info-packages");
const packageInfoUpstreamUrl = $("#package-info-upstream-url"); const packageInfoUpstreamUrl = document.getElementById("package-info-upstream-url");
const packageInfoVersion = $("#package-info-version"); const packageInfoVersion = document.getElementById("package-info-version");
const packageInfoVariablesBlock = $("#package-info-variables-block"); const packageInfoVariablesBlock = document.getElementById("package-info-variables-block");
const packageInfoVariablesDiv = $("#package-info-variables-div"); const packageInfoVariablesDiv = document.getElementById("package-info-variables-div");
function clearChart() { function clearChart() {
packageInfoEventsUpdateChartCanvas.hidden = true;
if (packageInfoEventsUpdateChart) { if (packageInfoEventsUpdateChart) {
packageInfoEventsUpdateChart.data = {}; packageInfoEventsUpdateChart.data = {};
packageInfoEventsUpdateChart.update(); packageInfoEventsUpdateChart.update();
@ -121,20 +125,15 @@
} }
async function copyChanges() { async function copyChanges() {
const changes = packageInfoChangesInput.text(); const changes = packageInfoChangesInput.textContent;
await copyToClipboard(changes, packageInfoChangesCopyButton); await copyToClipboard(changes, packageInfoChangesCopyButton);
} }
async function copyLogs() { async function copyLogs() {
const logs = packageInfoLogsInput.text(); const logs = packageInfoLogsInput.textContent;
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);
@ -164,12 +163,13 @@
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 = _ => {
$.ajax({ makeRequest(
url: `/api/v1/packages/${packageBase}/patches/${variable.key}`, `/api/v1/packages/${packageBase}/patches/${variable.key}`,
type: "DELETE", {
dataType: "json", method: "DELETE",
success: _ => variableInput.remove(), },
}); _ => variableInput.remove(),
);
}; };
// bring them together // bring them together
@ -178,52 +178,57 @@
variableInput.appendChild(variableValueInput); variableInput.appendChild(variableValueInput);
variableInput.appendChild(variableButtonRemove); variableInput.appendChild(variableButtonRemove);
packageInfoVariablesDiv.append(variableInput); packageInfoVariablesDiv.appendChild(variableInput);
} }
function loadChanges(packageBase, onFailure) { function loadChanges(packageBase, onFailure) {
$.ajax({ makeRequest(
url: `/api/v1/packages/${packageBase}/changes`, `/api/v1/packages/${packageBase}/changes`,
data: { {
query: {
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
}, },
type: "GET", convert: response => response.json(),
dataType: "json",
success: response => {
const changes = response.changes;
packageInfoChangesInput.text(changes || "");
packageInfoChangesInput.map((_, el) => highlight(el));
}, },
error: onFailure, data => {
}); const changes = data.changes;
packageInfoChangesInput.textContent = changes ?? "";
highlight(packageInfoChangesInput);
},
onFailure,
);
} }
function loadEvents(packageBase, onFailure) { function loadEvents(packageBase, onFailure) {
packageInfoEventsTable.bootstrapTable("showLoading"); packageInfoEventsTable.bootstrapTable("showLoading");
clearChart(); clearChart();
$.ajax({ makeRequest(
url: `/api/v1/events`, "/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,
}, },
type: "GET", convert: response => response.json(),
dataType: "json", },
success: response => { data => {
const events = response.map(event => { const events = data.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: [{
@ -235,32 +240,31 @@
}; };
packageInfoEventsUpdateChart.update(); packageInfoEventsUpdateChart.update();
} }
packageInfoEventsUpdateChartCanvas.hidden = !chart.length;
packageInfoEventsTable.bootstrapTable("load", events);
packageInfoEventsTable.bootstrapTable("hideLoading");
}, },
error: onFailure, onFailure,
}); );
} }
function loadLogs(packageBase, onFailure) { function loadLogs(packageBase, onFailure) {
$.ajax({ makeRequest(
url: `/api/v2/packages/${packageBase}/logs`, `/api/v2/packages/${packageBase}/logs`,
data: { {
query: {
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
}, },
type: "GET", convert: response => response.json(),
dataType: "json", },
success: response => { data => {
const logs = response.map(log_record => { const logs = data.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.text(logs.join("\n")); packageInfoLogsInput.textContent = logs.join("\n");
packageInfoLogsInput.map((_, el) => highlight(el)); highlight(packageInfoLogsInput);
}, },
error: onFailure, onFailure,
}); );
} }
function loadPackage(packageBase, onFailure) { function loadPackage(packageBase, onFailure) {
@ -272,16 +276,17 @@
return ["bg-secondary", "text-white"]; return ["bg-secondary", "text-white"];
}; };
$.ajax({ makeRequest(
url: `/api/v1/packages/${packageBase}`, `/api/v1/packages/${packageBase}`,
data: { {
query: {
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
}, },
type: "GET", convert: response => response.json(),
dataType: "json", },
success: response => { data => {
const description = response.find(Boolean); const description = data.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(
@ -291,72 +296,71 @@
) )
).sort(); ).sort();
packageInfo.text(`${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`); packageInfo.textContent = `${description.package.base} ${description.status.status} at ${new Date(1000 * description.status.timestamp).toISOStringShort()}`;
packageInfoModalHeader.removeClass(); packageInfoModalHeader.classList.remove(...packageInfoModalHeader.classList);
packageInfoModalHeader.addClass("modal-header"); packageInfoModalHeader.classList.add("modal-header");
headerClass(description.status.status).forEach(clz => packageInfoModalHeader.addClass(clz)); headerClass(description.status.status).forEach(clz => packageInfoModalHeader.classList.add(clz));
packageInfoAurUrl.html(aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : ""); packageInfoAurUrl.innerHTML = aurUrl ? safeLink(aurUrl, aurUrl, "AUR link").outerHTML : "";
packageInfoDepends.html(listToTable( packageInfoDepends.innerHTML = 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.html(listToTable(extractListProperties(description.package, "groups"))); packageInfoGroups.innerHTML = listToTable(extractListProperties(description.package, "groups"));
packageInfoLicenses.html(listToTable(extractListProperties(description.package, "licenses"))); packageInfoLicenses.innerHTML = listToTable(extractListProperties(description.package, "licenses"));
packageInfoPackager.text(description.package.packager); packageInfoPackager.textContent = description.package.packager;
packageInfoPackages.html(listToTable(packages)); packageInfoPackages.innerHTML = listToTable(packages);
packageInfoUpstreamUrl.html(upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>")); packageInfoUpstreamUrl.innerHTML = upstreamUrls.map(url => safeLink(url, url, "upstream link").outerHTML).join("<br>");
packageInfoVersion.text(description.package.version); packageInfoVersion.textContent = description.package.version;
hideInfoControls(false);
}, },
error: (jqXHR, _, errorThrown) => { onFailure,
hideInfoControls(true); );
onFailure(jqXHR, null, errorThrown);
},
});
} }
function loadPatches(packageBase, onFailure) { function loadPatches(packageBase, onFailure) {
$.ajax({ makeRequest(
url: `/api/v1/packages/${packageBase}/patches`, `/api/v1/packages/${packageBase}/patches`,
type: "GET", {
dataType: "json", convert: response => response.json(),
success: response => {
packageInfoVariablesDiv.empty();
response.map(patch => insertVariable(packageBase, patch));
packageInfoVariablesBlock.attr("hidden", response.length === 0);
}, },
error: onFailure, data => {
}); packageInfoVariablesDiv.replaceChildren();
data.map(patch => insertVariable(packageBase, patch));
packageInfoVariablesBlock.hidden = !data.length;
},
onFailure,
);
} }
function packageInfoRemove() { function packageInfoRemove() {
const packageBase = packageInfoModal.data("package"); const packageBase = packageInfoModal.package;
if (packageBase) return packagesRemove([packageBase]); packagesRemove([packageBase]);
} }
function packageInfoUpdate() { function packageInfoUpdate() {
const packageBase = packageInfoModal.data("package"); const packageBase = packageInfoModal.package;
if (packageBase) return packagesAdd(packageBase, [], repository); packagesAdd(packageBase, [], repository);
} }
function showPackageInfo(packageBase) { function showPackageInfo(packageBase) {
const isPackageBaseSet = packageBase !== undefined; 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) { if (isPackageBaseSet) {
const message = error => `Could not load package ${packageBase} info: ${error}`; // set package base as currently used
showFailure("Load failure", message, jqXHR, errorThrown); packageInfoModal.package = packageBase;
} else {
// read package base from the current window attribute
packageBase = packageInfoModal.package;
}
const onFailure = error => {
if (isPackageBaseSet) {
const message = details => `Could not load package ${packageBase} info: ${details}`;
showFailure("Load failure", message, error);
} }
}; };
@ -366,10 +370,12 @@
loadChanges(packageBase, onFailure); loadChanges(packageBase, onFailure);
loadEvents(packageBase, onFailure); loadEvents(packageBase, onFailure);
if (isPackageBaseSet) packageInfoModal.modal("show"); if (isPackageBaseSet) {
bootstrap.Modal.getOrCreateInstance(packageInfoModal).show();
}
} }
$(_ => { ready(_ => {
packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, { packageInfoEventsUpdateChart = new Chart(packageInfoEventsUpdateChartCanvas, {
type: "line", type: "line",
data: {}, data: {},
@ -378,27 +384,23 @@
}, },
}); });
packageInfoModal.on("hidden.bs.modal", _ => { packageInfoModal.addEventListener("hidden.bs.modal", _ => {
packageInfoAurUrl.empty(); packageInfoAurUrl.textContent = "";
packageInfoDepends.empty(); packageInfoDepends.textContent = "";
packageInfoGroups.empty(); packageInfoGroups.textContent = "";
packageInfoLicenses.empty(); packageInfoLicenses.textContent = "";
packageInfoPackager.empty(); packageInfoPackager.textContent = "";
packageInfoPackages.empty(); packageInfoPackages.textContent = "";
packageInfoUpstreamUrl.empty(); packageInfoUpstreamUrl.textContent = "";
packageInfoVersion.empty(); packageInfoVersion.textContent = "";
packageInfoVariablesBlock.attr("hidden", true); packageInfoVariablesBlock.hidden = true;
packageInfoVariablesDiv.empty(); packageInfoVariablesDiv.replaceChildren();
packageInfoLogsInput.empty(); packageInfoLogsInput.textContent = "";
packageInfoChangesInput.empty(); packageInfoChangesInput.textContent = "";
packageInfoEventsTable.bootstrapTable("load", []); packageInfoEventsTable.bootstrapTable("load", []);
clearChart(); clearChart();
packageInfoModal.trigger("reset");
hideInfoControls(true);
}); });
}); });
</script> </script>

View File

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

View File

@ -1,39 +1,34 @@
<script> <script>
const keyImportButton = $("#key-import-button"); const packageRemoveButton = document.getElementById("package-remove-button");
const packageAddButton = $("#package-add-button"); const packageUpdateButton = document.getElementById("package-update-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;
const table = $("#packages"); // so far bootstrap-table only operates with jquery elements
const table = $(document.getElementById("packages"));
const statusBadge = $("#badge-status"); const statusBadge = document.getElementById("badge-status");
const versionBadge = $("#badge-version"); const versionBadge = document.getElementById("badge-version");
function doPackageAction(uri, packages, repository, successText, failureText, data) { function doPackageAction(uri, packages, repository, successText, failureText, data) {
const queryParams = $.param({ makeRequest(
uri,
{
method: "POST",
query: {
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
}); // it will never be empty btw },
json: Object.assign({}, {packages: packages}, data || {}),
$.ajax({ },
url: `${uri}?${queryParams}`, _ => {
data: JSON.stringify(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: (jqXHR, _, errorThrown) => { error => {
showFailure("Action failed", failureText, jqXHR, errorThrown); showFailure("Action failed", failureText, error);
}, },
}); );
} }
function filterListGroups() { function filterListGroups() {
@ -49,10 +44,10 @@
} }
function getRepositorySelector(selector) { function getRepositorySelector(selector) {
const selected = selector.find(":selected"); const selected = selector.options[selector.selectedIndex];
return { return {
architecture: selected.data("architecture"), architecture: selected.getAttribute("data-architecture"),
repository: selected.data("repository"), repository: selected.getAttribute("data-repository"),
}; };
} }
@ -60,14 +55,6 @@
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`;
@ -97,16 +84,17 @@
return "btn-outline-secondary"; return "btn-outline-secondary";
}; };
$.ajax({ makeRequest(
url: "/api/v1/packages", "/api/v1/packages",
data: { {
query: {
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
}, },
type: "GET", convert: response => response.json(),
dataType: "json", },
success: response => { data => {
const payload = response.map(description => { const payload = data.map(description => {
const package_base = description.package.base; const package_base = description.package.base;
const web_url = description.package.remote.web_url; const web_url = description.package.remote.web_url;
return { return {
@ -125,10 +113,9 @@
table.bootstrapTable("load", payload); table.bootstrapTable("load", payload);
table.bootstrapTable("uncheckAll"); table.bootstrapTable("uncheckAll");
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
hideControls(false);
}, },
error: (jqXHR, _, errorThrown) => { error => {
if ((jqXHR.status === 401) || (jqXHR.status === 403)) { if ((error.status === 401) || (error.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();
@ -136,39 +123,39 @@
table.bootstrapTable("hideLoading"); table.bootstrapTable("hideLoading");
} else { } else {
// other errors // other errors
const message = error => `Could not load list of packages: ${error}`; const message = details => `Could not load list of packages: ${details}`;
showFailure("Load failure", message, jqXHR, errorThrown); showFailure("Load failure", message, error);
} }
hideControls(true);
}, },
}); );
$.ajax({ makeRequest(
url: "/api/v1/status", "/api/v1/status",
data: { {
query: {
architecture: repository.architecture, architecture: repository.architecture,
repository: repository.repository, repository: repository.repository,
}, },
type: "GET", convert: response => response.json(),
dataType: "json",
success: response => {
versionBadge.html(`<i class="bi bi-github"></i> ahriman ${safe(response.version)}`);
statusBadge
.popover("dispose")
.attr("data-bs-content", `${response.status.status} at ${new Date(1000 * response.status.timestamp).toISOStringShort()}`)
.popover();
statusBadge.removeClass();
statusBadge.addClass("btn");
statusBadge.addClass(badgeClass(response.status.status));
}, },
}); data => {
versionBadge.innerHTML = `<i class="bi bi-github"></i> ahriman ${safe(data.version)}`;
statusBadge.classList.remove(...statusBadge.classList);
statusBadge.classList.add("btn");
statusBadge.classList.add(badgeClass(data.status.status));
const popover = bootstrap.Popover.getOrCreateInstance(statusBadge);
popover.dispose();
statusBadge.dataset.bsContent = `${data.status.status} at ${new Date(1000 * data.status.timestamp).toISOStringShort()}`;
bootstrap.Popover.getOrCreateInstance(statusBadge);
},
);
} }
function selectRepository() { function selectRepository() {
const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}"; const fragment = window.location.hash.replace("#", "") || "{{ repositories[0].id }}";
const element = $(`#${fragment}-link`); document.getElementById(`${fragment}-link`).click();
element.click();
} }
function statusFormat(value) { function statusFormat(value) {
@ -182,20 +169,25 @@
return {classes: cellClass(value)}; return {classes: cellClass(value)};
} }
$(_ => { ready(_ => {
$("#repositories a").on("click", event => { document.querySelectorAll("#repositories a").forEach(element => {
const element = event.target; element.onclick = _ => {
repository = { repository = {
architecture: element.dataset.architecture, architecture: element.dataset.architecture,
repository: element.dataset.repository, repository: element.dataset.repository,
}; };
packageUpdateButton.html(`<i class="bi bi-play"></i> update<span class="d-none d-sm-inline"> ${safe(repository.repository)} (${safe(repository.architecture)})</span>`); if (packageUpdateButton) {
$(`#${element.id}`).tab("show"); 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(); 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", _ => {
packageRemoveButton.prop("disabled", !table.bootstrapTable("getSelections").length); if (packageRemoveButton) {
packageRemoveButton.disabled = !table.bootstrapTable("getSelections").length;
}
}); });
table.on("click-row.bs.table", (self, data, row, cell) => { table.on("click-row.bs.table", (self, data, row, cell) => {
if (0 === cell || "base" === cell) { if (0 === cell || "base" === cell) {
@ -204,26 +196,38 @@
} else showPackageInfo(data.id); } else showPackageInfo(data.id);
}); });
table.on("created-controls.bs.table", _ => { table.on("created-controls.bs.table", _ => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp"); new easepick.create({
pickerInput.daterangepicker({ element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
autoUpdateInput: false, css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: { locale: {
cancelLabel: "Clear", cancel: "Clear",
},
RangePlugin: {
tooltip: false,
},
plugins: [
"RangePlugin",
],
setup: picker => {
picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
// replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch");
}
};
}, },
}); });
pickerInput.on("apply.daterangepicker", (event, picker) => {
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`);
table.bootstrapTable("triggerSearch");
}); });
pickerInput.on("cancel.daterangepicker", _ => { bootstrap.Popover.getOrCreateInstance(statusBadge);
pickerInput.val("");
table.bootstrapTable("triggerSearch");
});
});
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 = $("#packages"); const table = $(document.getElementById("packages"));
const pacmanConf = $("#pacman-conf"); const pacmanConf = document.getElementById("pacman-conf");
const pacmanConfCopyButton = $("#copy-btn"); const pacmanConfCopyButton = document.getElementById("copy-btn");
async function copyPacmanConf() { async function copyPacmanConf() {
const conf = pacmanConf.text(); const conf = pacmanConf.textContent;
await copyToClipboard(conf, pacmanConfCopyButton); await copyToClipboard(conf, pacmanConfCopyButton);
} }
@ -127,24 +127,36 @@ SigLevel = Database{% if has_repo_signed %}Required{% else %}Never{% endif %} Pa
return extractDataList(table.bootstrapTable("getData"), "licenses"); return extractDataList(table.bootstrapTable("getData"), "licenses");
} }
$(_ => { ready(_ => {
table.on("created-controls.bs.table", _ => { table.on("created-controls.bs.table", _ => {
const pickerInput = $(".bootstrap-table-filter-control-timestamp"); new easepick.create({
pickerInput.daterangepicker({ element: document.querySelector(".bootstrap-table-filter-control-timestamp"),
autoUpdateInput: false, css: [
"https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.css",
],
grid: 2,
calendars: 2,
autoApply: false,
locale: { locale: {
cancelLabel: "Clear", cancel: "Clear",
}, },
}); RangePlugin: {
tooltip: false,
pickerInput.on("apply.daterangepicker", (event, picker) => { },
pickerInput.val(`${picker.startDate.format("YYYY-MM-DD")} - ${picker.endDate.format("YYYY-MM-DD")}`); plugins: [
table.bootstrapTable("triggerSearch"); "RangePlugin",
}); ],
setup: picker => {
pickerInput.on("cancel.daterangepicker", _ => { picker.on("select", _ => { table.bootstrapTable("triggerSearch"); });
pickerInput.val(""); // replace "Cancel" behaviour to "Clear"
picker.onClickCancelButton = element => {
if (picker.isCancelButton(element)) {
picker.clear();
picker.hide();
table.bootstrapTable("triggerSearch"); table.bootstrapTable("triggerSearch");
}
};
},
}); });
}); });
}); });

View File

@ -1,41 +1,30 @@
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery.md5@1.0.2/index.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/js-md5@0.8.3/src/md5.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/tableexport.jquery.plugin@1.30.0/tableExport.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.2/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.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/bootstrap-table.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/export/bootstrap-table-export.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.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/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/resizable/bootstrap-table-resizable.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script> <script src="https://cdn.jsdelivr.net/npm/@easepick/bundle@1.2.1/dist/index.umd.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/highlight.min.js" crossorigin="anonymous" type="application/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" crossorigin="anonymous" type="application/javascript"></script> <script 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) {
if (navigator.clipboard === undefined) {
const input = document.createElement("textarea");
input.innerHTML = text;
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
} else {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
} button.innerHTML = "<i class=\"bi bi-clipboard-check\"></i> copied";
setTimeout(_ => {
button.html("<i class=\"bi bi-clipboard-check\"></i> copied"); button.innerHTML = "<i class=\"bi bi-clipboard\"></i> copy";
setTimeout(()=> {
button.html("<i class=\"bi bi-clipboard\"></i> copy");
}, 2000); }, 2000);
} }
@ -76,6 +65,47 @@
.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;")
@ -89,7 +119,9 @@
const element = document.createElement("a"); const element = document.createElement("a");
element.href = url; element.href = url;
element.innerText = text; element.innerText = text;
if (title) element.title = title; if (title) {
element.title = title;
}
return element; return element;
} }

View File

@ -1,17 +1,15 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/bootstrap-table.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/jquery-resizable-columns@0.2.3/dist/jquery.resizableColumns.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.22.1/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-table@1.23.2/dist/extensions/filter-control/bootstrap-table-filter-control.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.2/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.3/dist/cosmo/bootstrap.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/daterangepicker@3.1.0/daterangepicker.css" crossorigin="anonymous" type="text/css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.10.0/build/styles/github.min.css" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.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\-08\-23" "ahriman" "Generated Python Manual" .TH AHRIMAN "1" "2024\-09\-04" "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.0" __version__ = "2.14.1"

View File

@ -75,7 +75,9 @@ 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.with_stem(f"{args.lock.stem}_{repository_id.id}") self.path = args.lock
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,6 +27,7 @@ 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
@ -103,23 +104,26 @@ 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) -> None: def package_clear(self, package_base: str, repository_id: RepositoryId | None = None) -> 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) self.build_queue_clear(package_base, repository_id)
self.patches_remove(package_base, []) self.patches_remove(package_base, None)
self.logs_remove(package_base, None) self.logs_remove(package_base, None, repository_id)
self.changes_remove(package_base) self.changes_remove(package_base, repository_id)
self.dependencies_remove(package_base) self.dependencies_remove(package_base, repository_id)
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.database.package_clear(package_base, self.repository_id)
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,7 +145,6 @@ 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,9 +41,12 @@ 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:
return "" raise ValueError("Repository ID is called on empty repository identifier")
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,6 +14,7 @@ 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:
@ -30,6 +31,8 @@ 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,6 +4,7 @@ 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:
@ -35,7 +36,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, mocker: MockerFixture) -> None: def test_package_clear(database: SQLite, repository_id: RepositoryId, mocker: MockerFixture) -> None:
""" """
must clear package data must clear package data
""" """
@ -44,12 +45,14 @@ def test_package_clear(database: SQLite, mocker: MockerFixture) -> None:
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") database.package_clear("package", repository_id)
build_queue_mock.assert_called_once_with("package") build_queue_mock.assert_called_once_with("package", repository_id)
patches_mock.assert_called_once_with("package", []) patches_mock.assert_called_once_with("package", None)
logs_mock.assert_called_once_with("package", None) logs_mock.assert_called_once_with("package", None, repository_id)
changes_mock.assert_called_once_with("package") changes_mock.assert_called_once_with("package", repository_id)
dependencies_mock.assert_called_once_with("package") dependencies_mock.assert_called_once_with("package", repository_id)
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) package_mock.assert_called_once_with(package_ahriman.base, local_client.repository_id)
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,13 +101,11 @@ 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,10 +7,17 @@ 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("", "")) base.service(RepositoryId("repo", "arch"))
def test_service_package(base: BaseView, repository_id: RepositoryId, mocker: MockerFixture) -> None: def test_service_package(base: BaseView, repository_id: RepositoryId, mocker: MockerFixture) -> None: