diff --git a/invenio_jobs/administration/jobs.py b/invenio_jobs/administration/jobs.py index 4f33a6c..3ffd01b 100644 --- a/invenio_jobs/administration/jobs.py +++ b/invenio_jobs/administration/jobs.py @@ -8,15 +8,8 @@ """Invenio administration view module.""" -from functools import partial - -from flask import current_app -from invenio_administration.views.base import ( - AdminResourceDetailView, - AdminResourceListView, -) +from invenio_administration.views.base import AdminResourceListView from invenio_i18n import lazy_gettext as _ -from invenio_search_ui.searchconfig import search_app_config class JobsListView(AdminResourceListView): @@ -41,56 +34,48 @@ class JobsListView(AdminResourceListView): item_field_list = { "job": {"text": _("Jobs"), "order": 1, "width": 3}, "last_run_start_time": {"text": _("Last run"), "order": 2, "width": 3}, - "last_run_status": {"text": _("Status"), "order": 3, "width": 1}, - "user": {"text": _("Started by"), "order": 4, "width": 3}, - "next_run": {"text": _("Next run"), "order": 5, "width": 3}, + "user": {"text": _("Started by"), "order": 3, "width": 3}, + "next_run": {"text": _("Next run"), "order": 4, "width": 3}, + "action": {"text": _("Action"), "order": 5, "width": 2}, } search_config_name = "JOBS_SEARCH" search_sort_config_name = "JOBS_SORT_OPTIONS" search_facets_config_name = "JOBS_FACETS" - actions = { - "settings": { - "text": "Settings", - "payload_schema": None, - "order": 1, - "icon": "star", - }, - "schedule": { - "text": "Schedule", - "payload_schema": None, - "order": 2, - }, - "run": { - "text": "Run Now", - "payload_schema": None, - "order": 2, - }, - } +class JobsDetailsView(AdminResourceListView): + """Configuration for Jobs detail view which shows runs.""" -class JobsDetailView(AdminResourceDetailView): - """Configuration for Jobs detail view.""" + 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/" - api_endpoint = "/jobs" search_request_headers = {"Accept": "application/json"} - name = "Job Details" - resource_config = "jobs_resource" + name = "job-details" + resource_config = "runs_resource" title = "Job Details" + disabled = lambda _: True - template = "invenio_administration/details.html" + template = "invenio_jobs/system/jobs/jobs-details.html" display_delete = False display_edit = False display_search = False + display_create = False list_view_name = "jobs" pid_path = "id" + pid_value = "" item_field_list = { - "run": {"text": _("Runs"), "order": 1}, - "duration": {"text": _("Duration"), "order": 2}, - "message": {"text": _("Message"), "order": 3}, - "user": {"text": _("Started by"), "order": 4}, + "run": {"text": _("Run"), "order": 1, "width": 2}, + "duration": {"text": _("Duration"), "order": 2, "width": 2}, + "message": {"text": _("Message"), "order": 3, "width": 10}, + "user": {"text": _("Started by"), "order": 4, "width": 2}, + "action": {"text": _("Action"), "order": 5, "width": 2}, } + + search_config_name = "JOBS_SEARCH" + search_sort_config_name = "JOBS_SORT_OPTIONS" + search_facets_config_name = "JOBS_FACETS" 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 new file mode 100644 index 0000000..67145fa --- /dev/null +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRuns.js @@ -0,0 +1,39 @@ +// 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 new file mode 100644 index 0000000..a6154bb --- /dev/null +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobRunsHeader.js @@ -0,0 +1,83 @@ +// 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 { NotificationContext } from "@js/invenio_administration"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { http } from "react-invenio-forms"; +import { RunButton } from "./RunButton"; +import { withCancel } from "react-invenio-forms"; + +export class JobRunsHeaderComponent extends Component { + constructor(props) { + super(props); + + this.state = { + title: i18next.t("Job Details"), + description: "", + config: {}, + loading: true, + }; + } + + 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 }), + }); + }) + .catch((error) => { + this.onError(error); + this.setState({ + loading: false, + }); + }) + ); + } + + static contextType = NotificationContext; + + onError = (e) => { + const { addNotification } = this.context; + addNotification({ + title: i18next.t("Status ") + e.status, + content: `${e.message}`, + type: "error", + }); + console.error(e); + }; + + render() { + const { title, description, config, loading } = this.state; + const { jobId } = this.props; + return ( + <> +
+

