diff --git a/MANIFEST.in b/MANIFEST.in index b443b5f..b074b38 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -24,3 +24,5 @@ recursive-include invenio_jobs *.js recursive-include invenio_jobs/translations *.po *.pot *.mo recursive-include tests *.py recursive-include invenio_jobs *.py +recursive-include invenio_jobs *.json +recursive-include invenio_jobs *.pot diff --git a/invenio_jobs/administration/jobs.py b/invenio_jobs/administration/jobs.py index f270658..0e391e3 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,29 @@ 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): + """Compute api endpoint link for job details view.""" + 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): + """Compute admin view context.""" + 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 +137,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 +167,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..4379252 100644 --- a/invenio_jobs/administration/runs.py +++ b/invenio_jobs/administration/runs.py @@ -27,3 +27,4 @@ class RunsListView(AdminResourceListView): display_delete = False display_edit = False display_create = False + actions = None diff --git a/invenio_jobs/alembic/1f896f6990b8_update_jobs_module_table_names.py b/invenio_jobs/alembic/1f896f6990b8_update_jobs_module_table_names.py new file mode 100644 index 0000000..3f3d04e --- /dev/null +++ b/invenio_jobs/alembic/1f896f6990b8_update_jobs_module_table_names.py @@ -0,0 +1,59 @@ +# +# This file is part of Invenio. +# Copyright (C) 2016-2018 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. + +"""Update jobs module table names.""" + +import sqlalchemy as sa +import sqlalchemy_utils +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "1f896f6990b8" +down_revision = "356496a01197" +branch_labels = () +depends_on = None + + +def upgrade(): + """Upgrade database.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("fk_run_job_id_job", "run", type_="foreignkey") + op.rename_table("job", "jobs_job") + op.rename_table("run", "jobs_run") + + op.create_foreign_key( + "fk_jobs_run_job_id_jobs_job", "jobs_run", "jobs_job", ["job_id"], ["id"] + ) + op.drop_column("jobs_job", "default_args") + + # ### end Alembic commands ### + + +def downgrade(): + """Downgrade database.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint("fk_jobs_run_job_id_jobs_job", "jobs_run", type_="foreignkey") + + op.rename_table("jobs_job", "job") + op.rename_table("jobs_run", "run") + op.create_foreign_key("fk_run_job_id_job", "run", "job", ["job_id"], ["id"]) + op.add_column( + "job", + sa.Column( + "default_args", + sa.JSON() + .with_variant(sqlalchemy_utils.types.json.JSONType(), "mysql") + .with_variant( + postgresql.JSONB(none_as_null=True, astext_type=sa.Text()), "postgresql" + ) + .with_variant(sqlalchemy_utils.types.json.JSONType(), "sqlite"), + nullable=True, + ), + ) + + # ### end Alembic commands ### diff --git a/invenio_jobs/api.py b/invenio_jobs/api.py new file mode 100644 index 0000000..0df85f4 --- /dev/null +++ b/invenio_jobs/api.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-Jobs is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Record class mock.""" + + +class AttrDict(dict): + """Mock record class.""" + + def __init__(self, *args, **kwargs): + """Constructor.""" + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self 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..2b7baf0 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 @@ -1,9 +1,15 @@ +// 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 React, { Component } from "react"; import PropTypes from "prop-types"; import { Button, Modal, Icon } from "semantic-ui-react"; import { ActionModal, ActionForm } from "@js/invenio_administration"; import _isEmpty from "lodash/isEmpty"; -import { i18next } from "@translations/invenio_app_rdm/i18next"; +import { i18next } from "@translations/invenio_jobs/i18next"; import ScheduleJobModal from "./ScheduleJobModal"; export class JobActions extends Component { @@ -17,9 +23,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 +51,8 @@ export class JobActions extends Component { actionSuccessCallback={this.handleSuccess} actionCancelCallback={this.closeModal} resource={resource} + actionPayload={resource} + actionConfig={actionsConfig[dataActionKey]} /> ), }); @@ -61,50 +68,41 @@ export class JobActions extends Component { }; handleSuccess = () => { + const { resource } = this.props; this.setState({ modalOpen: false, modalHeader: undefined, modalBody: undefined, }); setTimeout(() => { - window.location.reload(); - }, 1000); + window.location = resource.links.self_admin_html; + }, 1500); }; 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..3e8d512 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,20 @@ // 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 { + NotificationContext, + Loader, + ErrorPage, + Actions +} from "@js/invenio_administration"; +import { i18next } from "@translations/invenio_jobs/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 { withCancel } from "react-invenio-forms"; +import { Divider, Button, Grid, Header } from "semantic-ui-react"; +import { AdminUIRoutes } from "@js/invenio_administration"; -export class JobRunsHeaderComponent extends Component { +export class JobRunsHeader extends Component { constructor(props) { super(props); @@ -24,30 +29,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) => { @@ -60,30 +41,74 @@ export class JobRunsHeaderComponent extends Component { console.error(e); }; + handleSuccess = () => { + const { data } = this.props; + setTimeout(() => { + window.location = data.links.self_admin_html; + }, 1500); + }; + + render() { - const { title, description, config, loading, queue } = this.state; - const { jobId } = this.props; + const { + actions, + apiEndpoint, + idKeyPath, + listUIEndpoint, + resourceName, + displayDelete, + displayEdit, + 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..2894f97 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 @@ -7,14 +7,13 @@ */ import { BoolFormatter, NotificationContext } from "@js/invenio_administration"; -import { i18next } from "@translations/invenio_app_rdm/i18next"; +import { i18next } from "@translations/invenio_jobs/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, Button } from "semantic-ui-react"; import { Actions } from "@js/invenio_administration"; -import { RunButton } from "./RunButton"; import { StatusFormatter } from "./StatusFormatter"; import { AdminUIRoutes } from "@js/invenio_administration/src/routes"; @@ -139,18 +138,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, + loading, + formData, + error, + resource, + onSubmit, + } = this.props; + const jsonData = JSON.parse(resource.default_args); + const { activeIndex } = this.state; + return ( + <> + + {(props) => ( + <> + + + + + + + + + {i18next.t("Advanced configuration")} + + +
+ {i18next.t("Reference configuration of this job:")} +
+ +
+ + + + + "Modifying the Custom arguments field will + replace any arguments specified above and run the task + with custom configuration. + + +