-
Notifications
You must be signed in to change notification settings - Fork 24
administration: adds harvester view #701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # | ||
| # Copyright (C) 2026 CERN. | ||
| # | ||
| # CDS-RDM is free software; you can redistribute it and/or modify it | ||
| # under the terms of the GPL-2.0 License; see LICENSE file for more details. | ||
|
|
||
| """Custom administration dashboard view.""" | ||
|
|
||
| from functools import wraps | ||
|
|
||
| from flask import abort | ||
| from flask_principal import Permission, RoleNeed | ||
| from invenio_administration.permissions import administration_permission | ||
| from invenio_administration.views.dashboard import ( | ||
| AdminDashboardView as BaseAdminDashboardView, | ||
| ) | ||
|
|
||
| from cds_rdm.permissions import can_access_administration_menu | ||
|
|
||
|
|
||
| def require_admin_or_harvester_curator(f): | ||
| """Decorator to check if user has admin or harvester-curator permission.""" | ||
| @wraps(f) | ||
| def decorated_view(*args, **kwargs): | ||
| # Check standard administration permission | ||
| if administration_permission.can(): | ||
| return f(*args, **kwargs) | ||
|
|
||
| # Also allow harvester-curator role | ||
| if Permission(RoleNeed("harvester-curator")).can(): | ||
| return f(*args, **kwargs) | ||
|
|
||
| # No permission, return 403 | ||
| abort(403) | ||
|
|
||
| return decorated_view | ||
|
|
||
|
|
||
| class CDSAdminDashboardView(BaseAdminDashboardView): | ||
| """Custom dashboard view accessible by admin and harvester-curator.""" | ||
|
|
||
| decorators = [require_admin_or_harvester_curator] | ||
|
|
||
| @staticmethod | ||
| def visible_when(): | ||
| """Return a callable to check if dashboard menu should be visible.""" | ||
| return lambda: can_access_administration_menu() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # | ||
| # Copyright (C) 2026 CERN. | ||
| # | ||
| # CDS-RDM is free software; you can redistribute it and/or modify it | ||
| # 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, request | ||
| from flask_principal import Permission, RoleNeed | ||
| 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 | ||
|
|
||
|
|
||
| class HarvesterReportsView(AdminResourceListView): | ||
| """Harvester reports admin view for curators.""" | ||
|
|
||
| api_endpoint = "/audit-logs/" | ||
| extension_name = "invenio-audit-logs" | ||
| name = "harvester-reports" | ||
| resource_config = "audit_log_resource" | ||
|
|
||
| title = "Harvester Reports" | ||
| menu_label = "Harvester Reports" | ||
| category = "Logs" | ||
| pid_path = "id" | ||
| icon = "file alternate" | ||
| template = "cds_rdm/administration/harvester_reports.html" | ||
| order = 2 | ||
| search_request_headers = {"Accept": "application/vnd.inveniordm.v1+json"} | ||
|
|
||
| display_search = True | ||
| display_delete = False | ||
| display_create = False | ||
| display_edit = False | ||
|
|
||
| item_field_list = { | ||
| "resource.type": {"text": _("Resource"), "order": 1, "width": 2}, | ||
| "resource.id": {"text": _("Resource ID"), "order": 2}, | ||
| "action": {"text": _("Action"), "order": 3}, | ||
| "user.id": {"text": _("User"), "order": 4}, | ||
| "created": {"text": _("Created"), "order": 5}, | ||
| } | ||
|
|
||
| actions = { | ||
| "view_log": {"text": _("View Log"), "payload_schema": None, "order": 1}, | ||
| "view_changes": { | ||
| "text": _("View Changes"), | ||
| "payload_schema": None, | ||
| "order": 2, | ||
| "show_for": ["record.publish"], | ||
| }, | ||
| } | ||
|
|
||
| search_config_name = "AUDIT_LOGS_SEARCH" | ||
| search_facets_config_name = "AUDIT_LOGS_FACETS" | ||
| search_sort_config_name = "AUDIT_LOGS_SORT_OPTIONS" | ||
|
|
||
| decorators = [ | ||
| Permission(RoleNeed("harvester-curator")).require(http_exception=403) | ||
| ] | ||
|
|
||
| @staticmethod | ||
| def disabled(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO No need to hide this behind a feature flag |
||
| """Disable the view on demand.""" | ||
| return not current_app.config.get("HARVESTER_REPORTS_ENABLED", True) | ||
|
|
||
| @staticmethod | ||
| def visible_when(): | ||
| """Return a callable to check if menu should be visible.""" | ||
| return lambda: Permission(RoleNeed("harvester-curator")).can() | ||
|
|
||
| def _get_inspire_job_id(self): | ||
| """Get the INSPIRE harvester job ID.""" | ||
| job = Job.query.filter_by(task="process_inspire").first() | ||
| return job.id if job else None | ||
|
|
||
| def _fetch_recent_runs(self, job_id, limit=20): | ||
| """Fetch recent parent runs for the INSPIRE job.""" | ||
| # Fetch only parent runs (parent_run_id is None) that have started | ||
| runs = ( | ||
| Run.query.filter_by(job_id=job_id, parent_run_id=None) | ||
| .filter(Run.started_at.isnot(None)) | ||
| .order_by(Run.started_at.desc()) | ||
| .limit(limit) | ||
| .all() | ||
| ) | ||
|
|
||
| # Serialize runs with all available info | ||
| return [ | ||
| { | ||
| "id": str(run.id), | ||
| "started_at": run.started_at.isoformat() if run.started_at else None, | ||
| "finished_at": run.finished_at.isoformat() if run.finished_at else None, | ||
| "status": run.status.value if run.status else None, | ||
| "title": run.title or f"Run {run.id}", | ||
| "message": run.message, | ||
| } | ||
| for run in runs | ||
| ] | ||
|
|
||
| def get_context(self, **kwargs): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was actually more than what I had in mind, nice UX addition, thanks! |
||
| """Add runs data to template context.""" | ||
| context = super().get_context(**kwargs) | ||
|
|
||
| # Get INSPIRE job and its runs | ||
| job_id = self._get_inspire_job_id() | ||
| if job_id: | ||
| runs = self._fetch_recent_runs(job_id, limit=20) | ||
| context["harvester_runs"] = json.dumps(runs) | ||
| context["default_run"] = json.dumps(runs[0]) if runs else None | ||
| else: | ||
| context["harvester_runs"] = json.dumps([]) | ||
| context["default_run"] = None | ||
|
|
||
| return context | ||
|
|
||
| def init_search_config(self, **kwargs): | ||
| """Build search view config.""" | ||
| return partial( | ||
| search_app_config, | ||
| config_name=self.get_search_app_name(**kwargs), | ||
| available_facets=current_app.config.get(self.search_facets_config_name), | ||
| sort_options=current_app.config[self.search_sort_config_name], | ||
| endpoint=self.get_api_endpoint(**kwargs), | ||
| headers=self.get_search_request_headers(**kwargs), | ||
| pagination_options=(20, 50), | ||
| default_size=20, | ||
| hidden_params=[["action", "record.publish"]], | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| // This file is part of CDS-RDM | ||
| // Copyright (C) 2026 CERN. | ||
| // | ||
| // CDS-RDM is free software; you can redistribute it and/or modify it | ||
| // 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"; | ||
|
|
||
| const DownloadButtonComponent = ({ currentQueryState }) => { | ||
| const handleDownload = () => { | ||
| const query = currentQueryState.queryString || ""; | ||
| const hiddenParams = currentQueryState.hiddenParams || []; | ||
|
|
||
| const params = new URLSearchParams(); | ||
| if (query) params.set("q", query); | ||
| hiddenParams.forEach(([key, value]) => params.append(key, value)); | ||
|
|
||
| const downloadUrl = `/harvester-reports/download?${params.toString()}`; | ||
| window.location.href = downloadUrl; | ||
| }; | ||
|
|
||
| return ( | ||
| <Button | ||
| icon | ||
| labelPosition="left" | ||
| onClick={handleDownload} | ||
| className="harvester-download-button" | ||
| size="small" | ||
| > | ||
| <Icon name="download" /> | ||
| {i18next.t("Download")} | ||
| </Button> | ||
| ); | ||
| }; | ||
|
|
||
| export const DownloadButton = withState(DownloadButtonComponent); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| // This file is part of CDS-RDM | ||
| // Copyright (C) 2026 CERN. | ||
| // | ||
| // CDS-RDM is free software; you can redistribute it and/or modify it | ||
| // under the terms of the GPL-2.0 License; see LICENSE file for more details. | ||
|
|
||
| import React from "react"; | ||
| import { Segment, Header, Icon } from "semantic-ui-react"; | ||
| import { i18next } from "@translations/invenio_administration/i18next"; | ||
|
|
||
| /** | ||
| * Custom Empty Results component (without showing the query) | ||
| */ | ||
| export const CustomEmptyResults = () => { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added this to hide the query from the user, the default one shows it and we are "manipulating" it ot add the timestamp
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a feeling this could have been solved by hidden_params in search view |
||
| return ( | ||
| <Segment placeholder textAlign="center" className="harvester-empty-results"> | ||
| <Header icon> | ||
| <Icon name="search" /> | ||
| {i18next.t("No logs found")} | ||
| </Header> | ||
| <Segment.Inline> | ||
| <p>{i18next.t("No logs match your current filters. Try selecting a different run or adjusting your search.")}</p> | ||
| </Segment.Inline> | ||
| </Segment> | ||
| ); | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see my comment in the invenio-administration PR