Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix graph updates #401

Merged
merged 4 commits into from
Jul 13, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
39 changes: 19 additions & 20 deletions frontend/src/visualization/pages/Graph.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 +13,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 @@ -41,17 +40,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 @@ -64,12 +65,8 @@ function Graph(props: GraphProps): JSX.Element {
}
);
}
}),
map(([queryResult, styleProvider]) =>
convertQueryResult(queryResult, styleProvider)
)
),
{ edges: [], nodes: [] }
})
)
);

const detailsStore = useService(EntityDetailsStore);
Expand Down Expand Up @@ -141,23 +138,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
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