mirror of
https://github.com/weaveworks/scope.git
synced 2026-05-06 01:08:03 +00:00
Add a new search section to the help popover
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();
|
||||
}
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user