From db5baabeee1ba468bdca1f6250a535ea904ceb5a Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 12 Oct 2016 17:35:26 +0200 Subject: [PATCH 1/4] Add a new search section to the help popover --- client/app/scripts/components/help-panel.js | 147 +++++++++++++-- client/app/scripts/components/search.js | 14 +- .../app/scripts/selectors/chartSelectors.js | 7 + client/app/scripts/utils/search-utils.js | 36 +++- client/app/scripts/utils/string-utils.js | 2 +- client/app/styles/main.less | 178 ++++++++++++++---- 6 files changed, 330 insertions(+), 54 deletions(-) diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index 45f24e9f2f..07672b9448 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 ( -
+ +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}
+
+ ))} +
+ ); +} + + +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 ( +
+
-

Keyboard Shortcuts

+

Help

-

General

- {renderShortcuts(GENERAL_SHORTCUTS)} -

Canvas Metrics

- {renderShortcuts(CANVAS_METRIC_SHORTCUTS)} + {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 8671e3e920..4d64762c93 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 5792a56b2f..4e146a9ea6 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 77d67cc35d..390a813ede 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 7884316938..0687bc0ff0 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 9d1d47832d..31d4bec212 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; - } - div.key { - width: 100px; - display: inline-block; + &-search { + margin-right: 36px; + + &-row { + display: flex; + flex-direction: row; + + &-term { + flex: 1; + color: #5b5b88; + } + + &-term-label { + flex: 1; + } } - div.label { - display: inline-block; + } + + &-fields { + display: flex; + flex-direction: column; + + &-current-topology { + text-transform: uppercase; + color: #8383ac; + } + + &-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)"; + } + } } } } From c99f4dbfa272ae2866c6d6682932b4e0ad137bed Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 19 Oct 2016 11:59:56 +0200 Subject: [PATCH 2/4] Fixes search tests, forgot to include 0-9 when switching from \W --- client/app/scripts/utils/string-utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/scripts/utils/string-utils.js b/client/app/scripts/utils/string-utils.js index 0687bc0ff0..7f19f4f9e8 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 = /[^A-Za-z]/g; +const CLEAN_LABEL_REGEX = /[^A-Za-z0-9]/g; export function slugify(label) { return label.replace(CLEAN_LABEL_REGEX, '').toLowerCase(); } From 14164cc8b39facadd458d879aa160ef6c139cd83 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 19 Oct 2016 14:30:22 +0200 Subject: [PATCH 3/4] Review feedback on addition of search to help-panel --- client/app/scripts/components/help-panel.js | 22 ++++++++++++++++----- client/app/scripts/components/search.js | 6 +++--- client/app/styles/main.less | 11 +++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/client/app/scripts/components/help-panel.js b/client/app/scripts/components/help-panel.js index 07672b9448..b7147aaa64 100644 --- a/client/app/scripts/components/help-panel.js +++ b/client/app/scripts/components/help-panel.js @@ -51,19 +51,31 @@ function renderShortcutPanel() { const BASIC_SEARCHES = [ {term: 'foo', label: 'All fields for foo'}, - {term: 'pid: 12345', label: 'Any field matching pid for the value 12345'}, + { + 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'}, + { + 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'}, + {term: 'cpu > 4%', label: CPU greater than 4%}, + { + term: 'memory < 10mb', + label: Memory less than 10 megabytes + }, ]; diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 4d64762c93..70fd88839b 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -133,9 +133,9 @@ class Search extends React.Component { {!showPinnedSearches &&
- {getHint(nodes)} - Help! - + {getHint(nodes)}
} diff --git a/client/app/styles/main.less b/client/app/styles/main.less index 31d4bec212..975a04914a 100644 --- a/client/app/styles/main.less +++ b/client/app/styles/main.less @@ -1340,13 +1340,9 @@ h2 { } &-help-link { + .btn-opacity; cursor: pointer; - font-weight: bold; - text-transform: uppercase; - - &:hover { - text-decoration: underline; - } + font-size: 150%; } &-label { @@ -1611,6 +1607,9 @@ h2 { &-term-label { flex: 1; + b { + color: #5b5b88; + } } } } From ec2551b6bbd9985af704ac54302e5627b9a7e990 Mon Sep 17 00:00:00 2001 From: Simon Howe Date: Wed, 19 Oct 2016 15:49:17 +0200 Subject: [PATCH 4/4] Optimize re-rendering of help-panel > dynamic fields list --- client/app/scripts/selectors/chartSelectors.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/app/scripts/selectors/chartSelectors.js b/client/app/scripts/selectors/chartSelectors.js index 4e146a9ea6..6e934c3316 100644 --- a/client/app/scripts/selectors/chartSelectors.js +++ b/client/app/scripts/selectors/chartSelectors.js @@ -96,9 +96,11 @@ export const dataNodesSelector = createSelector( ); -export const searchableFieldsSelector = createSelector( - allNodesSelector, - getSearchableFields +export const searchableFieldsSelector = returnPreviousRefIfEqual( + createSelector( + allNodesSelector, + getSearchableFields + ) );