Skip to content
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

Job registry #54

Merged
merged 12 commits into from
Sep 26, 2024
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 41 additions & 17 deletions invenio_jobs/administration/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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",
},
}

kpsherva marked this conversation as resolved.
Show resolved Hide resolved

class JobsListView(JobsAdminMixin, AdminResourceListView):
"""Configuration for Jobs list view."""
Expand All @@ -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():
Expand All @@ -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/<pid_value>"
search_request_headers = {"Accept": "application/json"}
request_headers = {"Accept": "application/json"}
name = "job-details"
resource_config = "runs_resource"
title = "Job Details"
Expand All @@ -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."""
Expand All @@ -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,
Expand Down Expand Up @@ -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."),
},
jrcastro2 marked this conversation as resolved.
Show resolved Hide resolved
"created": {"order": 7},
"updated": {"order": 8},
}
Expand Down
1 change: 1 addition & 0 deletions invenio_jobs/administration/runs.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ class RunsListView(AdminResourceListView):
display_delete = False
display_edit = False
display_create = False
actions = None
Original file line number Diff line number Diff line change
@@ -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():
kpsherva marked this conversation as resolved.
Show resolved Hide resolved
"""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 ###
17 changes: 17 additions & 0 deletions invenio_jobs/api.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand All @@ -46,6 +51,8 @@ export class JobActions extends Component {
actionSuccessCallback={this.handleSuccess}
actionCancelCallback={this.closeModal}
resource={resource}
actionPayload={resource}
actionConfig={actionsConfig[dataActionKey]}
/>
),
});
Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this timeout of 1 sec?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is when we create a new job run and we switch to detail view of this run (to monitor it).
@0einstein0 was the 1s chosen for any specific reason?

}, 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 (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
icon
labelPosition="left"
>
<Icon name="calendar" />
{actionConfig.text}
</Element>
);
} else {
return (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
>
{actionConfig.text}
</Element>
);
}
const icon = actionConfig.icon;
const labelPos = icon ? "left" : null;
return (
<Element
key={actionKey}
onClick={this.onModalTriggerClick}
payloadSchema={actionConfig.payload_schema}
dataName={actionConfig.text}
dataActionKey={actionKey}
basic
icon={!_isEmpty(icon)}
labelPosition={labelPos}
>
{!_isEmpty(icon) && <Icon name={icon} />}
{actionConfig.text}
</Element>
);
})}
<ActionModal modalOpen={modalOpen} resource={resource}>
{modalHeader && <Modal.Header>{modalHeader}</Modal.Header>}
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
<OverridableContext.Provider value={cmps}>
<AdminDetailsView
title={title}
actions={actions}
apiEndpoint={apiEndpoint}
columns={fields}
pid={pidValue}
displayEdit={displayEdit}
displayDelete={displayDelete}
idKeyPath={idKeyPath}
resourceName={resourceName}
listUIEndpoint={listUIEndpoint}
resourceSchema={resourceSchema}
requestHeaders={requestHeaders}
uiSchema={uiSchema}
name={name}
/>
</OverridableContext.Provider>,
detailsConfig
);
Loading