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

Add Job details view #14

Merged
merged 24 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
dca4a5b
refactor: move search index
carlinmack May 15, 2024
d867ef9
UI: add MVP for job details view
carlinmack May 17, 2024
b745e13
UI: Set title to name of job and move status
carlinmack May 22, 2024
2181984
fix: adapt to actual API
carlinmack May 24, 2024
f3598e7
UX: Add run button dropdown
carlinmack May 24, 2024
e1f23ff
UI: remove settings button
carlinmack May 27, 2024
072bd1c
bug: fix getting user from API and pid_value encoding
carlinmack May 28, 2024
cc5e72c
UI: Fix details page and user display
carlinmack May 28, 2024
d4794c9
UI: add pop up showing timestamp
carlinmack May 28, 2024
ff54715
UI: add description
carlinmack May 28, 2024
e39626e
refactor: move classes into template
carlinmack May 29, 2024
f4b064c
UI: move active status and remove schedule button
carlinmack May 29, 2024
b6f7f0d
UX: add interactive run button to details page
carlinmack May 29, 2024
f5ad309
refactor: move status formatter to own component
carlinmack May 29, 2024
a931ce5
api: change RunStatusEnum states
carlinmack May 29, 2024
8259d8e
JS: add run button
carlinmack May 29, 2024
b61b037
JS: Add stop button
carlinmack May 30, 2024
9f10fa1
UX: add error message for actions
carlinmack May 30, 2024
2e76674
refactor: turn JobRunsHeader into a component
carlinmack May 30, 2024
771e9b7
refactor: move JS files
carlinmack May 30, 2024
007d3ed
UX: have stop button alter status state
carlinmack May 31, 2024
2b8a33b
UI: Refactor RunButton into component
carlinmack Jun 3, 2024
bee1708
UI: have run button update jobs list status
carlinmack Jun 3, 2024
5a49885
JS: use cancellable promises
carlinmack Jun 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
carlinmack marked this conversation as resolved.
Show resolved Hide resolved

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;
alejandromumo marked this conversation as resolved.
Show resolved Hide resolved
<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