Skip to content

Commit

Permalink
introducing list of jobs (#238)
Browse files Browse the repository at this point in the history
* introducing list of jobs
  • Loading branch information
eryshev authored Feb 26, 2018
1 parent 2f45034 commit b9ce644
Show file tree
Hide file tree
Showing 5 changed files with 351 additions and 7 deletions.
3 changes: 3 additions & 0 deletions core/src/main/javascript/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Backfills from "./app/pages/Backfills";
import BackfillCreate from "./app/pages/BackfillCreate";
import Favicon from "./app/components/Favicon";
import type { Statistics } from "./datamodel";
import { Jobs } from "./app/pages/Jobs";

type Props = {
page: Page,
Expand Down Expand Up @@ -107,6 +108,8 @@ class App extends React.Component {
return <BackfillCreate />;
case "timeseries/backfills/detail":
return <Backfill backfillId={page.backfillId} />;
case "jobs":
return <Jobs status={page.status} />;
default:
return null;
}
Expand Down
12 changes: 11 additions & 1 deletion core/src/main/javascript/ApplicationState.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ import type { Project, Workflow, Statistics } from "./datamodel";

import { prepareWorkflow } from "./datamodel";

export type JobStatus = "all" | "active" | "paused";

type JobsPage = {
id: "jobs",
status: JobStatus,
sort?: string,
order?: "asc" | "desc"
};

export type Page =
| { id: "" }
| {
Expand Down Expand Up @@ -58,7 +67,8 @@ export type Page =
| {
id: "timeseries/backfills/detail",
backfillId: string
};
}
| JobsPage;

export type State = {
page: Page,
Expand Down
32 changes: 28 additions & 4 deletions core/src/main/javascript/app/menu/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import MenuSubEntry from "./MenuSubEntry";
import LogIcon from "react-icons/lib/md/playlist-play";
import WorkflowIcon from "react-icons/lib/go/git-merge";
import CalendarIcon from "react-icons/lib/md/date-range";
import ListIcon from "react-icons/lib/md/format-list-bulleted";
import type { Statistics } from "../../datamodel";

type Props = {
Expand Down Expand Up @@ -99,14 +100,37 @@ const Menu = ({ classes, className, active, statistics }: Props) => (
link="/timeseries/backfills"
badges={[
statistics.scheduler &&
statistics.scheduler.backfills && {
label: statistics.scheduler.backfills,
kind: "alt"
}
statistics.scheduler.backfills && {
label: statistics.scheduler.backfills,
kind: "alt"
}
]}
/>
]}
/>
<MenuEntry
active={active.id.indexOf("jobs") === 0}
label="Jobs"
link="/jobs/all"
icon={<ListIcon style={{ transform: "translateY(-3px)" }} />}
subEntries={[
<MenuSubEntry
active={active.id.indexOf("jobs/all") === 0}
label="All"
link="/jobs/all"
/>,
<MenuSubEntry
active={active.id.indexOf("jobs/active") === 0}
label="Active"
link="/jobs/active"
/>,
<MenuSubEntry
active={active.id.indexOf("jobs/paused") === 0}
label="Paused"
link="/jobs/paused"
/>
]}
/>
</nav>
);

Expand Down
303 changes: 303 additions & 0 deletions core/src/main/javascript/app/pages/Jobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
// @flow

import * as React from "react";
import injectSheet from "react-jss";
import { connect } from "react-redux";
import { compose } from "redux";
import { navigate } from "redux-url";
import { displayFormat } from "../utils/Date";
import Spinner from "../components/Spinner";
import Table from "../components/Table";
import { Badge } from "../components/Badge";
import type { JobStatus } from "../../ApplicationState";

type Props = {
classes: any,
status: string,
selectedJobs: Array<string>,
envCritical: boolean,
sort: Sort,
open: (link: string, replace: boolean) => void
};

type State = {
data: Array<Job> | null,
pausedJobs: Set<string> | null
};

type Order = "asc" | "desc";

type Sort = {
column: string,
order: Order
};

type Columns = "id" | "name" | "description";

type Scheduling = {
start: string
};

type Job = {
[Columns]: string,
scheduling: Scheduling,
status: JobStatus
};

type JobsOrder = (Job, Job) => number;

const columns: Array<{
id: Columns | "startDate" | "status",
label: string,
sortable: boolean
}> = [
{ id: "id", label: "ID", sortable: true },
{ id: "name", label: "Name", sortable: true },
{ id: "description", label: "Description", sortable: false },
{ id: "startDate", label: "Start Date", sortable: true },
{ id: "status", label: "Status", sortable: true }
];

const column2Comp: {
[Columns]: ({ [Columns]: string }) => any,
startDate: ({ scheduling: Scheduling }) => any,
status: ({ status: JobStatus }) => any
} = {
id: ({ id }: { id: string }) => <span>{id}</span>,
name: ({ name }: { name: string }) => <span>{name}</span>,
description: ({ description }: { description: string }) => (
<span>{description}</span>
),
startDate: ({ scheduling }: { scheduling: Scheduling }) => (
<span>{displayFormat(new Date(scheduling.start))}</span>
),
status: ({ status }: { status: JobStatus }) => (
<Badge
label={status}
width={75}
light={true}
kind={status === "paused" ? "default" : "info"}
/>
)
};

const sortQueryString = (column: string, order: Order) =>
`?sort=${column}&order=${order}`;

const sortFunction: Sort => JobsOrder = (sort: Sort) => (a: Job, b: Job) => {
const idOrder = (a: Job, b: Job) => a.id.localeCompare(b.id);
const nameOrder = (a: Job, b: Job) => a.name.localeCompare(b.name);
const statusOrder = (a: Job, b: Job) => a.status.localeCompare(b.status);
const sortFn = (sort: Sort) => {
switch (sort.column) {
case "id":
return idOrder;
case "name":
return nameOrder;
case "status":
return statusOrder;
default:
return idOrder;
}
};

return sort.order === "desc" ? -sortFn(sort)(a, b) : sortFn(sort)(a, b);
};

const processResponse = (response: Response) => {
if (!response.ok) throw new Error({ _error: response.statusText });
return response.json();
};

const fetchWorkflow = (persist: ({ data: Array<Job> }) => void) => {
return fetch("/api/workflow_definition")
.then(processResponse)
.then(json => ({ data: json.jobs }))
.then(persist);
};

const fetchPausedJobs = (persist: ({ pausedJobs: Set<string> }) => void) => {
return fetch("/api/jobs/paused")
.then(processResponse)
.then(jsonArr => ({ pausedJobs: new Set(jsonArr) }))
.then(persist);
};

const NoJobs = ({
className,
status,
selectedJobs
}: {
className: string,
status: string,
selectedJobs: Array<string>
}) => (
<div className={className}>
{`No ${status.toLocaleLowerCase()} jobs for now`}
{selectedJobs.length ? " (some may have been filtered)" : ""}
</div>
);

class JobsComp extends React.Component<any, Props, State> {
state: State;

constructor(props: Props) {
super(props);
this.state = {
data: null,
pausedJobs: null
};
}

componentDidMount() {
const persist = this.setState.bind(this);
fetchWorkflow(persist);
fetchPausedJobs(persist);
}

componentWillReceiveProps() {
this.componentDidMount();
}

render() {
const { classes, status, sort, selectedJobs, envCritical } = this.props;
const { data, pausedJobs } = this.state;
const setOfSelectedJobs = new Set(selectedJobs);

const Data = () => {
if (data && data.length && pausedJobs) {
const preparedData = data
.map(datum => ({
...datum,
status: pausedJobs.has(datum.id) ? "paused" : "active"
}))
.filter(
job =>
(setOfSelectedJobs.size === 0 || setOfSelectedJobs.has(job.id)) &&
(job.status === status || status === "all")
)
.sort(sortFunction(sort));

return preparedData.length !== 0 ? (
<Table
data={preparedData}
columns={columns}
sort={sort}
envCritical={envCritical}
onSortBy={this.handleSortBy.bind(this)}
render={(column, row) => {
return column2Comp[column](row);
}}
/>
) : (
<NoJobs
className={classes.noData}
status={status}
selectedJobs={selectedJobs}
/>
);
} else if (data && pausedJobs) {
return (
<NoJobs
className={classes.noData}
status={status}
selectedJobs={selectedJobs}
/>
);
} else {
return <Spinner />;
}
};

return (
<div className={classes.container}>
<h1 className={classes.title}>Jobs</h1>
<div className={classes.grid}>
<div className={classes.data}>
<Data />
</div>
</div>
</div>
);
}

handleSortBy(column: string) {
const { sort } = this.props;
const order = sort.order === "asc" ? "desc" : "asc";
this.props.open(sortQueryString(column, order), false);
}
}

const styles = {
container: {
padding: "1em",
flex: "1",
display: "flex",
flexDirection: "column",
position: "relative"
},
title: {
fontSize: "1.2em",
margin: "0 0 16px 0",
color: "#607e96",
fontWeight: "normal"
},
menu: {
position: "absolute",
top: "1em",
right: "1em"
},
grid: {
overflow: "auto",
flex: "1",
display: "flex",
flexDirection: "column"
},
data: {
display: "flex",
flex: "1"
},
time: {
color: "#8089a2"
},
noData: {
flex: "1",
textAlign: "center",
fontSize: "0.9em",
color: "#8089a2",
alignSelf: "center",
paddingBottom: "15%"
},
openIcon: {
fontSize: "22px",
color: "#607e96",
padding: "15px",
margin: "-15px"
}
};

const mapStateToProps = (
{ app: { project, page: { sort, order }, selectedJobs } },
ownProps
) => {
return {
sort: {
column: sort || "id",
order: order || "asc"
},
selectedJobs,
envCritical: project.env.critical,
status: ownProps.status
};
};

const mapDispatchToProps = dispatch => ({
open(href, replace) {
dispatch(navigate(href, replace));
}
});

export const Jobs = compose(
injectSheet(styles),
connect(mapStateToProps, mapDispatchToProps)
)(JobsComp);
Loading

0 comments on commit b9ce644

Please sign in to comment.