highlight hovered search result
This commit is contained in:
parent
a215b7e8b0
commit
bdf1f12a1c
|
@ -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
|
||||
|
||||
|
|
|
@ -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: %{
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue