Skip to content

Commit

Permalink
Merge pull request #401 from amosproj/fix-graph-updates
Browse files Browse the repository at this point in the history
Fix graph updates
  • Loading branch information
CatoLeanTruetschel authored Jul 13, 2021
2 parents 37672c0 + 83fdd3d commit 2fc5a34
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 20 deletions.
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@types/react-dom": "^17.0.3",
"clsx": "^1.1.1",
"cypress": "^7.3.0",
"fast-equals": "^2.0.3",
"inversify": "^5.1.1",
"lru_map": "^0.4.1",
"notistack": "^1.0.9",
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/configureServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { EntityDetailsStateStore } from './stores/details/EntityDetailsStateStor
import { EntityDetailsStore } from './stores/details/EntityDetailsStore';
import { RoutingStateStore } from './stores/routing/RoutingStateStore';
import ChordDetailsStateStore from './stores/details/ChordDetailsStateStore';
import { GraphStateStore } from './stores/graph/GraphStateStore';

/**
* Configures all services in the frontend app.
Expand Down Expand Up @@ -85,4 +86,5 @@ export default function configureServices(container: Container): void {
container.bind(EntityDetailsStore).toSelf().inSingletonScope();
container.bind(RoutingStateStore).toSelf().inSingletonScope();
container.bind(ChordDetailsStateStore).toSelf().inSingletonScope();
container.bind(GraphStateStore).toSelf().inSingletonScope();
}
30 changes: 30 additions & 0 deletions frontend/src/stores/details/EntityDetailsStateStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import SimpleStore from '../SimpleStore';
import { EntityDetailsState } from './EntityDetailsState';
import { RoutingStateStore } from '../routing/RoutingStateStore';
import SearchSelectionStore from '../SearchSelectionStore';
import QueryResultStore from '../QueryResultStore';

@injectable()
export class EntityDetailsStateStore extends SimpleStore<EntityDetailsState> {
private routingStateStoreSubscription?: Subscription;
private searchSelectionStoreSubscription?: Subscription;
private queryResultStoreSubscription?: Subscription;

protected getInitialValue(): EntityDetailsState {
return { node: null, edge: null };
Expand All @@ -21,6 +23,9 @@ export class EntityDetailsStateStore extends SimpleStore<EntityDetailsState> {
@inject(SearchSelectionStore)
private readonly searchSelectionStore!: SearchSelectionStore;

@inject(QueryResultStore)
private readonly queryResultStore!: QueryResultStore;

public clear(): void {
this.setState(this.getInitialValue());
}
Expand All @@ -42,6 +47,10 @@ export class EntityDetailsStateStore extends SimpleStore<EntityDetailsState> {
this.searchSelectionStoreSubscription =
this.subscribeToSearchSelectionStore();
}

if (this.queryResultStoreSubscription == null) {
this.queryResultStoreSubscription = this.subscribeQueryResultStore();
}
}

private subscribeToRoutingStateStore(): Subscription {
Expand All @@ -67,6 +76,27 @@ export class EntityDetailsStateStore extends SimpleStore<EntityDetailsState> {
},
});
}

private subscribeQueryResultStore(): Subscription {
return this.queryResultStore.getState().subscribe({
next: (queryResult) => {
const state = this.getValue();
// If the current selection is a node
if (state.node !== null) {
// If the node is not part of the query-result
if (!queryResult.nodes.some((node) => node.id === state.node)) {
this.clear();
}
// If the current selection is an edge
} else if (state.edge !== null) {
// If the edge is not part of the query-result
if (!queryResult.edges.some((edge) => edge.id === state.edge)) {
this.clear();
}
}
},
});
}
}

export default EntityDetailsStateStore;
98 changes: 98 additions & 0 deletions frontend/src/stores/graph/GraphStateStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'reflect-metadata';
import { inject, injectable } from 'inversify';
import { Subscription } from 'rxjs';
import { GraphData } from 'react-graph-vis';
import { uuid } from 'uuidv4';
import { deepEqual } from 'fast-equals';
import SimpleStore from '../SimpleStore';
import QueryResultStore, { QueryResultMeta } from '../QueryResultStore';
import EntityStyleStore from '../colors/EntityStyleStore';
import convertQueryResult from '../../visualization/shared-ops/convertQueryResult';
import { EntityStyleProvider } from '../colors';
import { QueryResult } from '../../shared/queries';

export type UUID = string;

export interface GraphState {
graph: GraphData;
key: UUID;
/**
* If undefined: no shortest path queried.
* If false: shortest path queried but is not in result
* If true: shortest path queried and is in result.
*/
containsShortestPath?: boolean;
}

