Skip to content

Commit

Permalink
Add Job details view (#14)
Browse files Browse the repository at this point in the history
* refactor: move search index

* UI: add MVP for job details view

* UI: Set title to name of job and  move status

* fix: adapt to actual API

* UX: Add run button dropdown

* UI: remove settings button

* bug: fix getting user from API and pid_value encoding

* UI: Fix details page and user display

* UI: add pop up showing timestamp

* UI: add description

* refactor: move classes into template

* UI: move active status and remove schedule button

* UX: add interactive run button to details page

* refactor: move status formatter to own component

* api: change RunStatusEnum states

* JS: add run button

* JS: Add stop button

* UX: add error message for actions

* refactor: turn JobRunsHeader into a component

* refactor: move JS files

* UX: have stop button alter status state

* UI: Refactor RunButton into component

Plus changes requested by manuel

* UI: have run button update jobs list status

* JS: use cancellable promises
  • Loading branch information
carlinmack authored Jun 4, 2024
1 parent 6264e5c commit 34f9c02
Show file tree
Hide file tree
Showing 19 changed files with 728 additions and 231 deletions.
63 changes: 24 additions & 39 deletions invenio_jobs/administration/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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/<pid_value>"
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 = "<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"
Original file line number Diff line number Diff line change
@@ -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(<JobRunsHeaderComponent jobId={pidValue} />, header);
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className="column six wide">
<h1 className="ui header m-0">{title}</h1>
<p className="ui grey header">{description}</p>
</div>
<div className="column ten wide right aligned">
{loading ? null : (
<RunButton jobId={jobId} config={config} onError={this.onError} />
)}
</div>
</>
);
}
}

JobRunsHeaderComponent.propTypes = {
jobId: PropTypes.string.isRequired,
};
Original file line number Diff line number Diff line change
@@ -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 (
<Table.Row>
<Table.Cell
key={`job-name-${result.title}`}
data-label={i18next.t("Name")}
collapsing
className="word-break-all"
>
<a href={`/administration/jobs/${result.id}`}>{result.title}</a>
&nbsp;
<BoolFormatter
tooltip={i18next.t("Inactive")}
icon="ban"
color="grey"
value={result.active === false}
/>
</Table.Cell>
<Table.Cell
key={`job-last-run-${result.created}`}
data-label={i18next.t("Last run")}
collapsing
className=""
>
{lastRunStatus ? (
<>
<StatusFormatter status={lastRunStatus} />
<Popup
content={lastRunCreatedTime}
trigger={
<span>
{toRelativeTime(lastRunCreatedTime, i18next.language)}
</span>
}
/>
</>
) : (
"−"
)}
</Table.Cell>
{result?.last_run?.started_by ? (
<Table.Cell
key={`job-user-${result.last_run.started_by.id}`}
data-label={i18next.t("Started by")}
collapsing
className="word-break-all"
>
<UserListItemCompact
user={result.last_run.started_by}
id={result.last_run.started_by.id}
/>
</Table.Cell>
) : (
<Table.Cell
key="job-user"
data-label={i18next.t("Started by")}
collapsing
className="word-break-all"
>
System
</Table.Cell>
)}
<Table.Cell
collapsing
key={`job-next-run${result.next_run}`}
data-label={i18next.t("Next run")}
className="word-break-all"
>
{result.active === false
? "Inactive"
: toRelativeTime(result.next_run, i18next.language) ?? "−"}
</Table.Cell>
<Table.Cell collapsing>
<RunButton
jobId={result.id}
config={result.default_args ?? {}}
onError={this.onError}
setRun={(status, created) => {
this.setState({
lastRunStatus: status,
lastRunCreatedTime: created,
});
}}
/>
</Table.Cell>
</Table.Row>
);
}
}

SearchResultItemComponent.propTypes = {
result: PropTypes.object.isRequired,
};

SearchResultItemComponent.defaultProps = {};

export const SearchResultItemLayout = withState(SearchResultItemComponent);
Loading

0 comments on commit 34f9c02

Please sign in to comment.