Highlight selected instance (#21)

* improve error handling

* highlight currently selected instance
pull/1/head
Tao Bror Bojlén 2018-12-06 18:48:32 +00:00 zatwierdzone przez GitHub
rodzic 7182f14c74
commit d2335c8851
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 96 dodań i 17 usunięć

Wyświetl plik

@ -5,6 +5,7 @@ import { Dispatch } from 'redux';
import { Button, Classes, Dialog, NonIdealState, Spinner } from '@blueprintjs/core'; import { Button, Classes, Dialog, NonIdealState, Spinner } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import ErrorState from './components/ErrorState';
import { Graph } from './components/Graph'; import { Graph } from './components/Graph';
import { Nav } from './components/Nav'; import { Nav } from './components/Nav';
import { Sidebar } from './components/Sidebar'; import { Sidebar } from './components/Sidebar';
@ -17,6 +18,7 @@ interface IAppProps {
instances?: IInstance[], instances?: IInstance[],
isLoadingGraph: boolean; isLoadingGraph: boolean;
isLoadingInstances: boolean, isLoadingInstances: boolean,
graphLoadError: boolean,
fetchInstances: () => void; fetchInstances: () => void;
fetchGraph: () => void; fetchGraph: () => void;
} }
@ -34,7 +36,7 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
let body = <div />; let body = <div />;
if (this.props.isLoadingInstances || this.props.isLoadingGraph) { if (this.props.isLoadingInstances || this.props.isLoadingGraph) {
body = this.loadingState("Loading..."); body = this.loadingState("Loading...");
} else if (!!this.props.graph) { } else {
body = this.graphState(); body = this.graphState();
} }
return ( return (
@ -58,19 +60,20 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
} }
private load = () => { private load = () => {
if (!this.props.instances && !this.props.isLoadingInstances) { if (!this.props.instances && !this.props.isLoadingInstances && !this.props.graphLoadError) {
this.props.fetchInstances(); this.props.fetchInstances();
} }
if (!this.props.graph && !this.props.isLoadingGraph) { if (!this.props.graph && !this.props.isLoadingGraph && !this.props.graphLoadError) {
this.props.fetchGraph(); this.props.fetchGraph();
} }
} }
private graphState = () => { private graphState = () => {
const content = this.props.graphLoadError ? <ErrorState /> : <Graph />
return ( return (
<div> <div>
<Sidebar /> <Sidebar />
<Graph /> {content}
</div> </div>
) )
} }
@ -123,6 +126,7 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => ({
graph: state.data.graph, graph: state.data.graph,
graphLoadError: state.data.error,
instances: state.data.instances, instances: state.data.instances,
isLoadingGraph: state.data.isLoadingGraph, isLoadingGraph: state.data.isLoadingGraph,
isLoadingInstances: state.data.isLoadingInstances, isLoadingInstances: state.data.isLoadingInstances,

Wyświetl plik

@ -0,0 +1,12 @@
import { NonIdealState } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import * as React from 'react';
const ErrorState: React.SFC = () => (
<NonIdealState
icon={IconNames.ERROR}
title={"Something went wrong."}
/>
)
export default ErrorState;

Wyświetl plik

@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma'; import { Sigma, SigmaEnableWebGL, Filter, ForceAtlas2 } from 'react-sigma';
import { selectAndLoadInstance } from '../redux/actions'; import { selectAndLoadInstance } from '../redux/actions';
import ErrorState from './ErrorState';
const STYLE = { const STYLE = {
bottom: "0", bottom: "0",
@ -11,10 +12,12 @@ const STYLE = {
right: "0", right: "0",
top: "50px", top: "50px",
} }
const DEFAULT_NODE_COLOR = "#CED9E0";
const SELECTED_NODE_COLOR = "#48AFF0";
const SETTINGS = { const SETTINGS = {
defaultEdgeColor: "#5C7080", defaultEdgeColor: "#5C7080",
defaultLabelColor: "#F5F8FA", defaultLabelColor: "#F5F8FA",
defaultNodeColor: "#CED9E0", defaultNodeColor: DEFAULT_NODE_COLOR,
drawEdges: true, drawEdges: true,
drawLabels: true, drawLabels: true,
edgeColor: "default", edgeColor: "default",
@ -26,11 +29,15 @@ const SETTINGS = {
class GraphImpl extends React.Component { class GraphImpl extends React.Component {
constructor(props) {
super(props);
this.sigmaComponent = React.createRef();
}
render() { render() {
let graph = this.props.graph; let graph = this.props.graph;
if (!graph) { if (!graph) {
// TODO: error state return <ErrorState />;
return null;
} }
// Check that all nodes have size & coordinates; otherwise the graph will look messed up // Check that all nodes have size & coordinates; otherwise the graph will look messed up
const lengthBeforeFilter = graph.nodes.length; const lengthBeforeFilter = graph.nodes.length;
@ -38,6 +45,7 @@ class GraphImpl extends React.Component {
if (graph.nodes.length !== lengthBeforeFilter) { if (graph.nodes.length !== lengthBeforeFilter) {
// tslint:disable-next-line:no-console // 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)); console.error("Some nodes were missing details: " + this.props.graph.nodes.filter(n => !n.size || !n.x || !n.y).map(n => n.label));
return <ErrorState />;
} }
return ( return (
<Sigma <Sigma
@ -45,21 +53,40 @@ class GraphImpl extends React.Component {
renderer="webgl" renderer="webgl"
settings={SETTINGS} settings={SETTINGS}
style={STYLE} style={STYLE}
onClickNode={(e) => this.props.selectAndLoadInstance(e.data.node.label)} onClickNode={this.onClickNode}
onClickStage={this.onClickStage} onClickStage={this.onClickStage}
ref={this.sigmaComponent}
> >
<Filter neighborsOf={this.props.currentInstanceName} /> <Filter neighborsOf={this.props.currentInstanceName} />
<ForceAtlas2 iterationsPerRender={1} timeout={6000}/> <ForceAtlas2 iterationsPerRender={1} timeout={10000}/>
</Sigma> </Sigma>
) )
} }
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) => { onClickStage = (e) => {
// Deselect the instance (unless this was a drag event) // Deselect the instance (unless this was a drag event)
if (!e.data.captor.isDragging) { if (!e.data.captor.isDragging) {
this.props.selectAndLoadInstance(null); 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) => ({ const mapStateToProps = (state) => ({

Wyświetl plik

@ -13,10 +13,12 @@ import { IconNames } from '@blueprintjs/icons';
import { selectAndLoadInstance } from '../redux/actions'; import { selectAndLoadInstance } from '../redux/actions';
import { IAppState, IGraph, IInstanceDetails } from '../redux/types'; import { IAppState, IGraph, IInstanceDetails } from '../redux/types';
import ErrorState from './ErrorState';
interface ISidebarProps { interface ISidebarProps {
graph?: IGraph, graph?: IGraph,
instanceName: string | null, instanceName: string | null,
instanceLoadError: boolean,
instanceDetails: IInstanceDetails | null, instanceDetails: IInstanceDetails | null,
isLoadingInstanceDetails: boolean; isLoadingInstanceDetails: boolean;
selectAndLoadInstance: (instanceName: string) => void; selectAndLoadInstance: (instanceName: string) => void;
@ -64,6 +66,8 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
return this.renderPersonalInstanceErrorState(); return this.renderPersonalInstanceErrorState();
} else if (this.props.instanceDetails.status !== 'success') { } else if (this.props.instanceDetails.status !== 'success') {
return this.renderMissingDataState(); return this.renderMissingDataState();
} else if (this.props.instanceLoadError) {
return <ErrorState />;
} }
return ( return (
<div> <div>
@ -292,6 +296,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
const mapStateToProps = (state: IAppState) => ({ const mapStateToProps = (state: IAppState) => ({
graph: state.data.graph, graph: state.data.graph,
instanceDetails: state.currentInstance.currentInstanceDetails, instanceDetails: state.currentInstance.currentInstanceDetails,
instanceLoadError: state.currentInstance.error,
instanceName: state.currentInstance.currentInstanceName, instanceName: state.currentInstance.currentInstanceName,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails, isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
}); });

Wyświetl plik

@ -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) => { export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
return { return {
payload: instanceDetails, payload: instanceDetails,
@ -52,16 +64,15 @@ export const receiveInstanceDetails = (instanceDetails: IInstanceDetails) => {
/** Async actions: https://redux.js.org/advanced/asyncactions */ /** Async actions: https://redux.js.org/advanced/asyncactions */
export const fetchInstances = () => { export const fetchInstances = () => {
// TODO: handle errors
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
dispatch(requestInstances()); dispatch(requestInstances());
return getFromApi("instances") return getFromApi("instances")
.then(instances => dispatch(receiveInstances(instances))); .then(instances => dispatch(receiveInstances(instances)))
.catch(e => dispatch(graphLoadFailed()));
} }
} }
export const selectAndLoadInstance = (instanceName: string) => { export const selectAndLoadInstance = (instanceName: string) => {
// TODO: handle errors
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
if (!instanceName) { if (!instanceName) {
dispatch(deselectInstance()); dispatch(deselectInstance());
@ -69,12 +80,12 @@ export const selectAndLoadInstance = (instanceName: string) => {
} }
dispatch(selectInstance(instanceName)); dispatch(selectInstance(instanceName));
return getFromApi("instances/" + instanceName) return getFromApi("instances/" + instanceName)
.then(details => dispatch(receiveInstanceDetails(details))); .then(details => dispatch(receiveInstanceDetails(details)))
.catch(e => dispatch(instanceLoadFailed()));
} }
} }
export const fetchGraph = () => { export const fetchGraph = () => {
// TODO: handle errors
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
dispatch(requestGraph()); dispatch(requestGraph());
return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")]) return Promise.all([getFromApi("graph/edges"), getFromApi("graph/nodes")])
@ -84,6 +95,7 @@ export const fetchGraph = () => {
nodes: responses[1], nodes: responses[1],
}; };
}) })
.then(graph => dispatch(receiveGraph(graph))); .then(graph => dispatch(receiveGraph(graph)))
.catch(e => dispatch(graphLoadFailed()));
} }
} }

Wyświetl plik

@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
import { ActionType, IAction, ICurrentInstanceState, IDataState } from './types'; import { ActionType, IAction, ICurrentInstanceState, IDataState } from './types';
const initialDataState = { const initialDataState = {
error: false,
isLoadingGraph: false, isLoadingGraph: false,
isLoadingInstances: false, isLoadingInstances: false,
} }
@ -31,6 +32,13 @@ const data = (state: IDataState = initialDataState, action: IAction) => {
graph: action.payload, graph: action.payload,
isLoadingGraph: false, isLoadingGraph: false,
}; };
case ActionType.GRAPH_LOAD_ERROR:
return {
...state,
error: true,
isLoadingGraph: false,
isLoadingInstances: false,
};
default: default:
return state; return state;
} }
@ -39,7 +47,8 @@ const data = (state: IDataState = initialDataState, action: IAction) => {
const initialCurrentInstanceState = { const initialCurrentInstanceState = {
currentInstanceDetails: null, currentInstanceDetails: null,
currentInstanceName: null, currentInstanceName: null,
isLoadingInstanceDetails: false error: false,
isLoadingInstanceDetails: false,
}; };
const currentInstance = (state = initialCurrentInstanceState , action: IAction): ICurrentInstanceState => { const currentInstance = (state = initialCurrentInstanceState , action: IAction): ICurrentInstanceState => {
switch (action.type) { switch (action.type) {
@ -61,6 +70,12 @@ const currentInstance = (state = initialCurrentInstanceState , action: IAction):
currentInstanceDetails: null, currentInstanceDetails: null,
currentInstanceName: null, currentInstanceName: null,
} }
case ActionType.INSTANCE_LOAD_ERROR:
return {
...state,
error: true,
isLoadingInstanceDetails: false,
};
default: default:
return state; return state;
} }

Wyświetl plik

@ -5,7 +5,9 @@ export enum ActionType {
REQUEST_GRAPH = 'REQUEST_GRAPH', REQUEST_GRAPH = 'REQUEST_GRAPH',
RECEIVE_GRAPH = 'RECEIVE_GRAPH', RECEIVE_GRAPH = 'RECEIVE_GRAPH',
RECEIVE_INSTANCE_DETAILS = 'RECEIVE_INSTANCE_DETAILS', 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 { export interface IAction {
@ -57,6 +59,7 @@ export interface ICurrentInstanceState {
currentInstanceDetails: IInstanceDetails | null, currentInstanceDetails: IInstanceDetails | null,
currentInstanceName: string | null, currentInstanceName: string | null,
isLoadingInstanceDetails: boolean, isLoadingInstanceDetails: boolean,
error: boolean,
} }
export interface IDataState { export interface IDataState {
@ -64,6 +67,7 @@ export interface IDataState {
graph?: IGraph, graph?: IGraph,
isLoadingInstances: boolean, isLoadingInstances: boolean,
isLoadingGraph: boolean, isLoadingGraph: boolean,
error: boolean,
} }
export interface IAppState { export interface IAppState {