Skip to content

Commit

Permalink
Search all fields by default, gray out nodes if no match
Browse files Browse the repository at this point in the history
  • Loading branch information
davkal committed May 4, 2016
1 parent 1a7920c commit e95a8ac
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 66 deletions.
8 changes: 3 additions & 5 deletions client/app/scripts/charts/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ class Node extends React.Component {
const labelText = truncate ? ellipsis(label, 14, nodeScale(4 * scaleFactor)) : label;
const subLabelText = truncate ? ellipsis(subLabel, 12, nodeScale(4 * scaleFactor)) : subLabel;

let labelOffsetY = 18;
let subLabelOffsetY = 35;
let labelOffsetY = 8;
let labelFontSize = 14;
let subLabelFontSize = 12;

Expand All @@ -77,7 +76,6 @@ class Node extends React.Component {
labelFontSize /= zoomScale;
subLabelFontSize /= zoomScale;
labelOffsetY /= zoomScale;
subLabelOffsetY /= zoomScale;
}

const className = classNames({
Expand All @@ -98,11 +96,11 @@ class Node extends React.Component {
x={-nodeScale(scaleFactor * 0.5)}
y={-nodeScale(scaleFactor * 0.5)}
width={nodeScale(scaleFactor)}
height={nodeScale(scaleFactor) + subLabelOffsetY}
height={nodeScale(scaleFactor)}
/>
<foreignObject x={-nodeScale(2 * scaleFactor)}
y={labelOffsetY + nodeScale(0.5 * scaleFactor)}
width={nodeScale(scaleFactor * 4)} height={subLabelOffsetY}>
width={nodeScale(scaleFactor * 4)}>
<div className="node-label" style={{fontSize: labelFontSize}}>
<MatchedText text={labelText} matches={matches} fieldId="label" />
</div>
Expand Down
11 changes: 6 additions & 5 deletions client/app/scripts/charts/nodes-chart-edges.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import EdgeContainer from './edge-container';
class NodesChartEdges extends React.Component {
render() {
const { hasSelectedNode, highlightedEdgeIds, layoutEdges, layoutPrecision,
searchNodeMatches = makeMap(), selectedNodeId } = this.props;
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
|| searchNodeMatches.size > 0 && !(searchNodeMatches.has(edge.get('source'))
|| searchQuery && !(searchNodeMatches.has(edge.get('source'))
&& searchNodeMatches.has(edge.get('target')));
const focused = hasSelectedNode && (sourceSelected || targetSelected);

Expand All @@ -42,10 +42,11 @@ class NodesChartEdges extends React.Component {
function mapStateToProps(state) {
const currentTopologyId = state.get('currentTopologyId');
return {
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
hasSelectedNode: hasSelectedNodeFn(state),
selectedNodeId: state.get('selectedNodeId'),
highlightedEdgeIds: state.get('highlightedEdgeIds')
highlightedEdgeIds: state.get('highlightedEdgeIds'),
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
searchQuery: state.get('searchQuery'),
selectedNodeId: state.get('selectedNodeId')
};
}

Expand Down
6 changes: 4 additions & 2 deletions client/app/scripts/charts/nodes-chart-nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import NodeContainer from './node-container';
class NodesChartNodes extends React.Component {
render() {
const { adjacentNodes, highlightedNodeIds, layoutNodes, layoutPrecision,
nodeScale, scale, searchNodeMatches = makeMap(), selectedMetric, selectedNodeScale,
nodeScale, scale, searchNodeMatches = makeMap(), searchQuery,
selectedMetric, selectedNodeScale,
selectedNodeId, topCardNode } = this.props;

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

// make sure blurred nodes are in the background
const sortNodes = node => {
Expand Down Expand Up @@ -86,6 +87,7 @@ function mapStateToProps(state) {
selectedMetric: state.get('selectedMetric'),
selectedNodeId: state.get('selectedNodeId'),
searchNodeMatches: state.getIn(['searchNodeMatches', currentTopologyId]),
searchQuery: state.get('searchQuery'),
topCardNode: state.get('nodeDetails').last()
};
}
Expand Down
31 changes: 19 additions & 12 deletions client/app/scripts/components/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,26 @@ import cx from 'classnames';
import _ from 'lodash';

import { blurSearch, doSearch, focusSearch } from '../actions/app-actions';
import { slugify } from '../utils/string-utils';
import { isTopologyEmpty } from '../utils/topology-utils';

const SEARCH_HINTS = [
'Try "db" or "app1" to search by node label or sublabel.',
'Try "sublabel:my-host" to search by node sublabel.',
'Try "label:my-node" to search by node name.',
'Try "metadata:my-node" to search through all node metadata.',
'Try "dockerenv:my-env-value" to search through all docker environment variables.',
'Try "all:my-value" to search through all metdata and labels.'
];
// dynamic hint based on node names
function getHint(nodes) {
let label = 'mycontainer';
let metadataLabel = 'ip';
let metadataValue = '172.12';

// every minute different hint
function getHint() {
return SEARCH_HINTS[(new Date).getMinutes() % SEARCH_HINTS.length];
const node = nodes.last();
if (node) {
label = node.get('label');
if (node.get('metadata')) {
const metadataField = node.get('metadata').first();
metadataLabel = slugify(metadataField.get('label')).toLowerCase();
metadataValue = metadataField.get('value').toLowerCase();
}
}

return `Try "${label}" or "${metadataLabel}:${metadataValue}".`;
}

class Search extends React.Component {
Expand Down Expand Up @@ -84,7 +90,7 @@ class Search extends React.Component {
<span className="search-input-label-text">Search</span>
</label>
</div>
<div className="search-hint">{getHint()}</div>
<div className="search-hint">{getHint(this.props.nodes)}</div>
</div>
</div>
);
Expand All @@ -93,6 +99,7 @@ class Search extends React.Component {

export default connect(
state => ({
nodes: state.get('nodes'),
isTopologyEmpty: isTopologyEmpty(state),
searchFocused: state.get('searchFocused'),
searchQuery: state.get('searchQuery'),
Expand Down
78 changes: 36 additions & 42 deletions client/app/scripts/utils/search-utils.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,67 @@
import { Map as makeMap } from 'immutable';

import { slugify } from './string-utils';

// topolevel search fields
const SEARCH_FIELDS = makeMap({
label: 'label',
sublabel: 'label_minor'
});

// TODO make this dynamic based on topology
const SEARCH_TABLES = makeMap({
dockerlabel: 'docker_label_',
dockerenv: 'docker_env_',
});

const PREFIX_DELIMITER = ':';
const PREFIX_ALL = 'all';
const PREFIX_ALL_SHORT = 'a';
const PREFIX_METADATA = 'metadata';
const PREFIX_METADATA_SHORT = 'm';

function findNodeMatch(nodeMatches, keyPath, text, query, label) {
const index = text.indexOf(query);
if (index > -1) {
nodeMatches = nodeMatches.setIn(keyPath,
{text, label, matches: [{start: index, length: query.length}]});
function matchPrefix(label, prefix) {
if (label && prefix) {
return (new RegExp(prefix, 'i')).test(slugify(label));
}
return false;
}

function findNodeMatch(nodeMatches, keyPath, text, query, prefix, label) {
if (!prefix || matchPrefix(label, prefix)) {
const queryRe = new RegExp(query, 'i');
const matches = text.match(queryRe);
if (matches) {
const firstMatch = matches[0];
const index = text.search(queryRe);
nodeMatches = nodeMatches.setIn(keyPath,
{text, label, matches: [{start: index, length: firstMatch.length}]});
}
}
return nodeMatches;
}

function searchTopology(nodes, searchFields, prefix, query) {
function searchTopology(nodes, prefix, query) {
let nodeMatches = makeMap();
nodes.forEach((node, nodeId) => {
// top level fields
searchFields.forEach((field, label) => {
SEARCH_FIELDS.forEach((field, label) => {
const keyPath = [nodeId, label];
nodeMatches = findNodeMatch(nodeMatches, keyPath, node.get(field), query, label);
nodeMatches = findNodeMatch(nodeMatches, keyPath, node.get(field),
query, prefix, label);
});

// metadata
if (node.get('metadata') && (prefix === PREFIX_METADATA || prefix === PREFIX_METADATA_SHORT
|| prefix === PREFIX_ALL || prefix === PREFIX_ALL_SHORT)) {
if (node.get('metadata')) {
node.get('metadata').forEach(field => {
const keyPath = [nodeId, 'metadata', field.get('id')];
nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
query, field.get('label'));
query, prefix, field.get('label'));
});
}

// tables (envvars and labels)
const tables = node.get('tables');
if (tables) {
let searchTables;
if (prefix === PREFIX_ALL || prefix === PREFIX_ALL_SHORT) {
searchTables = SEARCH_TABLES;
} else if (prefix) {
searchTables = SEARCH_TABLES.filter((field, label) => prefix === label);
}
if (searchTables && searchTables.size > 0) {
searchTables.forEach((searchTable) => {
const table = tables.find(t => t.get('id') === searchTable);
if (table && table.get('rows')) {
table.get('rows').forEach(field => {
const keyPath = [nodeId, 'metadata', field.get('id')];
nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
query, field.get('label'));
});
}
});
}
tables.forEach((table) => {
if (table.get('rows')) {
table.get('rows').forEach(field => {
const keyPath = [nodeId, 'metadata', field.get('id')];
nodeMatches = findNodeMatch(nodeMatches, keyPath, field.get('value'),
query, prefix, field.get('label'));
});
}
});
}
});
return nodeMatches;
Expand All @@ -82,15 +78,13 @@ export function updateNodeMatches(state) {

if (query && (isPrefixQuery === isValidPrefixQuery)) {
const prefix = isValidPrefixQuery ? prefixQuery[0] : null;
let searchFields = SEARCH_FIELDS;
if (isPrefixQuery) {
query = prefixQuery[1];
searchFields = searchFields.filter((field, label) => label === prefix);
}
state.get('topologyUrlsById').forEach((url, topologyId) => {
const topologyNodes = state.getIn(['nodesByTopology', topologyId]);
if (topologyNodes) {
const nodeMatches = searchTopology(topologyNodes, searchFields, prefix, query);
const nodeMatches = searchTopology(topologyNodes, prefix, query);
state = state.setIn(['searchNodeMatches', topologyId], nodeMatches);
}
});
Expand Down
5 changes: 5 additions & 0 deletions client/app/scripts/utils/string-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,8 @@ function makeFormatMetric(renderFn) {
export const formatMetric = makeFormatMetric(renderHtml);
export const formatMetricSvg = makeFormatMetric(renderSvg);
export const formatDate = d3.time.format.iso;

const CLEAN_LABEL_REGEX = /\W/g;
export function slugify(label) {
return label.replace(CLEAN_LABEL_REGEX, '');
}

0 comments on commit e95a8ac

Please sign in to comment.