@injectable()
export class GraphStateStore extends SimpleStore<GraphState> {
protected getInitialValue(): GraphState {
return {
graph: {
nodes: [],
edges: [],
},
key: uuid(),
containsShortestPath: false,
};
}

private queryResultStoreSubscription?: Subscription;
private entityStyleStoreSubscription?: Subscription;

@inject(QueryResultStore)
private readonly queryResultStore!: QueryResultStore;

@inject(EntityStyleStore)
private readonly entityStyleStore!: EntityStyleStore;

protected ensureInit(): void {
if (this.queryResultStoreSubscription == null) {
this.queryResultStoreSubscription = this.subscribeQueryResultStore();
}

if (this.entityStyleStoreSubscription == null) {
this.entityStyleStoreSubscription = this.subscribeEntityStyleStore();
}
}

subscribeQueryResultStore(): Subscription {
return this.queryResultStore.getState().subscribe({
next: (queryResult) =>
this.update(queryResult, this.entityStyleStore.getValue()),
});
}

subscribeEntityStyleStore(): Subscription {
return this.entityStyleStore.getState().subscribe({
next: (styleProvider) =>
this.update(this.queryResultStore.getValue(), styleProvider),
});
}

update(
queryResult: QueryResult & QueryResultMeta,
styleProvider: EntityStyleProvider
): void {
const currentState = this.getValue();
const graph = convertQueryResult(queryResult, styleProvider);
const { containsShortestPath } = queryResult;
const updatedState = {
graph,
containsShortestPath,
key: uuid(),
};

// TODO: Do we need to update the render-token if containsShortestPath changed?
if (deepEqual(graph, currentState.graph)) {
// Make sure that the object are the same instance such that
// the graph-vis library does not update the graph component.
updatedState.graph = currentState.graph;
updatedState.key = currentState.key;
}

this.setState(updatedState);
}
}

export default GraphStateStore;
40 changes: 20 additions & 20 deletions frontend/src/visualization/pages/Graph.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/* istanbul ignore file */
import React, { useRef, useEffect } from 'react';
import VisGraph, { EventParameters, GraphEvents } from 'react-graph-vis';
import { uuid } from 'uuidv4';
import { map, tap } from 'rxjs/operators';
import { tap } from 'rxjs/operators';
import { combineLatest } from 'rxjs';
import { useSnackbar } from 'notistack';
import useStylesVisualization from './useStylesVisualization';
Expand All @@ -13,11 +14,10 @@ import QueryResultStore from '../../stores/QueryResultStore';
import SearchSelectionStore from '../../stores/SearchSelectionStore';
import useObservable from '../../utils/useObservable';
import { isEntitySelected } from '../../stores/colors/EntityStyleProviderImpl';
import convertQueryResult from '../shared-ops/convertQueryResult';
import EntityStyleStore from '../../stores/colors/EntityStyleStore';
import GraphDetails from './GraphDetails';
import { EntityDetailsStateStore } from '../../stores/details/EntityDetailsStateStore';
import { EntityDetailsStore } from '../../stores/details/EntityDetailsStore';
import GraphStateStore from '../../stores/graph/GraphStateStore';

/**
* Keys for the snackbar notifications.
Expand All @@ -42,17 +42,19 @@ function Graph(props: GraphProps): JSX.Element {

const detailsStateStore = useService(EntityDetailsStateStore);
const queryResultStore = useService(QueryResultStore);
const entityColorStore = useService(EntityStyleStore);
const searchSelectionStore = useService(SearchSelectionStore);

const graphData = useObservable(
const graphStateStore = useService(GraphStateStore);
const graphState = useObservable(
graphStateStore.getState(),
graphStateStore.getValue()
);
// eslint-disable-next-line spaced-comment
useObservable(
// When one emits, the whole observable emits with the last emitted value from the other inputs
// Example: New query result comes in => emits it with the most recent values from entityColorStore
combineLatest([
queryResultStore.getState(),
entityColorStore.getState(),
]).pipe(
tap(([queryResult]) => {
queryResultStore.getState().pipe(
tap((queryResult) => {
closeSnackbar(SNACKBAR_KEYS.SHORTEST_PATH_NOT_FOUND);
if (queryResult.containsShortestPath === false) {
// assign new random id to avoid strange ui glitches
Expand All @@ -65,12 +67,8 @@ function Graph(props: GraphProps): JSX.Element {
}
);
}
}),
map(([queryResult, styleProvider]) =>
convertQueryResult(queryResult, styleProvider)
)
),
{ edges: [], nodes: [] }
})
)
);

const detailsStore = useService(EntityDetailsStore);
Expand Down Expand Up @@ -170,23 +168,25 @@ function Graph(props: GraphProps): JSX.Element {
<GraphDetails />
<div className={classes.graphContainer} ref={graphRef}>
<VisGraph
graph={graphData}
graph={graphState.graph}
options={visGraphBuildOptions(
containerSize.width,
containerSize.height,
layout
)}
events={events}
key={uuid()}
key={graphState.key}
getNetwork={(network) => {
network.unselectAll();
if (details !== null) {
if (details.entityType === 'node') {
if (graphData.nodes.some((node) => node.id === details.id)) {
if (
graphState.graph.nodes.some((node) => node.id === details.id)
) {
network.selectNodes([details.id], true);
}
} else if (
graphData.edges.some((edge) => edge.id === details.id)
graphState.graph.edges.some((edge) => edge.id === details.id)
) {
network.selectEdges([details.id]);
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/visualization/pages/Schema.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* istanbul ignore file */
import React, { useEffect } from 'react';
import { map, tap } from 'rxjs/operators';
import { combineLatest } from 'rxjs';
Expand Down
5 changes: 5 additions & 0 deletions frontend/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7221,6 +7221,11 @@ fast-diff@^1.1.1, fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==

fast-equals@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.3.tgz#7039b0a039909f345a2ce53f6202a14e5f392efc"
integrity sha512-0EMw4TTUxsMDpDkCg0rXor2gsg+npVrMIHbEhvD0HZyIhUX6AktC/yasm+qKwfyswd06Qy95ZKk8p2crTo0iPA==

fast-glob@^3.1.1, fast-glob@^3.2.5:
version "3.2.5"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
Expand Down

0 comments on commit 2fc5a34

Please sign in to comment.