Skip to content

Commit

Permalink
Merge pull request #1919 from weaveworks/1762-improve-search-docs
Browse files Browse the repository at this point in the history
Add a new search section to the help popover
  • Loading branch information
foot authored Oct 20, 2016
2 parents f6e81cf + ec2551b commit 816224a
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 54 deletions.
159 changes: 147 additions & 12 deletions client/app/scripts/components/help-panel.js
Original file line number Diff line number Diff line change
@@ -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'},
Expand All @@ -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>
Expand All @@ -27,20 +35,147 @@ function renderShortcuts(cuts) {
);
}

export default class HelpPanel extends React.Component {
render() {
return (
<div className="help-panel">

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: <span>Any field matching <b>pid</b> for the value 12345</span>
},
];


const REGEX_SEARCHES = [
{
term: 'foo|bar',
label: 'All fields for foo or bar'
},
{
term: 'command: foo(bar|baz)',
label: <span><b>command</b> field for foobar or foobaz</span>
},
];


const METRIC_SEARCHES = [
{term: 'cpu > 4%', label: <span><b>CPU</b> greater than 4%</span>},
{
term: 'memory < 10mb',
label: <span><b>Memory</b> less than 10 megabytes</span>
},
];


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>
);
}


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>Keyboard Shortcuts</h2>
<h2>Help</h2>
</div>
<div className="help-panel-main">
<h3>General</h3>
{renderShortcuts(GENERAL_SHORTCUTS)}
<h3>Canvas Metrics</h3>
{renderShortcuts(CANVAS_METRIC_SHORTCUTS)}
{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);
14 changes: 10 additions & 4 deletions client/app/scripts/components/search.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ 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]
.toLowerCase()
.substr(0, 12);
}


// dynamic hint based on node names
function getHint(nodes) {
let label = 'mycontainer';
Expand All @@ -38,6 +40,7 @@ function getHint(nodes) {
Hit enter to apply the search as a filter.`;
}


class Search extends React.Component {

constructor(props, context) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -130,14 +133,17 @@ class Search extends React.Component {
</label>
</div>
{!showPinnedSearches && <div className="search-hint">
{getHint(nodes)}
{getHint(nodes)} <span
className="search-help-link fa fa-question-circle"
onClick={onClickHelp} />
</div>}
</div>
</div>
);
}
}


export default connect(
state => ({
nodes: state.get('nodes'),
Expand All @@ -148,5 +154,5 @@ export default connect(
searchNodeMatches: state.get('searchNodeMatches'),
topologiesLoaded: state.get('topologiesLoaded')
}),
{ blurSearch, doSearch, focusSearch }
{ blurSearch, doSearch, focusSearch, onClickHelp: showHelp }
)(Search);
9 changes: 9 additions & 0 deletions client/app/scripts/selectors/chartSelectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -95,6 +96,14 @@ export const dataNodesSelector = createSelector(
);


export const searchableFieldsSelector = returnPreviousRefIfEqual(
createSelector(
allNodesSelector,
getSearchableFields
)
);


//
// FIXME: this is a bit of a hack...
//
Expand Down
36 changes: 35 additions & 1 deletion client/app/scripts/utils/search-utils.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion client/app/scripts/utils/string-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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-z0-9]/g;
export function slugify(label) {
return label.replace(CLEAN_LABEL_REGEX, '').toLowerCase();
}
Expand Down
Loading

0 comments on commit 816224a

Please sign in to comment.