Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion assets/less/cds-rdm/administration/harvester-reports.less
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,29 @@
color: #999;
margin-top: 0.2em;
}
}
}

.harvester-run-success-message {
white-space: normal;
text-align: left;
}

// Job run report: wrap long lines; scroll region height (matches admin log-table intent)
.harvester-run-log-report {
width: 100%;

.harvester-run-log-table.log-table {
max-height: calc(100vh - 11rem);
overflow-x: hidden;
}

.harvester-run-log-segment {
overflow-x: hidden;
}

.log-line .log-message {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
}
}
34 changes: 31 additions & 3 deletions site/cds_rdm/administration/harvester_reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,40 @@
# under the terms of the GPL-2.0 License; see LICENSE file for more details.

"""Harvester Reports administration views."""

import json
from functools import partial

from flask import current_app
from flask import Blueprint, abort, current_app, render_template, request
from invenio_administration.views.base import AdminResourceListView
from invenio_i18n import lazy_gettext as _
from invenio_jobs.models import Job, Run
from invenio_search_ui.searchconfig import search_app_config

from cds_rdm.administration.permissions import curators_permission
from cds_rdm.harvester_runs.logs import HarvesterRunError, report_context


def create_harvester_report_blueprint(app):
"""Create the harvester run report UI blueprint."""
blueprint = Blueprint("cds_rdm_harvester_report_page", __name__)

def harvester_report_not_found(_error):
return render_template(current_app.config["THEME_404_TEMPLATE"]), 404

@blueprint.route("/administration/harvester-reports/<uuid:run_id>/report")
@curators_permission.require(http_exception=403)
def harvester_run_report(run_id):
try:
ctx = report_context(str(run_id))
except HarvesterRunError as error:
abort(error.code)
return render_template(
"cds_rdm/administration/harvester_run_report.html",
**ctx,
)

blueprint.register_error_handler(404, harvester_report_not_found)
return blueprint


class HarvesterReportsView(AdminResourceListView):
Expand Down Expand Up @@ -106,8 +129,13 @@ def get_context(self, **kwargs):
job_id = self._get_inspire_job_id()
if job_id:
runs = self._fetch_recent_runs(job_id, limit=20)
requested_run_id = request.args.get("run_id")
default_run = next(
(run for run in runs if run["id"] == requested_run_id),
runs[0] if runs else None,
)
context["harvester_runs"] = json.dumps(runs)
context["default_run"] = json.dumps(runs[0]) if runs else None
context["default_run"] = json.dumps(default_run) if default_run else None
else:
context["harvester_runs"] = json.dumps([])
context["default_run"] = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,43 @@
// under the terms of the GPL-2.0 License; see LICENSE file for more details.

import React from "react";
import { withState } from "react-searchkit";
import { Button, Icon } from "semantic-ui-react";
import { i18next } from "@translations/invenio_administration/i18next";
import { extractRunIdFromQuery } from "./utils";

const DownloadButtonComponent = ({ currentQueryState }) => {
const domContainer = document.getElementById("invenio-search-config");
const runs = JSON.parse(domContainer?.dataset.harvesterRuns || "[]");

const runId = extractRunIdFromQuery(
currentQueryState.queryString || "",
runs
);

const handleDownload = () => {
if (!runId) return;
const params = new URLSearchParams({ run_id: runId });
window.location.href = `/harvester-reports/download?${params.toString()}`;
};

return (
<Button
icon
labelPosition="left"
onClick={handleDownload}
disabled={!runId}
className="harvester-download-button"
size="small"
>
<Icon name="download" />
{i18next.t("Download")}
</Button>
);
};

