Highlight selected instance (#21)
* improve error handling * highlight currently selected instance
This commit is contained in:
parent
7182f14c74
commit
d2335c8851
|
@ -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,
|
||||
|
|
12
frontend/src/components/ErrorState.tsx
Normal file
12
frontend/src/components/ErrorState.tsx
Normal 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;
|
|
@ -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) => ({
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue