diff --git a/package/share/ahriman/templates/build-status/package-info-modal.jinja2 b/package/share/ahriman/templates/build-status/package-info-modal.jinja2 index 4a968736..315cf05f 100644 --- a/package/share/ahriman/templates/build-status/package-info-modal.jinja2 +++ b/package/share/ahriman/templates/build-status/package-info-modal.jinja2 @@ -97,7 +97,7 @@ - + {% endif %} {% if autorefresh_intervals %} @@ -315,6 +315,69 @@ } function loadLogs(packageBase, onFailure) { + const sortFn = (left, right) => left.process_id.localeCompare(right.process_id) || left.version.localeCompare(right.version); + const compareFn = (left, right) => left.process_id === right.process_id && left.version === right.version; + + makeRequest( + `/api/v2/packages/${packageBase}/logs`, + { + query: { + architecture: repository.architecture, + head: true, + repository: repository.repository, + }, + convert: response => response.json(), + }, + data => { + const currentVersions = Array.from(packageInfoLogsVersions.children) + .map(el => { + return { + process_id: el.dataset.processId, + version: el.dataset.version, + }; + }) + .sort(sortFn); + const newVersions = data + .map(el => { + return { + process_id: el.process_id, + version: el.version, + }; + }) + .sort(sortFn); + + if (currentVersions.equals(newVersions, compareFn)) + loadLogsActive(packageBase); + else + loadLogsAll(packageBase, onFailure); + }, + ) + } + + function loadLogsActive(packageBase) { + const activeLogSelector = packageInfoLogsVersions.querySelector(".active"); + + if (activeLogSelector) { + makeRequest( + `/api/v2/packages/${packageBase}/logs`, + { + query: { + architecture: repository.architecture, + repository: repository.repository, + version: activeLogSelector.dataset.version, + process_id: activeLogSelector.dataset.processId, + }, + convert: response => response.json(), + }, + data => { + activeLogSelector.dataset.logs = convertLogs(data); + activeLogSelector.click(); + }, + ); + } + } + + function loadLogsAll(packageBase, onFailure) { makeRequest( `/api/v2/packages/${packageBase}/logs`, { @@ -440,29 +503,6 @@ packagesAdd(packageBase, [], repository, {refresh: packageInfoRefreshInput.checked}); } - function reloadActiveLogs(packageBase) { - const activeLogSelector = packageInfoLogsVersions.querySelector(".active"); - - if (activeLogSelector) { - makeRequest( - `/api/v2/packages/${packageBase}/logs`, - { - query: { - architecture: repository.architecture, - repository: repository.repository, - version: activeLogSelector.dataset.version, - process_id: activeLogSelector.dataset.processId, - }, - convert: response => response.json(), - }, - data => { - activeLogSelector.dataset.logs = convertLogs(data); - activeLogSelector.click(); - }, - ); - } - } - function showPackageInfo(packageBase) { const isPackageBaseSet = packageBase !== undefined; if (isPackageBaseSet) { @@ -502,7 +542,7 @@ const packageBase = packageInfoModal.dataset.package; // we only poll status and logs here loadPackage(packageBase); - reloadActiveLogs(packageBase); + loadLogs(packageBase); } }); } diff --git a/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 b/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 index ebc35469..2a47bdd3 100644 --- a/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 +++ b/package/share/ahriman/templates/utils/bootstrap-scripts.jinja2 @@ -218,6 +218,21 @@ }); } + Array.prototype.equals = function (right, comparator) { + let index = this.length; + if (index !== right.length) { + return false; + } + + while (index--) { + if (!comparator(this[index], right[index])) { + return false; + } + } + + return true; + } + Date.prototype.toISOStringShort = function () { const pad = number => String(number).padStart(2, "0"); return `${this.getFullYear()}-${pad(this.getMonth() + 1)}-${pad(this.getDate())} ${pad(this.getHours())}:${pad(this.getMinutes())}:${pad(this.getSeconds())}`; diff --git a/src/ahriman/web/schemas/logs_search_schema.py b/src/ahriman/web/schemas/logs_search_schema.py index 9b570d2e..4dc293a9 100644 --- a/src/ahriman/web/schemas/logs_search_schema.py +++ b/src/ahriman/web/schemas/logs_search_schema.py @@ -27,6 +27,9 @@ class LogsSearchSchema(PaginationSchema): request log search schema """ + head = fields.Boolean(metadata={ + "description": "Return versions only without fetching logs themselves", + }) version = fields.String(metadata={ "description": "Package version to search", "example": __version__, diff --git a/src/ahriman/web/views/v2/packages/logs.py b/src/ahriman/web/views/v2/packages/logs.py index 0454422d..fdb2ce43 100644 --- a/src/ahriman/web/views/v2/packages/logs.py +++ b/src/ahriman/web/views/v2/packages/logs.py @@ -17,7 +17,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . # +import itertools + from aiohttp.web import Response, json_response +from dataclasses import replace from typing import ClassVar from ahriman.models.user_access import UserAccess @@ -28,7 +31,8 @@ from ahriman.web.views.status_view_guard import StatusViewGuard class LogsView(StatusViewGuard, BaseView): - """ + """ else: + package logs web view Attributes: @@ -66,5 +70,14 @@ class LogsView(StatusViewGuard, BaseView): logs = self.service(package_base=package_base).package_logs_get(package_base, version, process, limit, offset) + head = self.request.query.get("head", "false") + # pylint: disable=protected-access + if self.configuration._convert_to_boolean(head): # type: ignore[attr-defined] + # logs should be sorted already + logs = [ + replace(next(log_records), message="") # remove messages + for _, log_records in itertools.groupby(logs, lambda log_record: log_record.log_record_id) + ] + response = [log_record.view() for log_record in logs] return json_response(response) diff --git a/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py b/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py index bf7a9905..c1b8b14c 100644 --- a/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py +++ b/tests/ahriman/web/views/v2/packages/test_view_v2_packages_logs.py @@ -139,3 +139,41 @@ async def test_get_not_found(client: TestClient, package_ahriman: Package) -> No response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs") assert response.status == 404 assert not response_schema.validate(await response.json()) + + +async def test_get_head(client: TestClient, package_ahriman: Package) -> None: + """ + must return only versions if head parameter is set + """ + await client.post(f"/api/v1/packages/{package_ahriman.base}", + json={"status": BuildStatusEnum.Success.value, "package": package_ahriman.view()}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 42.0, "message": "message 1", "version": "42"}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 43.0, "message": "message 2", "version": "42"}) + await client.post(f"/api/v1/packages/{package_ahriman.base}/logs", + json={"created": 44.0, "message": "message 3", "version": "43"}) + request_schema = pytest.helpers.schema_request(LogsView.get, location="querystring") + response_schema = pytest.helpers.schema_response(LogsView.get) + + payload = {"head": "true"} + assert not request_schema.validate(payload) + response = await client.get(f"/api/v2/packages/{package_ahriman.base}/logs", params=payload) + assert response.status == 200 + + logs = await response.json() + assert not response_schema.validate(logs) + assert logs == [ + { + "created": 42.0, + "message": "", + "version": "42", + "process_id": LogRecordId.DEFAULT_PROCESS_ID, + }, + { + "created": 44.0, + "message": "", + "version": "43", + "process_id": LogRecordId.DEFAULT_PROCESS_ID, + }, + ]