highlight hovered search result

This commit is contained in:
Tao Bror Bojlén 2019-07-27 00:27:46 +03:00
parent a215b7e8b0
commit bdf1f12a1c
No known key found for this signature in database
GPG Key ID: C6EC7AAB905F9E6F
11 changed files with 99 additions and 16 deletions

View File

@ -11,6 +11,7 @@ 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.
- When you hover a search result, it is now highlighted on the graph.
### Changed

View File

@ -20,6 +20,7 @@ config :backend, BackendWeb.Endpoint,
config :backend, Backend.Repo, queue_target: 5000
config :backend, Backend.Elasticsearch.Cluster,
url: "http://localhost:9200",
api: Elasticsearch.API.HTTP,
json_library: Jason,
indexes: %{

View File

@ -56,8 +56,6 @@ config :backend, Backend.Repo,
hostname: "localhost",
pool_size: 10
config :backend, Backend.ElasticsearchCluster, url: "http://localhost:9200"
config :backend, :crawler,
status_age_limit_days: 28,
status_count_limit: 100,

View File

@ -3,7 +3,7 @@ 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, QUALITATIVE_COLOR_SCHEME, SELECTED_NODE_COLOR } from "../../constants";
import { DEFAULT_NODE_COLOR, HOVERED_NODE_COLOR, QUALITATIVE_COLOR_SCHEME, SELECTED_NODE_COLOR } from "../../constants";
import { IColorSchemeType } from "../../types";
const CytoscapeContainer = styled.div`
@ -16,6 +16,7 @@ interface ICytoscapeProps {
colorScheme?: IColorSchemeType;
currentNodeId: string | null;
elements: cytoscape.ElementsDefinition;
hoveringOver?: string;
navigateToInstancePath?: (domain: string) => void;
navigateToRoot?: () => void;
}
@ -128,6 +129,9 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
if (prevProps.colorScheme !== this.props.colorScheme) {
this.updateColorScheme();
}
if (prevProps.hoveringOver !== this.props.hoveringOver) {
this.updateHoveredNodeClass(prevProps.hoveringOver);
}
}
public componentWillUnmount() {
@ -187,7 +191,7 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
if (!style) {
style = this.cy!.style() as any;
}
style
style = style
.selector("node")
.style({
"background-color": DEFAULT_NODE_COLOR,
@ -202,6 +206,23 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
.selector("node:selected")
.style({
"background-color": SELECTED_NODE_COLOR
});
this.setHoveredNodeColorScheme(style);
};
/**
* We always want to set node hover styled at the end of a style change to make sure they don't get overwritten.
*/
private setHoveredNodeColorScheme = (style?: any) => {
if (!style) {
style = this.cy!.style() as any;
}
style
.selector("node.hovered")
.style({
"border-color": HOVERED_NODE_COLOR,
"border-width": 1000
})
.update();
};
@ -220,10 +241,30 @@ class Cytoscape extends React.PureComponent<ICytoscapeProps> {
"background-color": QUALITATIVE_COLOR_SCHEME[idx]
});
});
style
.selector("node:selected")
.style({ "background-color": SELECTED_NODE_COLOR })
.update();
style = style.selector("node:selected").style({ "background-color": SELECTED_NODE_COLOR });
this.setHoveredNodeColorScheme(style);
}
};
/**
* This function sets the hover class on the node that's currently being hovered over in the search results
* (and removes it from the previous one if there was one).
*
* We explicitly pass the ID of the previously hovered node, rather than just using a class selector.
* This is because lookups by ID are significantly faster than class selectors.
*/
private updateHoveredNodeClass = (prevHoveredId?: string) => {
if (!this.cy) {
throw new Error("Expected cytoscape, but there wasn't one!");
}
const { hoveringOver } = this.props;
if (!!prevHoveredId) {
this.cy.$id(prevHoveredId).removeClass("hovered");
}
if (!!hoveringOver) {
this.cy.$id(hoveringOver).addClass("hovered");
}
};
}

View File

@ -35,8 +35,10 @@ const StyledDescription = styled.div`
interface ISearchResultProps {
result: ISearchResultInstance;
onClick: () => void;
onMouseEnter: () => void;
onMouseLeave: () => void;
}
const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick }) => {
const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick, onMouseEnter, onMouseLeave }) => {
let shortenedDescription;
if (result.description) {
shortenedDescription = result.description && sanitize(result.description);
@ -55,7 +57,14 @@ const SearchResult: React.FC<ISearchResultProps> = ({ result, onClick }) => {
}
return (
<StyledCard elevation={Elevation.ONE} interactive={true} key={result.name} onClick={onClick}>
<StyledCard
elevation={Elevation.ONE}
interactive={true}
key={result.name}
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<StyledHeadingContainer>
<StyledH4>{result.name}</StyledH4>
{typeIcon}

View File

@ -20,6 +20,7 @@ interface IGraphProps {
fetchGraph: () => void;
graph?: IGraph;
graphLoadError: boolean;
hoveringOverResult?: string;
isLoadingGraph: boolean;
navigate: (path: string) => void;
}
@ -52,6 +53,7 @@ class GraphImpl extends React.PureComponent<IGraphProps, IGraphState> {
colorScheme={this.state.colorScheme}
currentNodeId={this.props.currentInstanceName}
elements={this.props.graph}
hoveringOver={this.props.hoveringOverResult}
navigateToInstancePath={this.navigateToInstancePath}
navigateToRoot={this.navigateToRoot}
ref={this.cytoscapeComponent}
@ -99,6 +101,7 @@ const mapStateToProps = (state: IAppState) => {
currentInstanceName: match && match.params.domain,
graph: state.data.graph,
graphLoadError: state.data.error,
hoveringOverResult: state.search.hoveringOverResult,
isLoadingGraph: state.data.isLoadingGraph
};
};

View File

@ -5,7 +5,7 @@ import React from "react";
import { connect } from "react-redux";
import { Dispatch } from "redux";
import styled from "styled-components";
import { updateSearch } from "../../redux/actions";
import { setResultHover, updateSearch } from "../../redux/actions";
import { IAppState, ISearchResultInstance } from "../../redux/types";
import { SearchResult } from "../molecules";
@ -34,6 +34,7 @@ interface ISearchScreenProps {
results: ISearchResultInstance[];
handleSearch: (query: string) => void;
navigateToInstance: (domain: string) => void;
setIsHoveringOver: (domain?: string) => void;
}
interface ISearchScreenState {
currentQuery: string;
@ -72,7 +73,13 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
{this.renderSearchBar()}
<SearchResults>
{results.map(result => (
<SearchResult result={result} key={result.name} onClick={this.selectInstanceFactory(result.name)} />
<SearchResult
result={result}
key={result.name}
onClick={this.selectInstanceFactory(result.name)}
onMouseEnter={this.onMouseEnterFactory(result.name)}
onMouseLeave={this.onMouseLeave}
/>
))}
{isLoadingResults && <StyledSpinner size={Spinner.SIZE_SMALL} />}
{!isLoadingResults && hasMoreResults && (
@ -103,6 +110,14 @@ class SearchScreen extends React.PureComponent<ISearchScreenProps, ISearchScreen
this.props.navigateToInstance(domain);
};
private onMouseEnterFactory = (domain: string) => () => {
this.props.setIsHoveringOver(domain);
};
private onMouseLeave = () => {
this.props.setIsHoveringOver(undefined);
};
private renderSearchBar = () => (
<SearchBarContainer className={`${Classes.INPUT_GROUP} ${Classes.LARGE}`}>
<span className={`${Classes.ICON} bp3-icon-${IconNames.SEARCH}`} />
@ -128,7 +143,8 @@ const mapStateToProps = (state: IAppState) => ({
});
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleSearch: (query: string) => dispatch(updateSearch(query) as any),
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`))
navigateToInstance: (domain: string) => dispatch(push(`/instance/${domain}`)),
setIsHoveringOver: (domain?: string) => dispatch(setResultHover(domain))
});
export default connect(
mapStateToProps,

View File

@ -3,6 +3,7 @@ export const DESKTOP_WIDTH_THRESHOLD = 1000;
export const DEFAULT_NODE_COLOR = "#CED9E0";
export const SELECTED_NODE_COLOR = "#48AFF0";
export const HOVERED_NODE_COLOR = "#FFB366";
// From https://blueprintjs.com/docs/#core/colors.qualitative-color-schemes
export const QUALITATIVE_COLOR_SCHEME = [

View File

@ -71,6 +71,13 @@ const resetSearch = () => {
};
};
export const setResultHover = (domain?: string) => {
return {
payload: domain,
type: ActionType.SET_SEARCH_RESULT_HOVER
};
};
/** Async actions: https://redux.js.org/advanced/asyncactions */
export const loadInstance = (instanceName: string | null) => {

View File

@ -109,6 +109,11 @@ const search = (state = initialSearchState, action: IAction): ISearchState => {
};
case ActionType.RESET_SEARCH:
return initialSearchState;
case ActionType.SET_SEARCH_RESULT_HOVER:
return {
...state,
hoveringOverResult: action.payload
};
default:
return state;
}

View File

@ -15,9 +15,9 @@ export enum ActionType {
REQUEST_SEARCH_RESULTS = "REQUEST_SEARCH_RESULTS",
RECEIVE_SEARCH_RESULTS = "RECEIVE_SEARCH_RESULTS",
SEARCH_RESULTS_ERROR = "SEARCH_RESULTS_ERROR",
RESET_SEARCH = "RESET_SEARCH"
// REQUEST_INSTANCES = "REQUEST_INSTANCES",
// RECEIVE_INSTANCES = "RECEIVE_INSTANCES",
RESET_SEARCH = "RESET_SEARCH",
// Search -- hovering over results
SET_SEARCH_RESULT_HOVER = "SET_SEARCH_RESULT_HOVER"
}
export interface IAction {
@ -102,6 +102,7 @@ export interface ISearchState {
next: string;
query: string;
results: ISearchResultInstance[];
hoveringOverResult?: string;
}
export interface IAppState {