From 03de8c48cb374c3579c69fc2ff748b2e36e1fcbb Mon Sep 17 00:00:00 2001 From: Karolina Przerwa Date: Fri, 23 Aug 2024 17:49:59 +0200 Subject: [PATCH] administration: customize jobs views --- invenio_jobs/administration/jobs.py | 56 ++++++--- invenio_jobs/administration/runs.py | 10 ++ .../invenio_jobs/administration/JobActions.js | 54 ++++---- .../administration/JobDetailsView.js | 86 +++++++++++++ .../js/invenio_jobs/administration/JobRuns.js | 39 ------ .../administration/JobRunsHeader.js | 115 +++++++++++------- .../JobSearchResultItemLayout.js | 12 -- .../administration/RunActionForm.js | 79 ++++++++++++ .../RunsSearchResultItemLayout.js | 2 +- .../js/invenio_jobs/administration/index.js | 3 + .../system/jobs/jobs-details.html | 39 +++--- invenio_jobs/webpack.py | 3 +- 12 files changed, 336 insertions(+), 162 deletions(-) create mode 100644 invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobDetailsView.js delete mode 100644 invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRuns.js create mode 100644 invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunActionForm.js diff --git a/invenio_jobs/administration/jobs.py b/invenio_jobs/administration/jobs.py index f270658..1ffa1e2 100644 --- a/invenio_jobs/administration/jobs.py +++ b/invenio_jobs/administration/jobs.py @@ -17,6 +17,7 @@ from invenio_jobs.config import JOBS_QUEUES from invenio_jobs.models import Task +from invenio_jobs.services.schema import RunSchema from invenio_jobs.services.ui_schema import ScheduleUISchema @@ -36,6 +37,21 @@ class JobsAdminMixin: search_sort_config_name = "JOBS_SORT_OPTIONS" search_facets_config_name = "JOBS_FACETS" + actions = { + "schedule": { + "text": "Schedule", + "payload_schema": ScheduleUISchema, + "order": 1, + "icon": "calendar", + }, + "runs": { + "text": "Run now", + "payload_schema": RunSchema, + "order": 2, + "icon": "play", + }, + } + class JobsListView(JobsAdminMixin, AdminResourceListView): """Configuration for Jobs list view.""" @@ -56,13 +72,6 @@ class JobsListView(JobsAdminMixin, AdminResourceListView): "user": {"text": _("Started by"), "order": 4, "width": 3}, "next_run": {"text": _("Next run"), "order": 5, "width": 3}, } - actions = { - "schedule": { - "text": "Schedule", - "payload_schema": ScheduleUISchema, - "order": 1, - } - } @staticmethod def disabled(): @@ -73,12 +82,9 @@ def disabled(): class JobsDetailsView(JobsAdminMixin, AdminResourceListView): """Configuration for Jobs detail view which shows runs.""" - def get_api_endpoint(self, pid_value=None): - """overwrite get_api_endpoint to accept pid_value.""" - return f"/api/jobs/{pid_value}/runs" - url = "/jobs/" search_request_headers = {"Accept": "application/json"} + request_headers = {"Accept": "application/json"} name = "job-details" resource_config = "runs_resource" title = "Job Details" @@ -97,6 +103,27 @@ def get_api_endpoint(self, pid_value=None): "action": {"text": _("Action"), "order": 5, "width": 2}, } + def get_api_endpoint(self, pid_value=None): + """overwrite get_api_endpoint to accept pid_value.""" + return f"/api/jobs/{pid_value}/runs" + + def get_details_api_endpoint(self): + api_url_prefix = current_app.config["SITE_API_URL"] + slash_tpl = "/" if not self.api_endpoint.startswith("/") else "" + + if not self.api_endpoint.startswith(api_url_prefix): + return f"{api_url_prefix}{slash_tpl}{self.api_endpoint}" + + return f"{slash_tpl}{self.api_endpoint}" + + def get_context(self, **kwargs): + ctx = super().get_context(**kwargs) + ctx["request_headers"] = self.request_headers + ctx["ui_config"] = self.item_field_list + ctx["name"] = self.name + ctx["api_endpoint"] = self.get_details_api_endpoint() + return ctx + class JobsFormMixin: """Mixin class for form fields.""" @@ -108,7 +135,7 @@ def form_fields(self): {"title_l10n": str(queue["title"]), "id": queue["name"]} for queue in JOBS_QUEUES.values() ] - tasks = [{"title_l10n": name, "id": name} for name, t in Task.all().items()] + tasks = [{"title_l10n": t.title, "id": name} for name, t in Task.all().items()] return { "title": { "order": 1, @@ -138,11 +165,6 @@ def form_fields(self): "order": 5, "text": _("Active"), }, - "default_args": { - "order": 6, - "text": _("Default Arguments"), - "description": _("A task for the job run."), - }, "created": {"order": 7}, "updated": {"order": 8}, } diff --git a/invenio_jobs/administration/runs.py b/invenio_jobs/administration/runs.py index 5f1e79e..10499fd 100644 --- a/invenio_jobs/administration/runs.py +++ b/invenio_jobs/administration/runs.py @@ -27,3 +27,13 @@ class RunsListView(AdminResourceListView): display_delete = False display_edit = False display_create = False + actions = None + + # item_field_list = { + # "job": {"text": _("Jobs"), "order": 1, "width": 3}, + # "active": {"text": _("Status"), "order": 2, "width": 2}, + # "last_run_start_time": {"text": _("Last run"), "order": 3, "width": 3}, + # "user": {"text": _("Started by"), "order": 4, "width": 3}, + # "next_run": {"text": _("Next run"), "order": 5, "width": 3}, + # } + diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobActions.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobActions.js index aa639cc..8810c7f 100644 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobActions.js +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobActions.js @@ -17,9 +17,8 @@ export class JobActions extends Component { } onModalTriggerClick = (e, { payloadSchema, dataName, dataActionKey }) => { - const { resource } = this.props; const { modalOpen } = this.state; - + const { resource, actions: actionsConfig } = this.props; if (dataActionKey === "schedule") { this.setState({ modalOpen: true, @@ -46,6 +45,8 @@ export class JobActions extends Component { actionSuccessCallback={this.handleSuccess} actionCancelCallback={this.closeModal} resource={resource} + actionPayload={resource} + actionConfig={actionsConfig[dataActionKey]} /> ), }); @@ -61,50 +62,41 @@ export class JobActions extends Component { }; handleSuccess = () => { + const {resource} = this.props; this.setState({ modalOpen: false, modalHeader: undefined, modalBody: undefined, }); setTimeout(() => { - window.location.reload(); + window.location = resource.links.self_admin_html; }, 1000); }; render() { const { actions, Element, resource } = this.props; const { modalOpen, modalHeader, modalBody } = this.state; + return ( <> {Object.entries(actions).map(([actionKey, actionConfig]) => { - if (actionKey === "schedule") { - return ( - - - {actionConfig.text} - - ); - } else { - return ( - - {actionConfig.text} - - ); - } + const icon = actionConfig.icon; + const labelPos = icon ? "left" : null; + return ( + + {!_isEmpty(icon) && } + {actionConfig.text} + + ); })} {modalHeader && {modalHeader}} diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobDetailsView.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobDetailsView.js new file mode 100644 index 0000000..7f9dd3e --- /dev/null +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobDetailsView.js @@ -0,0 +1,86 @@ +// This file is part of Invenio +// Copyright (C) 2024 CERN. +// +// Invenio RDM is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import { + NotificationController, + initDefaultSearchComponents, + AdminDetailsView, +} from "@js/invenio_administration"; +import { createSearchAppInit } from "@js/invenio_search_ui"; +import { RunActionForm } from "./RunActionForm"; +import _get from "lodash/get"; +import React from "react"; +import ReactDOM from "react-dom"; +import { JobRunsHeader } from "./JobRunsHeader"; +import { JobSearchLayout } from "./JobSearchLayout"; +import { SearchResultItemLayout } from "./RunsSearchResultItemLayout"; +import { OverridableContext, overrideStore } from "react-overridable"; + +const overriddenComponents = overrideStore.getAll(); + +const domContainer = document.getElementById("invenio-search-config"); + +const defaultComponents = initDefaultSearchComponents(domContainer); + +const overridenComponents = { + ...defaultComponents, + "InvenioAdministration.SearchResultItem.layout": SearchResultItemLayout, + "SearchApp.layout": JobSearchLayout, +}; + +createSearchAppInit( + overridenComponents, + true, + "invenio-search-config", + false, + NotificationController +); + +const pidValue = domContainer.dataset.pidValue; + +const detailsConfig = document.getElementById("invenio-details-config"); + +const title = detailsConfig.dataset.title; +const fields = JSON.parse(detailsConfig.dataset.fields); +const resourceName = JSON.parse(detailsConfig.dataset.resourceName); +const displayEdit = JSON.parse(detailsConfig.dataset.displayEdit); +const displayDelete = JSON.parse(detailsConfig.dataset.displayDelete); +const actions = JSON.parse(detailsConfig.dataset.actions); +const apiEndpoint = _get(detailsConfig.dataset, "apiEndpoint"); +const idKeyPath = JSON.parse(_get(detailsConfig.dataset, "pidPath", "pid")); +const listUIEndpoint = detailsConfig.dataset.listEndpoint; +const resourceSchema = JSON.parse(detailsConfig.dataset?.resourceSchema); +const requestHeaders = JSON.parse(detailsConfig.dataset?.requestHeaders); +const uiSchema = JSON.parse(detailsConfig.dataset?.uiConfig); +const name = detailsConfig.dataset?.name; + +const cmps = { + ...overriddenComponents, + "InvenioAdministration.AdminDetailsView.job-details.layout": JobRunsHeader, + "InvenioAdministration.ActionForm.runs.layout": RunActionForm, +}; +detailsConfig && + ReactDOM.render( + + + , + detailsConfig + ); diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRuns.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRuns.js deleted file mode 100644 index 67145fa..0000000 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRuns.js +++ /dev/null @@ -1,39 +0,0 @@ -// This file is part of Invenio -// Copyright (C) 2024 CERN. -// -// Invenio RDM is free software; you can redistribute it and/or modify it -// under the terms of the MIT License; see LICENSE file for more details. - -import { - NotificationController, - initDefaultSearchComponents, -} from "@js/invenio_administration"; -import { createSearchAppInit } from "@js/invenio_search_ui"; -import React from "react"; -import ReactDOM from "react-dom"; -import { JobRunsHeaderComponent } from "./JobRunsHeader"; -import { JobSearchLayout } from "./JobSearchLayout"; -import { SearchResultItemLayout } from "./RunsSearchResultItemLayout"; - -const domContainer = document.getElementById("invenio-search-config"); - -const defaultComponents = initDefaultSearchComponents(domContainer); - -const overridenComponents = { - ...defaultComponents, - "InvenioAdministration.SearchResultItem.layout": SearchResultItemLayout, - "SearchApp.layout": JobSearchLayout, -}; - -createSearchAppInit( - overridenComponents, - true, - "invenio-search-config", - false, - NotificationController -); - -const pidValue = domContainer.dataset.pidValue; -const header = document.getElementById("header"); - -header && ReactDOM.render(, header); diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRunsHeader.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRunsHeader.js index 2b7ecf4..df68802 100644 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRunsHeader.js +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRunsHeader.js @@ -4,15 +4,22 @@ // Invenio RDM is free software; you can redistribute it and/or modify it // under the terms of the MIT License; see LICENSE file for more details. -import { NotificationContext } from "@js/invenio_administration"; +import { + NotificationContext, + Loader, + ErrorPage, + Actions +} from "@js/invenio_administration"; import { i18next } from "@translations/invenio_app_rdm/i18next"; +import _isEmpty from "lodash/isEmpty"; import PropTypes from "prop-types"; import React, { Component } from "react"; import { http } from "react-invenio-forms"; -import { RunButton } from "./RunButton"; +import { Divider, Button, Grid, Header } from "semantic-ui-react"; +import { AdminUIRoutes } from "@js/invenio_administration"; import { withCancel } from "react-invenio-forms"; -export class JobRunsHeaderComponent extends Component { +export class JobRunsHeader extends Component { constructor(props) { super(props); @@ -24,30 +31,6 @@ export class JobRunsHeaderComponent extends Component { }; } - componentDidMount() { - const { jobId } = this.props; - withCancel( - http - .get("/api/jobs/" + jobId) - .then((response) => response.data) - .then((data) => { - this.setState({ - loading: false, - ...(data.title && { title: data.title }), - ...(data.description && { description: data.description }), - ...(data.default_args && { config: data.default_args }), - ...(data.default_queue && { queue: data.default_queue }), - }); - }) - .catch((error) => { - this.onError(error); - this.setState({ - loading: false, - }); - }) - ); - } - static contextType = NotificationContext; onError = (e) => { @@ -61,29 +44,69 @@ export class JobRunsHeaderComponent extends Component { }; render() { - const { title, description, config, loading, queue } = this.state; - const { jobId } = this.props; + const { + pid, + columns, + actions, + apiEndpoint, + idKeyPath, + listUIEndpoint, + resourceSchema, + resourceName, + displayDelete, + displayEdit, + uiSchema, + data, + error, + loading, + } = this.props; return ( - <> -
-

