diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 27d383c..5d2fb3c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import { Dispatch } from 'redux'; import { Button, Classes, Dialog, NonIdealState, Spinner } from '@blueprintjs/core'; import { IconNames } from '@blueprintjs/icons'; +import ErrorState from './components/ErrorState'; import { Graph } from './components/Graph'; import { Nav } from './components/Nav'; import { Sidebar } from './components/Sidebar'; @@ -17,6 +18,7 @@ interface IAppProps { instances?: IInstance[], isLoadingGraph: boolean; isLoadingInstances: boolean, + graphLoadError: boolean, fetchInstances: () => void; fetchGraph: () => void; } @@ -34,7 +36,7 @@ class AppImpl extends React.Component { let body =
; if (this.props.isLoadingInstances || this.props.isLoadingGraph) { body = this.loadingState("Loading..."); - } else if (!!this.props.graph) { + } else { body = this.graphState(); } return ( @@ -58,19 +60,20 @@ class AppImpl extends React.Component { } private load = () => { - if (!this.props.instances && !this.props.isLoadingInstances) { + if (!this.props.instances && !this.props.isLoadingInstances && !this.props.graphLoadError) { this.props.fetchInstances(); } - if (!this.props.graph && !this.props.isLoadingGraph) { + if (!this.props.graph && !this.props.isLoadingGraph && !this.props.graphLoadError) { this.props.fetchGraph(); } } private graphState = () => { + const content = this.props.graphLoadError ? : return (
- + {content}
) } @@ -123,6 +126,7 @@ class AppImpl extends React.Component { const mapStateToProps = (state: IAppState) => ({ graph: state.data.graph, + graphLoadError: state.data.error, instances: state.data.instances, isLoadingGraph: state.data.isLoadingGraph, isLoadingInstances: state.data.isLoadingInstances, diff --git a/frontend/src/components/ErrorState.tsx b/frontend/src/components/ErrorState.tsx new file mode 100644 index 0000000..086b12d --- /dev/null +++ b/frontend/src/components/ErrorState.tsx @@ -0,0 +1,12 @@ +import { NonIdealState } from '@blueprintjs/core'; +import { IconNames } from '@blueprintjs/icons'; +import * as React from 'react'; + + +const ErrorState: React.SFC = () => ( + +) +export default ErrorState; \ No newline at end of file diff --git a/frontend/src/components/Graph.jsx b/frontend/src/components/Graph.jsx index 2769a91..ba26e71 100644 --- a/frontend/src/components/Graph.jsx +++ b/frontend/src/components/Graph.jsx @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma'; import { selectAndLoadInstance } from '../redux/actions'; +import ErrorState from './ErrorState'; const STYLE = { bottom: "0", @@ -11,10 +12,12 @@ const STYLE = { right: "0", top: "50px", } +const DEFAULT_NODE_COLOR = "#CED9E0"; +const SELECTED_NODE_COLOR = "#48AFF0"; const SETTINGS = { defaultEdgeColor: "#5C7080", defaultLabelColor: "#F5F8FA", - defaultNodeColor: "#CED9E0", + defaultNodeColor: DEFAULT_NODE_COLOR, drawEdges: true, drawLabels: true, edgeColor: "default", @@ -26,11 +29,15 @@ const SETTINGS = { class GraphImpl extends React.Component { + constructor(props) { + super(props); + this.sigmaComponent = React.createRef(); + } + render() { let graph = this.props.graph; if (!graph) { - // TODO: error state - return null; + return ; } // Check that all nodes have size & coordinates; otherwise the graph will look messed up const lengthBeforeFilter = graph.nodes.length; @@ -38,6 +45,7 @@ class GraphImpl extends React.Component { if (graph.nodes.length !== lengthBeforeFilter) { // tslint:disable-next-line:no-console console.error("Some nodes were missing details: " + this.props.graph.nodes.filter(n => !n.size || !n.x || !n.y).map(n => n.label)); + return ; } return ( this.props.selectAndLoadInstance(e.data.node.label)} + onClickNode={this.onClickNode} onClickStage={this.onClickStage} + ref={this.sigmaComponent} > - + ) } + componentDidUpdate() { + const sigma = this.sigmaComponent.current.sigma; + sigma.graph.nodes().map(this.colorNodes); + sigma.refresh(); + } + + onClickNode = (e) => { + this.props.selectAndLoadInstance(e.data.node.label); + } + onClickStage = (e) => { // Deselect the instance (unless this was a drag event) if (!e.data.captor.isDragging) { this.props.selectAndLoadInstance(null); } } + + colorNodes = (n) => { + if (this.props.currentInstanceName && n.id === this.props.currentInstanceName) { + n.color = SELECTED_NODE_COLOR; + } else { + n.color = DEFAULT_NODE_COLOR; + } + } } const mapStateToProps = (state) => ({ diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index b97a12e..8256e94 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -13,10 +13,12 @@ import { IconNames } from '@blueprintjs/icons'; import { selectAndLoadInstance } from '../redux/actions'; import { IAppState, IGraph, IInstanceDetails } from '../redux/types'; +import ErrorState from './ErrorState'; interface ISidebarProps { graph?: IGraph, instanceName: string | null, + instanceLoadError: boolean, instanceDetails: IInstanceDetails | null, isLoadingInstanceDetails: boolean; selectAndLoadInstance: (instanceName: string) => void; @@ -64,6 +66,8 @@ class SidebarImpl extends React.Component { return this.renderPersonalInstanceErrorState(); } else if (this.props.instanceDetails.status !== 'success') { return this.renderMissingDataState(); + } else if (this.props.instanceLoadError) { + return ; } return (
@@ -292,6 +296,7 @@ class SidebarImpl extends React.Component { const mapStateToProps = (state: IAppState) => ({ graph: state.data.graph, instanceDetails: state.currentInstance.currentInstanceDetails, + instanceLoadError: state.currentInstance.error, instanceName: state.currentInstance.currentInstanceName, isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails, }); diff --git a/frontend/src/redux/actions.ts b/frontend/src/redux/actions.ts index 920b323..315e845 100644 --- a/frontend/src/redux/actions.ts +++ b/frontend/src/redux/actions.ts @@ -41,6 +41,18 @@ export const receiveGraph = (graph: IGraph) => { } } +const graphLoadFailed = () => { + return { + type: ActionType.GRAPH_LOAD_ERROR, + } +} + +const instanceLoadFailed = () => { + return { + type: ActionType.INSTANCE_LOAD_ERROR, + } +} + export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => { return { payload: instanceDetails, @@ -52,16 +64,15 @@ export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => { /** Async actions: https://redux.js.org/advanced/asyncactions */ export const fetchInstances = () => { - // TODO: handle errors return (dispatch: Dispatch) => { dispatch(requestInstances()); return getFromApi("instances") - .then(instances => dispatch(receiveInstances(instances))); + .then(instances => dispatch(receiveInstances(instances))) + .catch(e => dispatch(graphLoadFailed())); } } export const selectAndLoadInstance = (instanceName: string) => { - // TODO: handle errors return (dispatch: Dispatch) => { if (!instanceName) { dispatch(deselectInstance()); @@ -69,12 +80,12 @@ export const selectAndLoadInstance = (instanceName: string) => { } dispatch(selectInstance(instanceName)); return getFromApi("instances/" + instanceName) - .then(details => dispatch(receiveInstanceDetails(details))); + .then(details => dispatch(receiveInstanceDetails(details))) + .catch(e => dispatch(instanceLoadFailed())); } } export const fetchGraph = () => { - // TODO: handle errors return (dispatch: Dispatch) => { dispatch(requestGraph()); return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")]) @@ -84,6 +95,7 @@ export const fetchGraph = () => { nodes: responses[1], }; }) - .then(graph => dispatch(receiveGraph(graph))); + .then(graph => dispatch(receiveGraph(graph))) + .catch(e => dispatch(graphLoadFailed())); } } diff --git a/frontend/src/redux/reducers.ts b/frontend/src/redux/reducers.ts index 87a7da0..830d1c6 100644 --- a/frontend/src/redux/reducers.ts +++ b/frontend/src/redux/reducers.ts @@ -3,6 +3,7 @@ import { combineReducers } from 'redux'; import { ActionType, IAction, ICurrentInstanceState, IDataState } from './types'; const initialDataState = { + error: false, isLoadingGraph: false, isLoadingInstances: false, } @@ -31,6 +32,13 @@ const data = (state: IDataState = initialDataState, action: IAction) => { graph: action.payload, isLoadingGraph: false, }; + case ActionType.GRAPH_LOAD_ERROR: + return { + ...state, + error: true, + isLoadingGraph: false, + isLoadingInstances: false, + }; default: return state; } @@ -39,7 +47,8 @@ const data = (state: IDataState = initialDataState, action: IAction) => { const initialCurrentInstanceState = { currentInstanceDetails: null, currentInstanceName: null, - isLoadingInstanceDetails: false + error: false, + isLoadingInstanceDetails: false, }; const currentInstance = (state = initialCurrentInstanceState , action: IAction): ICurrentInstanceState => { switch (action.type) { @@ -61,6 +70,12 @@ const currentInstance = (state = initialCurrentInstanceState , action: IAction): currentInstanceDetails: null, currentInstanceName: null, } + case ActionType.INSTANCE_LOAD_ERROR: + return { + ...state, + error: true, + isLoadingInstanceDetails: false, + }; default: return state; } diff --git a/frontend/src/redux/types.ts b/frontend/src/redux/types.ts index 058eac0..7f4bfeb 100644 --- a/frontend/src/redux/types.ts +++ b/frontend/src/redux/types.ts @@ -5,7 +5,9 @@ export enum ActionType { REQUEST_GRAPH = 'REQUEST_GRAPH', RECEIVE_GRAPH = 'RECEIVE_GRAPH', RECEIVE_INSTANCE_DETAILS = 'RECEIVE_INSTANCE_DETAILS', - DESELECT_INSTANCE = "DESELECT_INSTANCE", + DESELECT_INSTANCE = 'DESELECT_INSTANCE', + GRAPH_LOAD_ERROR = 'GRAPH_LOAD_ERROR', + INSTANCE_LOAD_ERROR = 'INSTANCE_LOAD_ERROR' } export interface IAction { @@ -57,6 +59,7 @@ export interface ICurrentInstanceState { currentInstanceDetails: IInstanceDetails | null, currentInstanceName: string | null, isLoadingInstanceDetails: boolean, + error: boolean, } export interface IDataState { @@ -64,6 +67,7 @@ export interface IDataState { graph?: IGraph, isLoadingInstances: boolean, isLoadingGraph: boolean, + error: boolean, } export interface IAppState {