mirror of
https://github.com/weaveworks/scope.git
synced 2026-03-03 02:00:43 +00:00
Search all fields by default, gray out nodes if no match
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -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 (
|
||||
<g className="nodes-chart-edges">
|
||||
@@ -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')
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
<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>
|
||||
);
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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, '');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user