{title}

+

{description}

+
+
+ {loading ? null : ( + + )} +
+ + ); + } +} + +JobRunsHeaderComponent.propTypes = { + jobId: PropTypes.string.isRequired, +}; diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/JobSearchLayout.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchLayout.js similarity index 100% rename from invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/JobSearchLayout.js rename to invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchLayout.js 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 new file mode 100644 index 0000000..4992e84 --- /dev/null +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/JobSearchResultItemLayout.js @@ -0,0 +1,139 @@ +/* + * This file is part of Invenio. + * Copyright (C) 2024 CERN. + * + * Invenio 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 { BoolFormatter, NotificationContext } from "@js/invenio_administration"; +import { i18next } from "@translations/invenio_app_rdm/i18next"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { UserListItemCompact, toRelativeTime } from "react-invenio-forms"; +import { withState } from "react-searchkit"; +import { Popup, Table } from "semantic-ui-react"; +import { RunButton } from "./RunButton"; +import { StatusFormatter } from "./StatusFormatter"; + +class SearchResultItemComponent extends Component { + constructor(props) { + super(props); + + this.state = { + lastRunStatus: props.result?.last_run?.status, + lastRunCreatedTime: props.result?.last_run?.created, + }; + } + static contextType = NotificationContext; + + onError = (e) => { + const { addNotification } = this.context; + addNotification({ + title: i18next.t("Status ") + e.status, + content: `${e.message}`, + type: "error", + }); + console.error(e); + }; + + render() { + const { result } = this.props; + const { lastRunStatus, lastRunCreatedTime } = this.state; + + return ( + + + {result.title} +   + + + + {lastRunStatus ? ( + <> + + + {toRelativeTime(lastRunCreatedTime, i18next.language)} + + } + /> + + ) : ( + "−" + )} + + {result?.last_run?.started_by ? ( + + + + ) : ( + + System + + )} + + {result.active === false + ? "Inactive" + : toRelativeTime(result.next_run, i18next.language) ?? "−"} + + + { + this.setState({ + lastRunStatus: status, + lastRunCreatedTime: created, + }); + }} + /> + + + ); + } +} + +SearchResultItemComponent.propTypes = { + result: PropTypes.object.isRequired, +}; + +SearchResultItemComponent.defaultProps = {}; + +export const SearchResultItemLayout = withState(SearchResultItemComponent); diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunButton.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunButton.js new file mode 100644 index 0000000..a0383c8 --- /dev/null +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/RunButton.js @@ -0,0 +1,141 @@ +// This file is part of InvenioRDM +// Copyright (C) 2024 CERN +// +// Invenio RDM Records 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 { i18next } from "@translations/invenio_app_rdm/i18next"; +import PropTypes from "prop-types"; +import React, { Component } from "react"; +import { http, withCancel } from "react-invenio-forms"; +import { + Button, + Dropdown, + DropdownMenu, + Form, + FormField, + FormInput, + TextArea, +} from "semantic-ui-react"; + +export class RunButton extends Component { + constructor(props) { + super(props); + + this.state = { + title: "Manual run", + config: JSON.stringify(props.config, null, "\t"), + queue: "low", + loading: false, + }; + } + + componentWillUnmount() { + this.cancellableAction && this.cancellableAction.cancel(); + } + + handleTitleChange = (e, { name, value }) => this.setState({ title: value }); + handleConfigChange = (e, { name, value }) => this.setState({ config: value }); + handleQueueChange = (e, { name, value }) => this.setState({ queue: value }); + + handleSubmit = async () => { + this.setState({ loading: true }); + + const { jobId, onError, setRun } = this.props; + const { title, config, queue } = this.state; + + try { + var userConfigJSON = JSON.parse(config); + } catch (e) { + onError(e); + } + + const runData = { + title: title, + args: userConfigJSON, + queue: queue, + }; + + try { + this.cancellableAction = await withCancel( + http.post("/api/jobs/" + jobId + "/runs", runData) + ); + } catch (error) { + if (error.response) { + onError(error.response.data); + } else { + onError(error); + } + } + const response = await this.cancellableAction.promise; + setRun(response.data?.status, response.data?.created); + this.setState({ loading: false }); + }; + + render() { + const { title, config, queue, loading } = this.state; + const lines = config.split(/\r?\n/).length; + + return ( + + +
+ e.stopPropagation()} + onChange={this.handleTitleChange} + /> + e.stopPropagation()} + onChange={this.handleConfigChange} + /> + + + ); +}; + +StopButton.propTypes = { + stopURL: PropTypes.string.isRequired, + setStatus: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, +}; diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/details/index.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/details/index.js deleted file mode 100644 index db2c162..0000000 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/details/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// This file is part of InvenioCommunities -// 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. diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/index.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/index.js index 274e8db..0caa63e 100644 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/index.js +++ b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/index.js @@ -1,4 +1,4 @@ -// This file is part of InvenioCommunities +// This file is part of Invenio // Copyright (C) 2024 CERN. // // Invenio RDM is free software; you can redistribute it and/or modify it @@ -7,7 +7,8 @@ import { initDefaultSearchComponents } from "@js/invenio_administration"; import { createSearchAppInit } from "@js/invenio_search_ui"; import { NotificationController } from "@js/invenio_administration"; -import { SearchResultItemLayout, JobSearchLayout } from "./search"; +import { SearchResultItemLayout } from "./JobSearchResultItemLayout"; +import { JobSearchLayout } from "./JobSearchLayout"; const domContainer = document.getElementById("invenio-search-config"); diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/SearchResultItemLayout.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/SearchResultItemLayout.js deleted file mode 100644 index cada955..0000000 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/SearchResultItemLayout.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This file is part of Invenio. - * Copyright (C) 2022-2024 CERN. - * - * Invenio 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 { BoolFormatter } from "@js/invenio_administration"; -import { SystemJobActions } from "./SystemJobActions"; -import PropTypes from "prop-types"; -import React, { Component } from "react"; -import { Table } from "semantic-ui-react"; -import { withState } from "react-searchkit"; -import { i18next } from "@translations/invenio_app_rdm/i18next"; -import { UserListItemCompact } from "react-invenio-forms"; - -class SearchResultItemComponent extends Component { - render() { - const { result } = this.props; - - return ( - - - {result.name} - - - {result.last_run_start_time} - - - - - - - - - - {result.next_run} - - - - - - ); - } -} - -SearchResultItemComponent.propTypes = { - result: PropTypes.object.isRequired, -}; - -SearchResultItemComponent.defaultProps = {}; - -export const SearchResultItemLayout = withState(SearchResultItemComponent); diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/SystemJobActions.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/SystemJobActions.js deleted file mode 100644 index f32a195..0000000 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/SystemJobActions.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * This file is part of Invenio. - * Copyright (C) 2024 CERN. - * - * Invenio 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 React, { Component } from "react"; -import { Button, Icon } from "semantic-ui-react"; -import { i18next } from "@translations/invenio_app_rdm/i18next"; - -export class SystemJobActions extends Component { - handleAction = async (action) => { - const actionConfig = { - restore: { - label: i18next.t("Settings"), - icon: "cogwheel", - notificationTitle: i18next.t("Settings"), - }, - block: { - label: i18next.t("Schedule"), - icon: "calendar", - notificationTitle: i18next.t("Schedule"), - }, - deactivate: { - label: i18next.t("Run now"), - icon: "pause", - notificationTitle: i18next.t("Run now"), - }, - }[action]; - - return actionConfig; - }; - - render() { - const actionItems = [ - { key: "settings", label: "Settings", icon: "cog" }, - { key: "schedule", label: "Schedule", icon: "calendar" }, - { key: "run", label: "Run now", icon: "play" }, - ]; - - const generateActions = () => { - return ( - <> - {actionItems.map((actionItem) => ( - - ))} - - ); - }; - return ( -
- - {generateActions()} - -
- ); - } -} - -SystemJobActions.propTypes = {}; - -SystemJobActions.defaultProps = {}; diff --git a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/index.js b/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/index.js deleted file mode 100644 index f0d09f7..0000000 --- a/invenio_jobs/assets/semantic-ui/js/invenio_jobs/administration/search/index.js +++ /dev/null @@ -1,10 +0,0 @@ -/* - * This file is part of Invenio. - * Copyright (C) 2024 CERN. - * - * Invenio is free software; you can redistribute it and/or modify it - * under the terms of the MIT License; see LICENSE file for more details. - */ - -export { SearchResultItemLayout } from "./SearchResultItemLayout"; -export { JobSearchLayout } from "./JobSearchLayout.js"; diff --git a/invenio_jobs/resources/config.py b/invenio_jobs/resources/config.py index 12b86e7..55e2666 100644 --- a/invenio_jobs/resources/config.py +++ b/invenio_jobs/resources/config.py @@ -94,6 +94,12 @@ class RunsSearchRequestArgsSchema(SearchRequestArgsSchema): status = ma.fields.Enum(RunStatusEnum) +class JobsSearchRequestArgsSchema(SearchRequestArgsSchema): + """Jobs search request parameters.""" + + status = ma.fields.Boolean() + + class RunsResourceConfig(ResourceConfig, ConfiguratorMixin): """Runs resource config.""" diff --git a/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-details.html b/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-details.html index e528bb8..5d36538 100644 --- a/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-details.html +++ b/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-details.html @@ -1,12 +1,57 @@ {# - Copyright (C) 2024 CERN. - - Invenio App 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. - #} - {% extends "invenio_administration/details.html" %} - - {% block javascript %} - {{ super() }} - {{ webpack['invenio-jobs-details.js'] }} - {% endblock %} + Copyright (C) 2024 CERN. + + Invenio App 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. +#} + +{%- from "invenio_administration/macros.html" import go_back %} + +{% extends "invenio_administration/search.html" %} + +{% block admin_main_column %} +
+ +
+ {{ go_back() }} + + {% block admin_page_content %} + + + {%- block search_app %} +
+
+ {%- endblock search_app %} + {% endblock admin_page_content %} +
+
+{% endblock %} + +{% block javascript %} + {{ super() }} + {{ webpack['invenio-jobs-details.js'] }} +{% endblock %} diff --git a/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-search.html b/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-search.html index 271652f..d0dee4d 100644 --- a/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-search.html +++ b/invenio_jobs/templates/semantic-ui/invenio_jobs/system/jobs/jobs-search.html @@ -1,12 +1,12 @@ {# - Copyright (C) 2024 CERN. + Copyright (C) 2024 CERN. - Invenio App 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. - #} - {% extends "invenio_administration/search.html" %} + Invenio App 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. +#} +{% extends "invenio_administration/search.html" %} - {% block javascript %} +{% block javascript %} {{ super() }} {{ webpack['invenio-jobs-search.js'] }} {% endblock %} diff --git a/invenio_jobs/webpack.py b/invenio_jobs/webpack.py index 389288c..3d9dda6 100644 --- a/invenio_jobs/webpack.py +++ b/invenio_jobs/webpack.py @@ -17,7 +17,7 @@ "semantic-ui": dict( entry={ "invenio-jobs-search": "./js/invenio_jobs/administration/index.js", - "invenio-jobs-details": "./js/invenio_jobs/administration/details/index.js", + "invenio-jobs-details": "./js/invenio_jobs/administration/JobRuns.js", }, dependencies={ "react-invenio-forms": "^3.0.0", diff --git a/setup.cfg b/setup.cfg index c148ad4..0cef36b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,7 +60,7 @@ invenio_assets.webpack = jobs = invenio_jobs.webpack:administration invenio_administration.views = jobs_list = invenio_jobs.administration.jobs:JobsListView - jobs_details = invenio_jobs.administration.jobs:JobsDetailView + jobs_details = invenio_jobs.administration.jobs:JobsDetailsView invenio_base.api_apps = jobs = invenio_jobs:InvenioJobs invenio_base.api_blueprints =