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

Tasks component #18

Merged
merged 15 commits into from
Nov 14, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
2 changes: 1 addition & 1 deletion backend/elasticsurgery/views/clusters_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ def put_cluster_settings(cluster_client):
@app.route('/api/clusters/<cluster_slug>/tasks', methods=('GET',))
@pass_cluster_client
def get_cluster_tasks(cluster_client):
tasks = cluster_client.tasks.list()
tasks = cluster_client.tasks.list(group_by='parents')
return jsonify(**tasks)
2 changes: 1 addition & 1 deletion frontend/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>ElasticSurgery</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
Expand Down
41 changes: 38 additions & 3 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React from 'react';
import './App.css';
import { AppBar, Grid, Toolbar, Typography, Drawer, List, ListItem } from '@material-ui/core';
import { AppBar, Grid, Toolbar, Typography, Drawer, List, ListItem, ListSubheader } from '@material-ui/core';
import { Provider } from 'react-redux';

import { BrowserRouter as Router, Route, Link } from "react-router-dom";

import ShardsDashboard from './ShardsDashboard.jsx';
import NodesDashboard from './NodesDashboard.jsx';
import TasksDashboard from './TasksDashboard.jsx';
import Settings from './Settings.jsx';
import ClustersDashboard from './ClustersDashboard.jsx';
import ClusterSelector from './ClusterSelector.jsx';
import store from './data/store';
import { loadNodes } from './data/nodes/actions';

