Skip to content

Commit

Permalink
Apply search as filter
Browse files Browse the repository at this point in the history
  • Loading branch information
davkal committed May 4, 2016
1 parent e95a8ac commit dcad43d
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 87 deletions.
31 changes: 31 additions & 0 deletions client/app/scripts/actions/app-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ActionTypes from '../constants/action-types';
import { saveGraph } from '../utils/file-utils';
import { modulo } from '../utils/math-utils';
import { updateRoute } from '../utils/router-utils';
import { parseQuery } from '../utils/search-utils';
import { bufferDeltaUpdate, resumeUpdate,
resetUpdateBuffer } from '../utils/update-buffer-utils';
import { doControlRequest, getAllNodes, getNodesDelta, getNodeDetails,
Expand Down Expand Up @@ -69,6 +70,20 @@ export function pinNextMetric(delta) {
};
}

export function pinSearch(query) {
return {
type: ActionTypes.PIN_SEARCH,
query
};
}

export function unpinSearch(query) {
return {
type: ActionTypes.UNPIN_SEARCH,
query
};
}

export function blurSearch() {
return { type: ActionTypes.BLUR_SEARCH };
}
Expand Down Expand Up @@ -301,6 +316,22 @@ export function focusSearch() {
};
}

export function hitEnter() {
return (dispatch, getState) => {
const state = getState();
// pin query based on current search field
if (state.get('searchFocused')) {
const query = state.get('searchQuery');
if (query && parseQuery(query)) {
dispatch({
type: ActionTypes.PIN_SEARCH,
query
});
}
}
};
}

