highlight search results on graph

This commit is contained in:
Tao Bror Bojlén 2019-07-27 01:25:57 +03:00
parent bdf1f12a1c
commit 7e55296a26
No known key found for this signature in database
GPG Key ID: C6EC7AAB905F9E6F
7 changed files with 75 additions and 37 deletions

View File

@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Instance administrators can now log in to opt in or out of crawling.
- Added ElasticSearch full-text search over instance domains and descriptions.
- Search results are now highlighted on the graph.
- When you hover a search result, it is now highlighted on the graph.
### Changed
- Instances are now crawled hourly instead of every 30 minutes.
- The colors for color coding have been made brighter (more visible against the dark background.
### Deprecated

View File

@ -102,7 +102,7 @@ defmodule Backend.Api do
end
def search_instances(query, from \\ 0) do
page_size = 10
page_size = 50
search_response =
Elasticsearch.post(Backend.Elasticsearch.Cluster, "/instances/_search", %{

View File

@ -47,7 +47,7 @@ const GraphKey: React.FC<IGraphKeyProps> = ({ current, colorSchemes, onItemSelec
<H6>Key</H6>
<ul className={Classes.LIST_UNSTYLED}>
{current.values.map(v => (
<StyledLi>
<StyledLi key={v}>
<InstanceType type={v} />
</StyledLi>
))}

View File

@ -1,9 +1,16 @@
import cytoscape from "cytoscape";
import { isEqual } from "lodash";
import * as React from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
import tippy, { Instance } from "tippy.js";
import { DEFAULT_NODE_COLOR, HOVERED_NODE_COLOR, QUALITATIVE_COLOR_SCHEME, SELECTED_NODE_COLOR } from "../../constants";
import {
DEFAULT_NODE_COLOR,
HOVERED_NODE_COLOR,
QUALITATIVE_COLOR_SCHEME,
SEARCH_RESULT_COLOR,
SELECTED_NODE_COLOR
} from "../../constants";
import { IColorSchemeType } from "../../types";
const CytoscapeContainer = styled.div`
@ -17,6 +24,7 @@ interface ICytoscapeProps {
currentNodeId: string | null;
elements: cytoscape.ElementsDefinition;
hoveringOver?: string;
searchResultIds?: string[];
navigateToInstancePath?: (domain: string) => void;
navigateToRoot?: () => void;
}
@ -132,6 +140,9 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
if (prevProps.hoveringOver !== this.props.hoveringOver) {
this.updateHoveredNodeClass(prevProps.hoveringOver);
}
if (!isEqual(prevProps.searchResultIds, this.props.searchResultIds)) {
this.updateSearchResultNodeClass();
}
}
public componentWillUnmount() {
@ -191,39 +202,44 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
if (!style) {
style = this.cy!.style() as any;
}
style = style
.selector("node")
.style({
"background-color": DEFAULT_NODE_COLOR,
// The size from the backend is log_10(userCount), which from 10 <= userCount <= 1,000,000 gives us the range
// 1-6. We map this to the range of sizes we want.
// TODO: I should probably check that that the backend is actually using log_10 and not log_e, but it look
// quite good as it is, so...
height: "mapData(size, 1, 6, 20, 200)",
label: "data(id)",
width: "mapData(size, 1, 6, 20, 200)"
})
.selector("node:selected")
.style({
"background-color": SELECTED_NODE_COLOR
});
style = style.selector("node").style({
"background-color": DEFAULT_NODE_COLOR,
// The size from the backend is log_10(userCount), which from 10 <= userCount <= 1,000,000 gives us the range
// 1-6. We map this to the range of sizes we want.
// TODO: I should probably check that that the backend is actually using log_10 and not log_e, but it look
// quite good as it is, so...
height: "mapData(size, 1, 6, 20, 200)",
label: "data(id)",
width: "mapData(size, 1, 6, 20, 200)"
});
this.setHoveredNodeColorScheme(style);
this.setNodeSearchColorScheme(style);
};
/**
* We always want to set node hover styled at the end of a style change to make sure they don't get overwritten.
* We always want to set node search/hover styles at the end of a style change to make sure they don't get overwritten.
*/
private setHoveredNodeColorScheme = (style?: any) => {
private setNodeSearchColorScheme = (style?: any) => {
if (!style) {
style = this.cy!.style() as any;
}
style
.selector("node.searchResult")
.style({
"background-color": SEARCH_RESULT_COLOR,
"border-color": SEARCH_RESULT_COLOR,
"border-opacity": 0.7,
"border-width": 250
})
.selector("node.hovered")
.style({
"border-color": HOVERED_NODE_COLOR,
"border-width": 1000
})
.selector("node:selected")
.style({
"background-color": SELECTED_NODE_COLOR
})
.update();
};
@ -241,9 +257,8 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
"background-color": QUALITATIVE_COLOR_SCHEME[idx]
});
});
style = style.selector("node:selected").style({ "background-color": SELECTED_NODE_COLOR });
this.setHoveredNodeColorScheme(style);
this.setNodeSearchColorScheme(style);
}
};
@ -267,6 +282,22 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
this.cy.$id(hoveringOver).addClass("hovered");
}
};
private updateSearchResultNodeClass = () => {
if (!this.cy) {
throw new Error("Expected cytoscape, but there wasn't one!");
}
const { searchResultIds } = this.props;
this.cy.batch(() => {
this.cy!.nodes().removeClass("searchResult");
if (!!searchResultIds && searchResultIds.length > 0) {
const currentResultSelector = searchResultIds.map(id => `node[id = "${id}"]`).join(", ");
this.cy!.$(currentResultSelector).addClass("searchResult");
}
});
};
}
export default Cytoscape;

View File

@ -22,6 +22,7 @@ interface IGraphProps {
graphLoadError: boolean;
hoveringOverResult?: string;
isLoadingGraph: boolean;
searchResultDomains: string[];
navigate: (path: string) => void;
}
interface IGraphState {
@ -56,6 +57,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
hoveringOver={this.props.hoveringOverResult}
navigateToInstancePath={this.navigateToInstancePath}
navigateToRoot={this.navigateToRoot}
searchResultIds={this.props.searchResultDomains}
ref={this.cytoscapeComponent}
/>
<GraphTools
@ -102,7 +104,8 @@ const mapStateToProps = (state: IAppState) => {
graph: state.data.graph,
graphLoadError: state.data.error,
hoveringOverResult: state.search.hoveringOverResult,
isLoadingGraph: state.data.isLoadingGraph
isLoadingGraph: state.data.isLoadingGraph,
searchResultDomains: state.search.results.map(r => r.name)
};
};
const mapDispatchToProps = (dispatch: Dispatch) => ({

View File

@ -107,6 +107,7 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
};
private selectInstanceFactory = (domain: string) => () => {
this.props.setIsHoveringOver(undefined);
this.props.navigateToInstance(domain);
};

View File

@ -3,20 +3,21 @@ export const DESKTOP_WIDTH_THRESHOLD = 1000;
export const DEFAULT_NODE_COLOR = "#CED9E0";
export const SELECTED_NODE_COLOR = "#48AFF0";
export const HOVERED_NODE_COLOR = "#FFB366";
export const SEARCH_RESULT_COLOR = "#AD99FF";
export const HOVERED_NODE_COLOR = SEARCH_RESULT_COLOR;
// From https://blueprintjs.com/docs/#core/colors.qualitative-color-schemes
// From https://blueprintjs.com/docs/#core/colors.qualitative-color-schemes, but brightened
export const QUALITATIVE_COLOR_SCHEME = [
"#2965CC",
"#29A634",
"#D99E0B",
"#D13913",
"#8F398F",
"#00B3A4",
"#DB2C6F",
"#9BBF30",
"#96622D",
"#7157D9"
"#669EFF",
"#62D96B",
"#FFC940",
"#FF6E4A",
"#C274C2",
"#2EE6D6",
"#FF66A1",
"#D1F26D",
"#C99765",
"#AD99FF"
];
export const INSTANCE_DOMAIN_PATH = "/instance/:domain";