{title}

-

{description}

-
-
- {loading ? null : ( - - )} -
- + + + + + +
{data?.title}
+ {data?.description} +
+ + + + + +
+
+ +
+
); } } -JobRunsHeaderComponent.propTypes = { +JobRunsHeader.propTypes = { jobId: PropTypes.string.isRequired, }; diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchResultItemLayout.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchResultItemLayout.js index 590a304..c8c5dcf 100644 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchResultItemLayout.js +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchResultItemLayout.js @@ -139,18 +139,6 @@ class SearchResultItemComponent extends Component { - { - this.setState({ - lastRunStatus: status, - lastRunCreatedTime: created, - }); - }} - /> { + const { index } = titleProps; + const { activeIndex } = this.state; + const newIndex = activeIndex === index ? -1 : index; + + this.setState({ activeIndex: newIndex }); + }; + + render() { + const { + actionSchema, + actionCancelCallback, + actionConfig, + actionKey, + loading, + formData, + error, + resource, + onSubmit, + } = this.props; + const jsonData = JSON.parse(resource.default_args); + const { activeIndex } = this.state; + return ( + <> + + + + + Check advanced arguments + + + + + + + + + ); + } +} + +RunActionForm.propTypes = { + resource: PropTypes.object.isRequired, + actionSchema: PropTypes.object.isRequired, + actionKey: PropTypes.string.isRequired, + actionSuccessCallback: PropTypes.func.isRequired, + actionCancelCallback: PropTypes.func.isRequired, + formFields: PropTypes.object, + actionConfig: PropTypes.object.isRequired, + actionPayload: PropTypes.object, + onSubmit: PropTypes.func.isRequired, +}; + +RunActionForm.defaultProps = { + formFields: {}, + actionPayload: {}, +}; diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunsSearchResultItemLayout.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunsSearchResultItemLayout.js index f55c783..1336e72 100644 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunsSearchResultItemLayout.js +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunsSearchResultItemLayout.js @@ -41,7 +41,7 @@ class SearchResultItemComponent extends Component { render() { const { result } = this.props; const { status } = this.state; - + debugger; // eslint-disable-line no-debugger return ( - +
{{ go_back() }} - {% block admin_page_content %} +
{%- block search_app %}
{{ title or name }} data-display-delete='{{ display_delete | tojson }}' data-resource-schema='{{ resource_schema | tojson }}' data-actions='{{ actions | tojson }}' - data-api-endpoint='{{ api_endpoint }}' + data-api-endpoint='{{ api_endpoint }}/{{ pid_value }}/runs' data-pid-path='{{ pid_path | tojson }}' data-pid-value='{{ pid_value }}' data-create-endpoint='{{ create_ui_endpoint }}' data-list-endpoint='{{ list_ui_endpoint }}' - > -
+ >
{%- endblock search_app %} {% endblock admin_page_content %} diff --git a/invenio_jobs/webpack.py b/invenio_jobs/webpack.py index 96ca4c6..7dafa87 100644 --- a/invenio_jobs/webpack.py +++ b/invenio_jobs/webpack.py @@ -17,11 +17,12 @@ "semantic-ui": dict( entry={ "invenio-jobs-search": "./js/invenio_jobs/administration/index.js", - "invenio-jobs-details": "./js/invenio_jobs/administration/JobRuns.js", + "invenio-jobs-details": "./js/invenio_jobs/administration/JobDetailsView.js", }, dependencies={ "react-invenio-forms": "^4.0.0", "react-searchkit": "^2.0.0", + "react-json-view": "^1.21.3", }, aliases={ "@less/invenio_jobs": "less/invenio_jobs",