export const DownloadButton = withState(DownloadButtonComponent);
export const DownloadButton = ({ runId }) => (
<div className="ui horizontal list">
<div className="item">
<Button
icon
labelPosition="left"
onClick={() => {
if (!runId) return;
const q = new URLSearchParams({ run_id: runId });
window.location.href = `/harvester-reports/download?${q}`;
}}
disabled={!runId}
className="harvester-download-log-button"
size="small"
>
<Icon name="download" />
{i18next.t("Download error logs")}
</Button>
</div>
<div className="item">
<Button
icon
labelPosition="left"
onClick={() => {
if (!runId) return;
window.location.assign(`/administration/harvester-reports/${runId}/report`);
}}
disabled={!runId}
className="harvester-view-logs-button"
size="small"
>
<Icon name="file alternate outline" />
{i18next.t("View error logs")}
</Button>
</div>
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,15 @@ const SearchBarComponent = ({ updateQueryState, currentQueryState }) => {

const { sortOptions, sortOrderDisabled } = useContext(SearchConfigurationContext);

// Derive selected run from the timestamp in the current query — null if user typed a custom range
const runIdFromQuery = extractRunIdFromQuery(currentQueryState.queryString, runs);
const selectedRun = runs.find((r) => r.id === runIdFromQuery) || null;
const [activeRunId, setActiveRunId] = React.useState(() => defaultRun?.id ?? null);

const [inputValue, setInputValue] = React.useState(currentQueryState.queryString || "");
const runIdFromQuery = extractRunIdFromQuery(currentQueryState.queryString, runs);
const effectiveRunId = runIdFromQuery || activeRunId;
const selectedRun = runs.find((r) => r.id === effectiveRunId) || null;

// Auto-select default run on mount only if there is no existing query
React.useEffect(() => {
if (!currentQueryState.queryString && defaultRun) {
executeSearch(defaultRun, "");
}
}, []);
const [inputValue, setInputValue] = React.useState(
currentQueryState.queryString || ""
);

const executeSearch = (run, userInput) => {
const timestampFilter = buildTimestampFilter(run);
Expand All @@ -57,9 +54,23 @@ const SearchBarComponent = ({ updateQueryState, currentQueryState }) => {
});
};

React.useEffect(() => {
const q = currentQueryState.queryString || "";
const fromQuery = extractRunIdFromQuery(q, runs);
if (fromQuery) {
setActiveRunId(fromQuery);
} else if (!q && defaultRun) {
setActiveRunId(defaultRun.id);
executeSearch(defaultRun, "");
}
}, []);
Comment thread
TahaKhan998 marked this conversation as resolved.

const onRunChange = (e, { value }) => {
setActiveRunId(value || null);
const run = runs.find((r) => r.id === value);
executeSearch(run, "");
if (run) {
executeSearch(run, "");
}
};

const onBtnSearchClick = () => {
Expand All @@ -68,6 +79,10 @@ const SearchBarComponent = ({ updateQueryState, currentQueryState }) => {
queryString: inputValue,
hiddenParams,
});
const id = extractRunIdFromQuery(inputValue, runs);
if (id) {
setActiveRunId(id);
}
};

const formatDate = (dateStr) => {
Expand Down Expand Up @@ -114,7 +129,7 @@ const SearchBarComponent = ({ updateQueryState, currentQueryState }) => {
selection
placeholder={i18next.t("Select a harvest run...")}
options={runOptions}
value={selectedRun?.id || ""}
value={activeRunId || ""}
onChange={onRunChange}
/>

Expand Down Expand Up @@ -162,7 +177,7 @@ const SearchBarComponent = ({ updateQueryState, currentQueryState }) => {
</Grid.Column>
</Grid.Row>
<Grid.Row>
<Grid.Column width={11}>
<Grid.Column width={9}>
<Header as="h4">{i18next.t("Search Logs")}</Header>
<Input
action={{
Expand All @@ -184,15 +199,15 @@ const SearchBarComponent = ({ updateQueryState, currentQueryState }) => {
}}
/>
</Grid.Column>
<Grid.Column width={3} verticalAlign="bottom">
<Grid.Column width={2} verticalAlign="bottom">
<Sort
sortOrderDisabled={sortOrderDisabled}
values={sortOptions}
ariaLabel={i18next.t("Sort")}
/>
</Grid.Column>
<Grid.Column width={2} verticalAlign="bottom">
<DownloadButton />
<Grid.Column width={5} verticalAlign="bottom" textAlign="right">
<DownloadButton runId={effectiveRunId} />
</Grid.Column>
</Grid.Row>
</Grid>
Expand Down
Loading
Loading