mirror of
https://github.com/weaveworks/scope.git
synced 2026-07-28 17:51:21 +00:00
Add a search section to the help-popup
This commit is contained in:
@@ -1,4 +1,10 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { searchableFieldsSelector } from '../selectors/chartSelectors';
|
||||
import { CANVAS_MARGINS } from '../constants/styles';
|
||||
import { hideHelp } from '../actions/app-actions';
|
||||
|
||||
|
||||
const GENERAL_SHORTCUTS = [
|
||||
{key: 'esc', label: 'Close active panel'},
|
||||
@@ -8,17 +14,19 @@ const GENERAL_SHORTCUTS = [
|
||||
{key: 'g', label: 'Toggle Graph mode'},
|
||||
];
|
||||
|
||||
|
||||
const CANVAS_METRIC_SHORTCUTS = [
|
||||
{key: '<', label: 'Select and pin previous metric'},
|
||||
{key: '>', label: 'Select and pin next metric'},
|
||||
{key: 'q', label: 'Unpin current metric'},
|
||||
];
|
||||
|
||||
|
||||
function renderShortcuts(cuts) {
|
||||
return (
|
||||
<div>
|
||||
{cuts.map(({key, label}) => (
|
||||
<div key={key} className="help-panel-shortcut">
|
||||
<div key={key} className="help-panel-shortcuts-shortcut">
|
||||
<div className="key"><kbd>{key}</kbd></div>
|
||||
<div className="label">{label}</div>
|
||||
</div>
|
||||
@@ -27,20 +35,135 @@ function renderShortcuts(cuts) {
|
||||
);
|
||||
}
|
||||
|
||||
export default class HelpPanel extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="help-panel">
|
||||
<div className="help-panel-header">
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
|
||||
function renderShortcutPanel() {
|
||||
return (
|
||||
<div className="help-panel-shortcuts">
|
||||
<h2>Shortcuts</h2>
|
||||
<h3>General</h3>
|
||||
{renderShortcuts(GENERAL_SHORTCUTS)}
|
||||
<h3>Canvas Metrics</h3>
|
||||
{renderShortcuts(CANVAS_METRIC_SHORTCUTS)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const BASIC_SEARCHES = [
|
||||
{term: 'foo', label: 'All fields for foo'},
|
||||
{term: 'pid: 12345', label: 'Any field matching pid for the value 12345'},
|
||||
];
|
||||
|
||||
|
||||
const REGEX_SEARCHES = [
|
||||
{term: 'foo|bar', label: 'All fields for foo or bar'},
|
||||
{term: 'command: foo(bar|baz)', label: 'Command field for foobar or foobaz'},
|
||||
];
|
||||
|
||||
|
||||
const METRIC_SEARCHES = [
|
||||
{term: 'cpu > 4%', label: 'CPU greater than 4%'},
|
||||
{term: 'memory < 10mb', label: 'memory less than 4mb'},
|
||||
];
|
||||
|
||||
|
||||
function renderSearches(searches) {
|
||||
return (
|
||||
<div>
|
||||
{searches.map(({term, label}) => (
|
||||
<div key={term} className="help-panel-search-row">
|
||||
<div className="help-panel-search-row-term">
|
||||
<i className="fa fa-search search-label-icon"></i>
|
||||
{term}
|
||||
</div>
|
||||
<div className="help-panel-search-row-term-label">{label}</div>
|
||||
</div>
|
||||
<div className="help-panel-main">
|
||||
<h3>General</h3>
|
||||
{renderShortcuts(GENERAL_SHORTCUTS)}
|
||||
<h3>Canvas Metrics</h3>
|
||||
{renderShortcuts(CANVAS_METRIC_SHORTCUTS)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function renderSearchPanel() {
|
||||
return (
|
||||
<div className="help-panel-search">
|
||||
<h2>Search</h2>
|
||||
<h3>Basics</h3>
|
||||
{renderSearches(BASIC_SEARCHES)}
|
||||
|
||||
<h3>Regular expressions</h3>
|
||||
{renderSearches(REGEX_SEARCHES)}
|
||||
|
||||
<h3>Metrics</h3>
|
||||
{renderSearches(METRIC_SEARCHES)}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function renderFieldsPanel(currentTopologyName, searchableFields) {
|
||||
const none = <span style={{fontStyle: 'italic'}}>None</span>;
|
||||
return (
|
||||
<div className="help-panel-fields">
|
||||
<h2>Fields and Metrics</h2>
|
||||
<p>
|
||||
Searchable fields and metrics in the <br />
|
||||
currently selected <span className="help-panel-fields-current-topology">
|
||||
{currentTopologyName}</span> topology:
|
||||
</p>
|
||||
<div className="help-panel-fields-fields">
|
||||
<div className="help-panel-fields-fields-column">
|
||||
<h3>Fields</h3>
|
||||
<div className="help-panel-fields-fields-column-content">
|
||||
{searchableFields.get('fields').map(f => (
|
||||
<div key={f}>{f}</div>
|
||||
))}
|
||||
{searchableFields.get('fields').size === 0 && none}
|
||||
</div>
|
||||
</div>
|
||||
<div className="help-panel-fields-fields-column">
|
||||
<h3>Metrics</h3>
|
||||
<div className="help-panel-fields-fields-column-content">
|
||||
{searchableFields.get('metrics').map(m => (
|
||||
<div key={m}>{m}</div>
|
||||
))}
|
||||
{searchableFields.get('metrics').size === 0 && none}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function HelpPanel({currentTopologyName, searchableFields, onClickClose}) {
|
||||
return (
|
||||
<div className="help-panel-wrapper">
|
||||
<div className="help-panel" style={{marginTop: CANVAS_MARGINS.top}}>
|
||||
<div className="help-panel-header">
|
||||
<h2>Help</h2>
|
||||
</div>
|
||||
<div className="help-panel-main">
|
||||
{renderShortcutPanel()}
|
||||
{renderSearchPanel()}
|
||||
{renderFieldsPanel(currentTopologyName, searchableFields)}
|
||||
</div>
|
||||
<div className="help-panel-tools">
|
||||
<span title="Close details" className="fa fa-close" onClick={onClickClose} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
searchableFields: searchableFieldsSelector(state),
|
||||
currentTopologyName: state.getIn(['currentTopology', 'fullName'])
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(mapStateToProps, { onClickClose: hideHelp })(HelpPanel);
|
||||
|
||||
@@ -4,11 +4,12 @@ import { connect } from 'react-redux';
|
||||
import classnames from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { blurSearch, doSearch, focusSearch } from '../actions/app-actions';
|
||||
import { blurSearch, doSearch, focusSearch, showHelp } from '../actions/app-actions';
|
||||
import { slugify } from '../utils/string-utils';
|
||||
import { isTopologyEmpty } from '../utils/topology-utils';
|
||||
import SearchItem from './search-item';
|
||||
|
||||
|
||||
function shortenHintLabel(text) {
|
||||
return text
|
||||
.split(' ')[0]
|
||||
@@ -16,6 +17,7 @@ function shortenHintLabel(text) {
|
||||
.substr(0, 12);
|
||||
}
|
||||
|
||||
|
||||
// dynamic hint based on node names
|
||||
function getHint(nodes) {
|
||||
let label = 'mycontainer';
|
||||
@@ -38,6 +40,7 @@ function getHint(nodes) {
|
||||
Hit enter to apply the search as a filter.`;
|
||||
}
|
||||
|
||||
|
||||
class Search extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
@@ -95,7 +98,7 @@ class Search extends React.Component {
|
||||
|
||||
render() {
|
||||
const { inputId = 'search', nodes, pinnedSearches, searchFocused,
|
||||
searchNodeMatches, searchQuery, topologiesLoaded } = this.props;
|
||||
searchNodeMatches, searchQuery, topologiesLoaded, onClickHelp } = this.props;
|
||||
const disabled = this.props.isTopologyEmpty;
|
||||
const matchCount = searchNodeMatches
|
||||
.reduce((count, topologyMatches) => count + topologyMatches.size, 0);
|
||||
@@ -130,7 +133,9 @@ class Search extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
{!showPinnedSearches && <div className="search-hint">
|
||||
{getHint(nodes)}
|
||||
{getHint(nodes)} <span className="search-help-link" onClick={onClickHelp}>
|
||||
Help!
|
||||
</span>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
@@ -138,6 +143,7 @@ class Search extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
nodes: state.get('nodes'),
|
||||
@@ -148,5 +154,5 @@ export default connect(
|
||||
searchNodeMatches: state.get('searchNodeMatches'),
|
||||
topologiesLoaded: state.get('topologiesLoaded')
|
||||
}),
|
||||
{ blurSearch, doSearch, focusSearch }
|
||||
{ blurSearch, doSearch, focusSearch, onClickHelp: showHelp }
|
||||
)(Search);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'
|
||||
import { Map as makeMap, is, Set } from 'immutable';
|
||||
|
||||
import { getAdjacentNodes } from '../utils/topology-utils';
|
||||
import { getSearchableFields } from '../utils/search-utils';
|
||||
|
||||
|
||||
const log = debug('scope:selectors');
|
||||
@@ -95,6 +96,12 @@ export const dataNodesSelector = createSelector(
|
||||
);
|
||||
|
||||
|
||||
export const searchableFieldsSelector = createSelector(
|
||||
allNodesSelector,
|
||||
getSearchableFields
|
||||
);
|
||||
|
||||
|
||||
//
|
||||
// FIXME: this is a bit of a hack...
|
||||
//
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Map as makeMap } from 'immutable';
|
||||
import { Map as makeMap, Set as makeSet, List as makeList } from 'immutable';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { slugify } from './string-utils';
|
||||
@@ -247,6 +247,40 @@ export function updateNodeMatches(state) {
|
||||
return state;
|
||||
}
|
||||
|
||||
|
||||
export function getSearchableFields(nodes) {
|
||||
const get = (node, key) => node.get(key) || makeList();
|
||||
|
||||
const baseLabels = makeSet(nodes.size > 0 ? SEARCH_FIELDS.valueSeq() : []);
|
||||
|
||||
const metadataLabels = nodes.reduce((labels, node) => (
|
||||
labels.union(get(node, 'metadata').map(f => f.get('label')))
|
||||
), makeSet());
|
||||
|
||||
const parentLabels = nodes.reduce((labels, node) => (
|
||||
labels.union(get(node, 'parents').map(p => p.get('topologyId')))
|
||||
), makeSet());
|
||||
|
||||
const tableRowLabels = nodes.reduce((labels, node) => (
|
||||
labels.union(get(node, 'tables').flatMap(t => (t.get('rows') || makeList)
|
||||
.map(f => f.get('label'))
|
||||
))
|
||||
), makeSet());
|
||||
|
||||
const metricLabels = nodes.reduce((labels, node) => (
|
||||
labels.union(get(node, 'metrics').map(f => f.get('label')))
|
||||
), makeSet());
|
||||
|
||||
return makeMap({
|
||||
fields: baseLabels.union(metadataLabels, parentLabels, tableRowLabels)
|
||||
.map(slugify)
|
||||
.toList()
|
||||
.sort(),
|
||||
metrics: metricLabels.toList().map(slugify).sort()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set `filtered:true` in state's nodes if a pinned search matches
|
||||
*/
|
||||
|
||||
@@ -65,7 +65,7 @@ export const formatMetric = makeFormatMetric(renderHtml);
|
||||
export const formatMetricSvg = makeFormatMetric(renderSvg);
|
||||
export const formatDate = d3.time.format.iso;
|
||||
|
||||
const CLEAN_LABEL_REGEX = /\W/g;
|
||||
const CLEAN_LABEL_REGEX = /[^A-Za-z]/g;
|
||||
export function slugify(label) {
|
||||
return label.replace(CLEAN_LABEL_REGEX, '').toLowerCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user