From e7ad07c8b8ccd45279d846e76de9bb3deceaa596 Mon Sep 17 00:00:00 2001 From: Alexey Eryshev Date: Fri, 23 Feb 2018 15:26:06 +0100 Subject: [PATCH 1/2] introducing list of jobs --- core/src/main/javascript/App.js | 7 + core/src/main/javascript/ApplicationState.js | 9 +- core/src/main/javascript/app/menu/Menu.js | 24 ++ core/src/main/javascript/app/pages/Jobs.js | 304 +++++++++++++++++++ core/src/main/javascript/index.js | 8 +- 5 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 core/src/main/javascript/app/pages/Jobs.js diff --git a/core/src/main/javascript/App.js b/core/src/main/javascript/App.js index 5dd51af03..158bd875a 100644 --- a/core/src/main/javascript/App.js +++ b/core/src/main/javascript/App.js @@ -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, @@ -107,6 +108,12 @@ class App extends React.Component { return ; case "timeseries/backfills/detail": return ; + case "jobs/all": + return ; + case "jobs/active": + return ; + case "jobs/paused": + return ; default: return null; } diff --git a/core/src/main/javascript/ApplicationState.js b/core/src/main/javascript/ApplicationState.js index 46700b2dc..02b08c810 100644 --- a/core/src/main/javascript/ApplicationState.js +++ b/core/src/main/javascript/ApplicationState.js @@ -5,6 +5,12 @@ import type { Project, Workflow, Statistics } from "./datamodel"; import { prepareWorkflow } from "./datamodel"; +type JobsPage = { + id: "jobs" | "jobs/active" | "jobs/paused" | "jobs/all", + sort?: string, + order?: "asc" | "desc" +}; + export type Page = | { id: "" } | { @@ -58,7 +64,8 @@ export type Page = | { id: "timeseries/backfills/detail", backfillId: string - }; + } + | JobsPage; export type State = { page: Page, diff --git a/core/src/main/javascript/app/menu/Menu.js b/core/src/main/javascript/app/menu/Menu.js index 9770a7190..ae6fe59ce 100644 --- a/core/src/main/javascript/app/menu/Menu.js +++ b/core/src/main/javascript/app/menu/Menu.js @@ -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 = { @@ -107,6 +108,29 @@ const Menu = ({ classes, className, active, statistics }: Props) => ( /> ]} /> + } + subEntries={[ + , + , + + ]} + /> ); diff --git a/core/src/main/javascript/app/pages/Jobs.js b/core/src/main/javascript/app/pages/Jobs.js new file mode 100644 index 000000000..d10480a97 --- /dev/null +++ b/core/src/main/javascript/app/pages/Jobs.js @@ -0,0 +1,304 @@ +// @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"; + +type Props = { + classes: any, + status: string, + selectedJobs: Array, + envCritical: boolean, + sort: Sort, + open: (link: string, replace: boolean) => void +}; + +type State = { + data: Array | null, + pausedJobs: Set | null +}; + +type Order = "asc" | "desc"; + +type Sort = { + column: string, + order: Order +}; + +type Columns = "id" | "name" | "description"; + +type Scheduling = { + start: string +}; + +type JobStatus = "Paused" | "Active"; + +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 }) => {id}, + name: ({ name }: { name: string }) => {name}, + description: ({ description }: { description: string }) => ( + {description} + ), + startDate: ({ scheduling }: { scheduling: Scheduling }) => ( + {displayFormat(new Date(scheduling.start))} + ), + status: ({ status }: { status: JobStatus }) => ( + + ) +}; + +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 }) => void) => { + return fetch("/api/workflow_definition") + .then(processResponse) + .then(json => ({ data: json.jobs })) + .then(persist); +}; + +const fetchPausedJobs = (persist: ({ pausedJobs: Set }) => 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 +}) => ( +
+ {`No ${status.toLocaleLowerCase()} jobs for now`} + {selectedJobs.length ? " (some may have been filtered)" : ""} +
+); + +class JobsComp extends React.Component { + 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 ? ( + { + return column2Comp[column](row); + }} + /> + ) : ( + + ); + } else if (data && pausedJobs) { + return ( + + ); + } else { + return ; + } + }; + + return ( +
+

Jobs

+
+
+ +
+
+
+ ); + } + + 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); diff --git a/core/src/main/javascript/index.js b/core/src/main/javascript/index.js index f0286a220..ebf81be41 100644 --- a/core/src/main/javascript/index.js +++ b/core/src/main/javascript/index.js @@ -58,7 +58,13 @@ const routes = { id: "timeseries/executions", ...parseExecutionsRoute(_) }); - } + }, + "/jobs/all": (_, { sort, order }) => + openPage({ id: "jobs/all", sort, order }), + "/jobs/active": (_, { sort, order }) => + openPage({ id: "jobs/active", sort, order }), + "/jobs/paused": (_, { sort, order }) => + openPage({ id: "jobs/paused", sort, order }) }; const parseExecutionsRoute = (() => { From f9a05bfe53c352c479abfef946a518ee631067c3 Mon Sep 17 00:00:00 2001 From: Alexey Eryshev Date: Mon, 26 Feb 2018 13:27:44 +0100 Subject: [PATCH 2/2] factorisation of jobs routes --- core/src/main/javascript/App.js | 8 ++------ core/src/main/javascript/ApplicationState.js | 5 ++++- core/src/main/javascript/app/menu/Menu.js | 10 +++++----- core/src/main/javascript/app/pages/Jobs.js | 9 ++++----- core/src/main/javascript/index.js | 12 +++++------- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/core/src/main/javascript/App.js b/core/src/main/javascript/App.js index 158bd875a..6e203647c 100644 --- a/core/src/main/javascript/App.js +++ b/core/src/main/javascript/App.js @@ -108,12 +108,8 @@ class App extends React.Component { return ; case "timeseries/backfills/detail": return ; - case "jobs/all": - return ; - case "jobs/active": - return ; - case "jobs/paused": - return ; + case "jobs": + return ; default: return null; } diff --git a/core/src/main/javascript/ApplicationState.js b/core/src/main/javascript/ApplicationState.js index 02b08c810..3a34e75ba 100644 --- a/core/src/main/javascript/ApplicationState.js +++ b/core/src/main/javascript/ApplicationState.js @@ -5,8 +5,11 @@ import type { Project, Workflow, Statistics } from "./datamodel"; import { prepareWorkflow } from "./datamodel"; +export type JobStatus = "all" | "active" | "paused"; + type JobsPage = { - id: "jobs" | "jobs/active" | "jobs/paused" | "jobs/all", + id: "jobs", + status: JobStatus, sort?: string, order?: "asc" | "desc" }; diff --git a/core/src/main/javascript/app/menu/Menu.js b/core/src/main/javascript/app/menu/Menu.js index ae6fe59ce..ed43a2a95 100644 --- a/core/src/main/javascript/app/menu/Menu.js +++ b/core/src/main/javascript/app/menu/Menu.js @@ -100,16 +100,16 @@ 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" + } ]} /> ]} /> } diff --git a/core/src/main/javascript/app/pages/Jobs.js b/core/src/main/javascript/app/pages/Jobs.js index d10480a97..557d8be17 100644 --- a/core/src/main/javascript/app/pages/Jobs.js +++ b/core/src/main/javascript/app/pages/Jobs.js @@ -9,6 +9,7 @@ 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, @@ -37,8 +38,6 @@ type Scheduling = { start: string }; -type JobStatus = "Paused" | "Active"; - type Job = { [Columns]: string, scheduling: Scheduling, @@ -77,7 +76,7 @@ const column2Comp: { label={status} width={75} light={true} - kind={status === "Paused" ? "default" : "info"} + kind={status === "paused" ? "default" : "info"} /> ) }; @@ -170,12 +169,12 @@ class JobsComp extends React.Component { const preparedData = data .map(datum => ({ ...datum, - status: pausedJobs.has(datum.id) ? "Paused" : "Active" + status: pausedJobs.has(datum.id) ? "paused" : "active" })) .filter( job => (setOfSelectedJobs.size === 0 || setOfSelectedJobs.has(job.id)) && - (job.status === status || status === "All") + (job.status === status || status === "all") ) .sort(sortFunction(sort)); diff --git a/core/src/main/javascript/index.js b/core/src/main/javascript/index.js index ebf81be41..255a0f03c 100644 --- a/core/src/main/javascript/index.js +++ b/core/src/main/javascript/index.js @@ -59,12 +59,8 @@ const routes = { ...parseExecutionsRoute(_) }); }, - "/jobs/all": (_, { sort, order }) => - openPage({ id: "jobs/all", sort, order }), - "/jobs/active": (_, { sort, order }) => - openPage({ id: "jobs/active", sort, order }), - "/jobs/paused": (_, { sort, order }) => - openPage({ id: "jobs/paused", sort, order }) + "/jobs/:status": ({ status }, { sort, order }) => + openPage({ id: "jobs", status, sort, order }) }; const parseExecutionsRoute = (() => { @@ -98,7 +94,9 @@ router.sync(); store.dispatch(Actions.loadAppData()); // Global stats listener -let statisticsQuery = null, statisticsListener = null, statisticsError = null; +let statisticsQuery = null, + statisticsListener = null, + statisticsError = null; let listenForStatistics = (query: string) => { if (query != statisticsQuery) { statisticsListener && statisticsListener.close();