export function hitEsc() {
return (dispatch, getState) => {
const state = getState();
Expand Down
8 changes: 5 additions & 3 deletions client/app/scripts/charts/nodes-chart-edges.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import EdgeContainer from './edge-container';

class NodesChartEdges extends React.Component {
render() {
const { hasSelectedNode, highlightedEdgeIds, layoutEdges, layoutPrecision,
searchNodeMatches = makeMap(), searchQuery, selectedNodeId } = this.props;
const { hasPinnedSearches, hasSelectedNode, highlightedEdgeIds, layoutEdges,
layoutPrecision, searchNodeMatches = makeMap(), searchQuery,
selectedNodeId } = this.props;

return (
<g className="nodes-chart-edges">
{layoutEdges.toIndexedSeq().map(edge => {
const sourceSelected = selectedNodeId === edge.get('source');
const targetSelected = selectedNodeId === edge.get('target');
const blurred = hasSelectedNode && !sourceSelected && !targetSelected
|| searchQuery && !(searchNodeMatches.has(edge.get('source'))
|| (hasPinnedSearches || searchQuery) && !(searchNodeMatches.has(edge.get('source'))
&& searchNodeMatches.has(edge.get('target')));
const focused = hasSelectedNode && (sourceSelected || targetSelected);

Expand All @@ -42,6 +43,7 @@ class NodesChartEdges extends React.Component {
function mapStateToProps(state) {
const currentTopologyId = state.get('currentTopologyId');
return {
hasPinnedSearches: state.get('pinnedSearches').size > 0,
hasSelectedNode: hasSelectedNodeFn(state),
highlightedEdgeIds: state.get('highlightedEdgeIds'),
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
Expand Down
11 changes: 6 additions & 5 deletions client/app/scripts/charts/nodes-chart-nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import NodeContainer from './node-container';

class NodesChartNodes extends React.Component {
render() {
const { adjacentNodes, highlightedNodeIds, layoutNodes, layoutPrecision,
nodeScale, scale, searchNodeMatches = makeMap(), searchQuery,
selectedMetric, selectedNodeScale,
selectedNodeId, topCardNode } = this.props;
const { adjacentNodes, hasPinnedSearches, highlightedNodeIds, layoutNodes,
layoutPrecision, nodeScale, scale, searchNodeMatches = makeMap(),
searchQuery, selectedMetric, selectedNodeScale, selectedNodeId,
topCardNode } = this.props;

const zoomScale = scale;

Expand All @@ -22,7 +22,7 @@ class NodesChartNodes extends React.Component {
|| (adjacentNodes && adjacentNodes.includes(node.get('id')))));
const setBlurred = node => node.set('blurred',
selectedNodeId && !node.get('focused')
|| searchQuery && !searchNodeMatches.has(node.get('id')));
|| (hasPinnedSearches || searchQuery) && !searchNodeMatches.has(node.get('id')));

// make sure blurred nodes are in the background
const sortNodes = node => {
Expand Down Expand Up @@ -83,6 +83,7 @@ function mapStateToProps(state) {
const currentTopologyId = state.get('currentTopologyId');
return {
adjacentNodes: getAdjacentNodes(state),
hasPinnedSearches: state.get('pinnedSearches').size > 0,
highlightedNodeIds: state.get('highlightedNodeIds'),
selectedMetric: state.get('selectedMetric'),
selectedNodeId: state.get('selectedNodeId'),
Expand Down
19 changes: 8 additions & 11 deletions client/app/scripts/charts/nodes-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,17 +198,14 @@ class NodesChart extends React.Component {
if (!edges.has(edgeId)) {
const source = edge[0];
const target = edge[1];

if (!stateNodes.has(source) || !stateNodes.has(target)) {
log('Missing edge node', edge[0], edge[1]);
if (stateNodes.has(source) && stateNodes.has(target)) {
edges = edges.set(edgeId, makeMap({
id: edgeId,
value: 1,
source,
target
}));
}

edges = edges.set(edgeId, makeMap({
id: edgeId,
value: 1,
source,
target
}));
}
});
}
Expand Down Expand Up @@ -404,7 +401,7 @@ function mapStateToProps(state) {
return {
adjacentNodes: getAdjacentNodes(state),
forceRelayout: state.get('forceRelayout'),
nodes: state.get('nodes'),
nodes: state.get('nodes').filter(node => !node.get('filtered')),
selectedNodeId: state.get('selectedNodeId'),
topologyId: state.get('topologyId'),
topologyOptions: getActiveTopologyOptions(state)
Expand Down
5 changes: 4 additions & 1 deletion client/app/scripts/components/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Status from './status.js';
import Topologies from './topologies.js';
import TopologyOptions from './topology-options.js';
import { getApiDetails, getTopologies } from '../utils/web-api-utils';
import { pinNextMetric, hitEsc, unpinMetric,
import { pinNextMetric, hitEnter, hitEsc, unpinMetric,
selectMetric, toggleHelp } from '../actions/app-actions';
import Details from './details';
import Nodes from './nodes';
Expand All @@ -23,6 +23,7 @@ import DebugToolbar, { showingDebugToolbar,
import { getUrlState } from '../utils/router-utils';
import { getActiveTopologyOptions } from '../utils/topology-utils';

const ENTER_KEY_CODE = 13;
const ESC_KEY_CODE = 27;
const keyPressLog = debug('scope:app-key-press');

Expand Down Expand Up @@ -55,6 +56,8 @@ class App extends React.Component {
// don't get esc in onKeyPress
if (ev.keyCode === ESC_KEY_CODE) {
this.props.dispatch(hitEsc());
} else if (ev.keyCode === ENTER_KEY_CODE) {
this.props.dispatch(hitEnter());
}
}

Expand Down
28 changes: 28 additions & 0 deletions client/app/scripts/components/search-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { connect } from 'react-redux';

import { unpinSearch } from '../actions/app-actions';

class SearchItem extends React.Component {

constructor(props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}

handleClick(ev) {
ev.preventDefault();
this.props.unpinSearch(this.props.query);
}

render() {
return (
<span className="search-item">
<span className="search-item-label">{this.props.query}</span>
<span className="search-item-icon fa fa-close" onClick={this.handleClick} />
</span>
);
}
}

export default connect(null, { unpinSearch })(SearchItem);
32 changes: 22 additions & 10 deletions client/app/scripts/components/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import _ from 'lodash';
import { blurSearch, doSearch, focusSearch } from '../actions/app-actions';
import { slugify } from '../utils/string-utils';
import { isTopologyEmpty } from '../utils/topology-utils';
import SearchItem from './search-item';

// dynamic hint based on node names
function getHint(nodes) {
Expand All @@ -23,7 +24,8 @@ function getHint(nodes) {
}
}

return `Try "${label}" or "${metadataLabel}:${metadataValue}".`;
return `Try "${label}" or "${metadataLabel}:${metadataValue}".
Hit enter to apply the search as a filter.`;
}

class Search extends React.Component {
Expand Down Expand Up @@ -65,14 +67,17 @@ class Search extends React.Component {
}

render() {
const inputId = this.props.inputId || 'search';
const disabled = this.props.isTopologyEmpty || !this.props.topologiesLoaded;
const matchCount = this.props.searchNodeMatches
const { inputId = 'search', nodes, pinnedSearches, searchFocused,
searchNodeMatches, topologiesLoaded } = this.props;
const disabled = this.props.isTopologyEmpty || !topologiesLoaded;
const matchCount = searchNodeMatches
.reduce((count, topologyMatches) => count + topologyMatches.size, 0);
const showPinnedSearches = pinnedSearches.size > 0;
const classNames = cx('search', {
'search-pinned': showPinnedSearches,
'search-matched': matchCount,
'search-filled': this.state.value,
'search-focused': this.props.searchFocused,
'search-focused': searchFocused,
'search-disabled': disabled
});
const title = matchCount ? `${matchCount} matches` : null;
Expand All @@ -81,16 +86,22 @@ class Search extends React.Component {
<div className="search-wrapper">
<div className={classNames} title={title}>
<div className="search-input">
<i className="fa fa-search search-input-icon"></i>
<label className="search-input-label" htmlFor={inputId}>
Search
</label>
{showPinnedSearches && <span className="search-input-items">
{pinnedSearches.toIndexedSeq()
.map(query => <SearchItem query={query} key={query} />)}
</span>}
<input className="search-input-field" type="text" id={inputId}
value={this.state.value} onChange={this.handleChange}
onBlur={this.handleBlur} onFocus={this.handleFocus}
disabled={disabled} />
<label className="search-input-label" htmlFor={inputId}>
<i className="fa fa-search search-input-label-icon"></i>
<span className="search-input-label-text">Search</span>
</label>
</div>
<div className="search-hint">{getHint(this.props.nodes)}</div>
{!showPinnedSearches && <div className="search-hint">
{getHint(nodes)}
</div>}
</div>
</div>
);
Expand All @@ -101,6 +112,7 @@ export default connect(
state => ({
nodes: state.get('nodes'),
isTopologyEmpty: isTopologyEmpty(state),
pinnedSearches: state.get('pinnedSearches'),
searchFocused: state.get('searchFocused'),
searchQuery: state.get('searchQuery'),
searchNodeMatches: state.get('searchNodeMatches'),
Expand Down
3 changes: 3 additions & 0 deletions client/app/scripts/constants/action-types.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import _ from 'lodash';

const ACTION_TYPES = [
'ADD_QUERY_FILTER',
'BLUR_SEARCH',
'CHANGE_TOPOLOGY_OPTION',
'CLEAR_CONTROL_ERROR',
Expand Down Expand Up @@ -28,7 +29,9 @@ const ACTION_TYPES = [
'LEAVE_EDGE',
'LEAVE_NODE',
'PIN_METRIC',
'PIN_SEARCH',
'UNPIN_METRIC',
'UNPIN_SEARCH',
'OPEN_WEBSOCKET',
'RECEIVE_CONTROL_NODE_REMOVED',
'RECEIVE_CONTROL_PIPE',
Expand Down
19 changes: 18 additions & 1 deletion client/app/scripts/reducers/root.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fromJS, is as isDeepEqual, List as makeList, Map as makeMap,

import ActionTypes from '../constants/action-types';
import { EDGE_ID_SEPARATOR } from '../constants/naming';
import { updateNodeMatches } from '../utils/search-utils';
import { applyPinnedSearches, updateNodeMatches } from '../utils/search-utils';
import { findTopologyById, getAdjacentNodes, setTopologyUrlsById,
updateTopologyIds, filterHiddenTopologies } from '../utils/topology-utils';

Expand Down Expand Up @@ -55,6 +55,7 @@ export const initialState = makeMap({
// allows us to keep the same metric "type" selected when the topology changes.
pinnedMetricType: null,
plugins: makeList(),
pinnedSearches: makeList(), // list of node filters
routeSet: false,
searchFocused: false,
searchNodeMatches: makeMap(),
Expand Down Expand Up @@ -398,6 +399,13 @@ export function rootReducer(state = initialState, action) {
return state.set('searchFocused', true);
}

case ActionTypes.PIN_SEARCH: {
state = state.set('searchQuery', '');
const pinnedSearches = state.get('pinnedSearches');
state = state.setIn(['pinnedSearches', pinnedSearches.size], action.query);
return applyPinnedSearches(state);
}

case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
return closeNodeDetails(state, action.nodeId);
}
Expand Down Expand Up @@ -476,6 +484,9 @@ export function rootReducer(state = initialState, action) {
state = state.setIn(['nodes', node.id], fromJS(makeNode(node)));
});

// apply pinned searches, filters nodes that dont match
state = applyPinnedSearches(state);

state = state.set('availableCanvasMetrics', state.get('nodes')
.valueSeq()
.flatMap(n => (n.get('metrics') || makeList()).map(m => (
Expand Down Expand Up @@ -578,6 +589,12 @@ export function rootReducer(state = initialState, action) {
return state;
}

case ActionTypes.UNPIN_SEARCH: {
const pinnedSearches = state.get('pinnedSearches').filter(query => query !== action.query);
state = state.set('pinnedSearches', pinnedSearches);
return applyPinnedSearches(state);
}

default: {
return state;
}
Expand Down
Loading

0 comments on commit dcad43d

Please sign in to comment.