From 3ee802a516f7efc9c362d18ff1148fe79685c87c Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 3 May 2016 15:58:09 +0200 Subject: [PATCH] Search all fields by default, gray out nodes if no match --- client/app/scripts/charts/node.js | 8 +- .../app/scripts/charts/nodes-chart-edges.js | 11 +-- .../app/scripts/charts/nodes-chart-nodes.js | 6 +- client/app/scripts/components/search.js | 31 +++++--- client/app/scripts/utils/search-utils.js | 78 +++++++++---------- client/app/scripts/utils/string-utils.js | 5 ++ 6 files changed, 73 insertions(+), 66 deletions(-) diff --git a/client/app/scripts/charts/node.js b/client/app/scripts/charts/node.js index 7a6d359f6..1c8ecda43 100644 --- a/client/app/scripts/charts/node.js +++ b/client/app/scripts/charts/node.js @@ -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; @@ -77,7 +76,6 @@ class Node extends React.Component { labelFontSize /= zoomScale; subLabelFontSize /= zoomScale; labelOffsetY /= zoomScale; - subLabelOffsetY /= zoomScale; } const className = classNames({ @@ -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)} /> + width={nodeScale(scaleFactor * 4)}>
diff --git a/client/app/scripts/charts/nodes-chart-edges.js b/client/app/scripts/charts/nodes-chart-edges.js index 7c4352679..d646a2c71 100644 --- a/client/app/scripts/charts/nodes-chart-edges.js +++ b/client/app/scripts/charts/nodes-chart-edges.js @@ -8,7 +8,7 @@ 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 ( @@ -16,7 +16,7 @@ class NodesChartEdges extends React.Component { 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); @@ -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') }; } diff --git a/client/app/scripts/charts/nodes-chart-nodes.js b/client/app/scripts/charts/nodes-chart-nodes.js index af8f5b748..1a2d8ca62 100644 --- a/client/app/scripts/charts/nodes-chart-nodes.js +++ b/client/app/scripts/charts/nodes-chart-nodes.js @@ -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; @@ -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 => { @@ -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() }; } diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index f45212947..39691b74e 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -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 { @@ -84,7 +90,7 @@ class Search extends React.Component { Search -
{getHint()}
+
{getHint(this.props.nodes)}
); @@ -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'), diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js index 5f0f796af..e5b7ba26e 100644 --- a/client/app/scripts/utils/search-utils.js +++ b/client/app/scripts/utils/search-utils.js @@ -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; @@ -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); } }); diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 9b7c22df4..9490ce4b4 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -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, ''); +}