+
@@ -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}) => (
+
-
-
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)";
+ }
+ }
}
}
}