const routes = [

const clusterRoutes = [
{
name: "Home",
path: "/",
Expand All @@ -29,13 +32,29 @@ const routes = [
path: "/nodes",
component: NodesDashboard,
},
{
name: "Tasks",
path: "/tasks",
component: TasksDashboard,
},
{
name: "Settings",
path: "/settings",
component: Settings,
},
]

const configRoutes = [
{
name: "Clusters",
path: "/clusters",
component: ClustersDashboard,
}
]

const routes = clusterRoutes.concat(configRoutes);


class App extends React.Component {
componentDidMount() {
store.dispatch(loadNodes());
Expand Down Expand Up @@ -96,7 +115,23 @@ class App extends React.Component {
<ClusterSelector />
</div>
<List>
{routes.map(route => (
{clusterRoutes.map(route => (
<ListItem button component={Link} key={route.name} to={route.path}>
{route.name}
</ListItem>
))}
</List>
<List
subheader={
<ListSubheader
component="div"
id="nested-list-subheader"
>
ElasticSurgery Configuration
</ListSubheader>
}
>
{configRoutes.map(route => (
<ListItem button component={Link} key={route.name} to={route.path}>
{route.name}
</ListItem>
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/ClustersDashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from "react";
import { connect } from 'react-redux';

const mapStateToProps = () => ({});

const mapDispatchToProps = {
}

class ClustersDashboard extends React.Component {

getContainerStyles(dataLoaded) {
return {
display: 'flex',
margin: '0 auto',
height: '100vh',
alignItems: 'flex-start',
justifyContent: dataLoaded ? 'flex-start' : 'center',
};
}

render() {
return "Clusters!"
}
}

export default connect(mapStateToProps, mapDispatchToProps)(ClustersDashboard);
4 changes: 2 additions & 2 deletions frontend/src/NodesDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const mapDispatchToProps = {
loadNodes,
}

class ShardsDashboard extends React.Component {
class NodesDashboard extends React.Component {
static propTypes = {
nodes: PropTypes.shape({
loadingState: PropTypes.oneOf(['NOT_LOADED', 'LOADING', 'LOADED', 'ERROR']).isRequired,
Expand Down Expand Up @@ -96,4 +96,4 @@ class ShardsDashboard extends React.Component {
}
}

export default connect(mapStateToProps, mapDispatchToProps)(ShardsDashboard);
export default connect(mapStateToProps, mapDispatchToProps)(NodesDashboard);
150 changes: 150 additions & 0 deletions frontend/src/TasksDashboard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import React from "react";
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Typography from '@material-ui/core/Typography';
import CircularProgress from '@material-ui/core/CircularProgress';

import { loadTasks } from './data/tasks/actions';
import { loadNodes } from './data/nodes/actions';
import { isErrored, isLoaded, isLoading, isNotLoaded } from './data/utils';
import Table from './Table';

const mapStateToProps = ({ tasks, nodes, clusters }) => ({
tasks,
nodes,
clusters,
});

const mapDispatchToProps = {
loadTasks,
loadNodes,
}

class TasksDashboard extends React.Component {
static propTypes = {
tasks: PropTypes.shape({
loadingState: PropTypes.oneOf(['NOT_LOADED', 'LOADING', 'LOADED', 'ERROR']).isRequired,
data: PropTypes.object,
error: PropTypes.object,
}).isRequired,
Fizzadar marked this conversation as resolved.
Show resolved Hide resolved
nodes: PropTypes.shape({
loadingState: PropTypes.oneOf(['NOT_LOADED', 'LOADING', 'LOADED', 'ERROR']).isRequired,
data: PropTypes.object,
error: PropTypes.object,
}).isRequired,
clusters: PropTypes.shape({
loadingState: PropTypes.oneOf(['NOT_LOADED', 'LOADING', 'LOADED', 'ERROR']).isRequired,
currentCluster: PropTypes.string,
}).isRequired,
loadNodes: PropTypes.func.isRequired,
loadTasks: PropTypes.func.isRequired,
};

componentDidMount() {
const { tasks, clusters, nodes } = this.props;
if (!isLoaded(clusters)) {
return;
}

if (isNotLoaded(tasks) || isErrored(tasks)) {
this.props.loadTasks();
}

if (isNotLoaded(nodes) || isErrored(nodes)) {
this.props.loadNodes();
}
}

componentDidUpdate(prevProps) {
if (prevProps.clusters.currentCluster !== this.props.clusters.currentCluster) {
this.props.loadNodes();
this.props.loadTasks();
}
}

getContainerStyles(dataLoaded) {
return {
display: 'flex',
margin: '0 auto',
height: '100vh',
alignItems: 'flex-start',
justifyContent: dataLoaded ? 'flex-start' : 'center',
};
}

render() {
const { tasks, nodes } = this.props;
if (isNotLoaded(tasks) || isLoading(tasks) || isNotLoaded(nodes) || isLoading(nodes)) {
return <div style={this.getContainerStyles()}>
<CircularProgress />
</div>;
}

if (isErrored(tasks) || isErrored(nodes)) {
return <div style={this.getContainerStyles()}>
<Typography variant="body1" component="p">
An error occurred loading the current status
</Typography>
</div>;
}

const tableConfig = [
{
title: 'ID',
dataKey: 'id',
width: 100,
sortable: true,
},
{
title: 'Type',
dataKey: 'type',
width: 200,
sortable: true,
},
{
title: 'Action',
dataKey: 'action',
width: 500,
sortable: true,
},
{
title: 'Node',
dataKey: 'node',
width: 300,
sortable: true,
formatter: nodeId => {
const nodeData = nodes.data.nodes[nodeId];
return nodeData ? nodeData.name : nodeId;
},
},
{
title: 'Running Time',
dataKey: 'running_time_in_nanos',
width: 200,
sortable: true,
},
{
title: 'Child Tasks',
dataKey: 'children',
sortable: true,
formatter: children => children ? children.length : 0,
},
];

const tasksData = this.props.tasks.data;
const taskIndices = Object.keys(tasksData.tasks);

console.log(tasksData, taskIndices);

const tableData = taskIndices.map(index => ({
index,
...tasksData.tasks[index]
}));

return <div style={this.getContainerStyles(true)}>
<Table config={tableConfig} data={tableData} />
</div>;
}
}

export default connect(mapStateToProps, mapDispatchToProps)(TasksDashboard);
2 changes: 1 addition & 1 deletion frontend/src/data/clusters/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const CLUSTER_ACTION_TYPES = {
export function setCurrentCluster(clusterSlug) {
return {
type: CLUSTER_ACTION_TYPES.SET_CURRENT_CLUSTER,
clusterSlug,
currentCluster: clusterSlug,
};
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/data/clusters/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function reducer(state, action) {
case CLUSTER_ACTION_TYPES.SET_CURRENT_CLUSTER:
return {
...state,
currentCluster: action.clusterSlug,
currentCluster: action.currentCluster,
};
default:
return state;
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/data/reducers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import { reducer as shards } from './shards/reducer';
import { reducer as nodes } from './nodes/reducer';
import { reducer as clusters } from './clusters/reducer';
import { reducer as settings } from './settings/reducer';
import { reducer as tasks } from './tasks/reducer';

export default combineReducers({ shards, nodes, clusters, settings });
export default combineReducers({ shards, nodes, clusters, settings, tasks });
36 changes: 36 additions & 0 deletions frontend/src/data/tasks/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export const TASK_ACTION_TYPES = {
LOAD_TASK_STATUS: 'LOAD_TASK_STATUS',
LOAD_TASK_STATUS_SUCCESS: 'LOAD_TASK_STATUS_SUCCESS',
LOAD_TASK_STATUS_ERROR: 'LOAD_TASK_STATUS_ERROR',
};

export function loadTasks() {
return async (dispatch, getState) => {
const currentCluster = getState().clusters.currentCluster;
if (!currentCluster) {
return;
}

dispatch({
type: TASK_ACTION_TYPES.LOAD_TASK_STATUS,
});

try {
const response = await fetch(`/api/clusters/${currentCluster}/tasks`);
if (response.ok) {
const data = await response.json();
dispatch({
type: TASK_ACTION_TYPES.LOAD_TASK_STATUS_SUCCESS,
data,
});
} else {
throw new Error(`Bad response from server: ${response.statusText}`);
}
} catch (error) {
dispatch({
type: TASK_ACTION_TYPES.LOAD_TASK_STATUS_ERROR,
error,
});
}
}
}
36 changes: 36 additions & 0 deletions frontend/src/data/tasks/reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { TASK_ACTION_TYPES } from './actions';

export const initialState = {
loadingState: 'NOT_LOADED',
};

export function reducer(state, action) {
if (!state) {
return {
...initialState,
};
}

switch (action.type) {
case TASK_ACTION_TYPES.LOAD_TASK_STATUS:
return {
...state,
loadingState: 'LOADING',
error: null,
};
case TASK_ACTION_TYPES.LOAD_TASK_STATUS_SUCCESS:
return {
...state,
loadingState: 'LOADED',
data: action.data,
};
case TASK_ACTION_TYPES.LOAD_TASK_STATUS_ERROR:
return {
...state,
loadingState: 'ERROR',
error: action.error,
};
default:
return state;
}
}