Highlight selected instance (#21)

* improve error handling

* highlight currently selected instance
This commit is contained in:
Tao Bror Bojlén 2018-12-06 18:48:32 +00:00 committed by GitHub
parent 7182f14c74
commit d2335c8851
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 96 additions and 17 deletions

View file

@ -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<IAppProps, IAppLocalState> {
let body = <div />;
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<IAppProps, IAppLocalState> {
}
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 ? <ErrorState /> : <Graph />
return (
<div>
<Sidebar />
<Graph />
{content}
</div>
)
}
@ -123,6 +126,7 @@ class AppImpl extends React.Component<IAppProps, IAppLocalState> {
const mapStateToProps = (state: IAppState) => ({
graph: state.data.graph,
graphLoadError: state.data.error,
instances: state.data.instances,
isLoadingGraph: state.data.isLoadingGraph,
isLoadingInstances: state.data.isLoadingInstances,

View file

@ -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;

View file

@ -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 <ErrorState />;
}
// 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 <ErrorState />;
}
return (
<Sigma
@ -45,21 +53,40 @@ class GraphImpl extends React.Component {
renderer="webgl"
settings={SETTINGS}
style={STYLE}
onClickNode={(e) => this.props.selectAndLoadInstance(e.data.node.label)}
onClickNode={this.onClickNode}
onClickStage={this.onClickStage}
ref={this.sigmaComponent}
>
<Filter neighborsOf={this.props.currentInstanceName} />
<ForceAtlas2 iterationsPerRender={1} timeout={6000}/>
<ForceAtlas2 iterationsPerRender={1} timeout={10000}/>
</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) => {
// 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) => ({

View file

@ -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<ISidebarProps, ISidebarState> {
return this.renderPersonalInstanceErrorState();
} else if (this.props.instanceDetails.status !== 'success') {
return this.renderMissingDataState();
} else if (this.props.instanceLoadError) {
return <ErrorState />;
}
return (
<div>
@ -292,6 +296,7 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
const mapStateToProps = (state: IAppState) => ({
graph: state.data.graph,
instanceDetails: state.currentInstance.currentInstanceDetails,
instanceLoadError: state.currentInstance.error,
instanceName: state.currentInstance.currentInstanceName,
isLoadingInstanceDetails: state.currentInstance.isLoadingInstanceDetails,
});

View file

@ -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()));
}
}

View file

@ -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;
}

View file

@ -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 {