diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index 45f24e9f2..07672b944 100644 --- a/client/app/scripts/components/help-panel.js +++ b/client/app/scripts/components/help-panel.js @@ -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 (
{cuts.map(({key, label}) => ( -
+
{key}
{label}
@@ -27,20 +35,135 @@ function renderShortcuts(cuts) { ); } -export default class HelpPanel extends React.Component { - render() { - return ( -
-
-

Keyboard Shortcuts

+ +function renderShortcutPanel() { + return ( +
+

Shortcuts

+

General

+ {renderShortcuts(GENERAL_SHORTCUTS)} +

Canvas Metrics

+ {renderShortcuts(CANVAS_METRIC_SHORTCUTS)} +
+ ); +} + + +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 ( +
+ {searches.map(({term, label}) => ( +
+
+ + {term} +
+
{label}
-
-

General

- {renderShortcuts(GENERAL_SHORTCUTS)} -

Canvas Metrics

- {renderShortcuts(CANVAS_METRIC_SHORTCUTS)} + ))} +
+ ); +} + + +function renderSearchPanel() { + return ( +
+

Search

+

Basics

+ {renderSearches(BASIC_SEARCHES)} + +

Regular expressions

+ {renderSearches(REGEX_SEARCHES)} + +

Metrics

+ {renderSearches(METRIC_SEARCHES)} + +
+ ); +} + + +function renderFieldsPanel(currentTopologyName, searchableFields) { + const none = None; + return ( +
+

Fields and Metrics

+

+ Searchable fields and metrics in the
+ currently selected + {currentTopologyName} topology: +

+
+
+

Fields

+
+ {searchableFields.get('fields').map(f => ( +
{f}
+ ))} + {searchableFields.get('fields').size === 0 && none} +
+
+
+

Metrics

+
+ {searchableFields.get('metrics').map(m => ( +
{m}
+ ))} + {searchableFields.get('metrics').size === 0 && none} +
- ); - } +
+ ); } + + +function HelpPanel({currentTopologyName, searchableFields, onClickClose}) { + return ( +
+
+
+

Help

+
+
+ {renderShortcutPanel()} + {renderSearchPanel()} + {renderFieldsPanel(currentTopologyName, searchableFields)} +
+
+ +
+
+
+ ); +} + + +function mapStateToProps(state) { + return { + searchableFields: searchableFieldsSelector(state), + currentTopologyName: state.getIn(['currentTopology', 'fullName']) + }; +} + + +export default connect(mapStateToProps, { onClickClose: hideHelp })(HelpPanel); diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 8671e3e92..4d64762c9 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -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 {
{!showPinnedSearches &&
- {getHint(nodes)} + {getHint(nodes)} + Help! +
}
@@ -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); diff --git a/client/app/scripts/selectors/chartSelectors.js b/client/app/scripts/selectors/chartSelectors.js index 5792a56b2..4e146a9ea 100644 --- a/client/app/scripts/selectors/chartSelectors.js +++ b/client/app/scripts/selectors/chartSelectors.js @@ -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... // diff --git a/client/app/scripts/utils/search-utils.js b/client/app/scripts/utils/search-utils.js index 77d67cc35..390a813ed 100644 --- a/client/app/scripts/utils/search-utils.js +++ b/client/app/scripts/utils/search-utils.js @@ -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 */ diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 788431693..0687bc0ff 100644 --- a/client/app/scripts/utils/string-utils.js +++ b/client/app/scripts/utils/string-utils.js @@ -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(); } diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 9d1d47832..31d4bec21 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1339,6 +1339,16 @@ h2 { text-align: left; } + &-help-link { + cursor: pointer; + font-weight: bold; + text-transform: uppercase; + + &:hover { + text-decoration: underline; + } + } + &-label { position: absolute; pointer-events: none; @@ -1482,62 +1492,158 @@ h2 { // Help panel! // -@help-panel-width: 400px; -@help-panel-height: 420px; .help-panel { - position: absolute; - -webkit-transform: translate3d(0, 0, 0); - top: 50%; - left: 50%; - width: @help-panel-width; - height: @help-panel-height; - margin-left: @help-panel-width / -2; - margin-top: @help-panel-height / -2; z-index: 2048; background-color: white; .shadow-2; + display: flex; + position: relative; + + &-wrapper { + position: absolute; + width: 100%; + height: 100%; + + display: flex; + justify-content: center; + align-items: flex-start; + } &-header { background-color: @weave-blue; - padding: 36px; + padding: 12px 24px; color: white; h2 { margin: 0; + text-transform: uppercase; + font-size: 125%; } } + &-tools { + position: absolute; + top: 6px; + right: 8px; + + span { + .btn-opacity; + padding: 4px 5px; + margin-left: 2px; + font-size: 110%; + color: #8383ac; + cursor: pointer; + border: 1px solid rgba(131, 131, 172, 0); + border-radius: 10%; + + &:hover { + border-color: rgba(131, 131, 172, 0.6); + } + } + } + + &-main { - padding: 12px 36px 36px; + padding: 12px 36px 36px 36px; + display: flex; + flex-direction: row; + align-items: stretch; + + h2 { + text-transform: uppercase; + line-height: 150%; + font-size: 125%; + color: #8383ac; + padding: 4px 0; + border-bottom: 1px solid rgba(131, 131, 172, 0.1); + } + + h3 { + text-transform: uppercase; + font-size: 90%; + color: #8383ac; + padding: 4px 0; + } + + p { + margin: 0; + } } - h3 { - text-transform: uppercase; - font-size: 90%; - color: #8383ac; - padding: 4px 0; + &-shortcuts { + margin-right: 36px; + + &-shortcut { + kbd { + display: inline-block; + padding: 3px 5px; + font-size: 11px; + line-height: 10px; + color: #555; + vertical-align: middle; + background-color: #fcfcfc; + border: solid 1px #ccc; + border-bottom-color: #bbb; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #bbb; + } + div.key { + width: 80px; + display: inline-block; + } + div.label { + display: inline-block; + } + } } - &-shortcut { - kbd { - display: inline-block; - padding: 3px 5px; - font-size: 11px; - line-height: 10px; - color: #555; - vertical-align: middle; - background-color: #fcfcfc; - border: solid 1px #ccc; - border-bottom-color: #bbb; - border-radius: 3px; - box-shadow: inset 0 -1px 0 #bbb; + &-search { + margin-right: 36px; + + &-row { + display: flex; + flex-direction: row; + + &-term { + flex: 1; + color: #5b5b88; + } + + &-term-label { + flex: 1; + } } - div.key { - width: 100px; - display: inline-block; + } + + &-fields { + display: flex; + flex-direction: column; + + &-current-topology { + text-transform: uppercase; + color: #8383ac; } - div.label { - display: inline-block; + + &-fields { + display: flex; + align-items: stretch; + + &-column { + display: flex; + flex-direction: column; + flex: 1; + margin-right: 12px; + + &-content { + overflow: auto; + // 160px for top and bottom margins and the rest of the help window + // is about 160px too. + // Notes: Firefox gets a bit messy if you try and bubble + // heights + overflow up (min-height issue + still doesn't work v.well), + // so this is a bit of a hack. + max-height: ~"calc(100vh - 160px - 160px - 160px)"; + } + } } } }