Merge pull request #3337 from weaveworks/3314-use-new-search-bar

Use new Search component
This commit is contained in:
Filip Barl
2018-09-06 17:32:31 +02:00
committed by GitHub
10 changed files with 112 additions and 404 deletions

View File

@@ -171,23 +171,29 @@ export function pinPreviousMetric() {
};
}
export function pinSearch() {
export function updateSearch(searchQuery = '', pinnedSearches = []) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.PIN_SEARCH,
query: getState().get('searchQuery'),
type: ActionTypes.UPDATE_SEARCH,
pinnedSearches,
searchQuery,
});
updateRoute(getState);
};
}
export function unpinSearch(query) {
export function focusSearch() {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.UNPIN_SEARCH,
query
});
updateRoute(getState);
dispatch({ type: ActionTypes.FOCUS_SEARCH });
// update nodes cache to allow search across all topologies,
// wait a second until animation is over
// NOTE: This will cause matching recalculation (and rerendering)
// of all the nodes in the topology, instead applying it only on
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState(), dispatch);
}, 1200);
};
}
@@ -266,16 +272,6 @@ export function clickForceRelayout() {
};
}
export function doSearch(searchQuery) {
return (dispatch, getState) => {
dispatch({
type: ActionTypes.DO_SEARCH,
searchQuery
});
updateRoute(getState);
};
}
export function setViewportDimensions(width, height) {
return (dispatch) => {
dispatch({ type: ActionTypes.SET_VIEWPORT_DIMENSIONS, width, height });
@@ -450,38 +446,6 @@ export function enterNode(nodeId) {
};
}
export function focusSearch() {
return (dispatch, getState) => {
dispatch({ type: ActionTypes.FOCUS_SEARCH });
// update nodes cache to allow search across all topologies,
// wait a second until animation is over
// NOTE: This will cause matching recalculation (and rerendering)
// of all the nodes in the topology, instead applying it only on
// the nodes delta. The solution would be to implement deeper
// search selectors with per-node caching instead of per-topology.
setTimeout(() => {
getAllNodes(getState(), dispatch);
}, 1200);
};
}
export function hitBackspace() {
return (dispatch, getState) => {
const state = getState();
// remove last pinned query if search query is empty
if (state.get('searchFocused') && !state.get('searchQuery')) {
const query = state.get('pinnedSearches').last();
if (query) {
dispatch({
type: ActionTypes.UNPIN_SEARCH,
query
});
updateRoute(getState);
}
}
};
}
export function hitEsc() {
return (dispatch, getState) => {
const state = getState();
@@ -492,13 +456,6 @@ export function hitEsc() {
pipeId: controlPipe.get('id')
});
updateRoute(getState);
// Don't deselect node on ESC if there is a controlPipe (keep terminal open)
} else if (state.get('searchFocused')) {
if (state.get('searchQuery')) {
dispatch(doSearch(''));
} else {
dispatch(blurSearch());
}
} else if (state.get('showingHelp')) {
dispatch(hideHelp());
} else if (state.get('nodeDetails').last() && !controlPipe) {
@@ -634,9 +591,6 @@ export function receiveTopologies(topologies) {
getNodes(getState, dispatch);
// Populate search matches on first load
const state = getState();
if (firstLoad && state.get('searchQuery')) {
dispatch(focusSearch());
}
// Fetch all the relevant nodes once on first load
if (firstLoad && isResourceViewModeSelector(state)) {
getResourceViewNodesSnapshot(state, dispatch);

View File

@@ -23,7 +23,6 @@ import {
focusSearch,
pinNextMetric,
pinPreviousMetric,
hitBackspace,
hitEsc,
unpinMetric,
toggleHelp,
@@ -54,7 +53,6 @@ import {
} from '../selectors/topology';
import { VIEWPORT_RESIZE_DEBOUNCE_INTERVAL } from '../constants/timer';
import {
BACKSPACE_KEY_CODE,
ESC_KEY_CODE,
} from '../constants/key-codes';
@@ -116,8 +114,6 @@ class App extends React.Component {
// don't get esc in onKeyPress
if (ev.keyCode === ESC_KEY_CODE) {
this.props.dispatch(hitEsc());
} else if (ev.keyCode === BACKSPACE_KEY_CODE) {
this.props.dispatch(hitBackspace());
} else if (ev.code === 'KeyD' && ev.ctrlKey && !showingTerminal) {
toggleDebugToolbar();
this.forceUpdate();

View File

@@ -12,7 +12,6 @@ const Match = match => (
{match.label}:
</span>
<MatchedText
noBorder
text={match.text}
match={match}
maxLength={MAX_MATCH_LENGTH}

View File

@@ -1,27 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { unpinSearch } from '../actions/app-actions';
class SearchItem extends React.Component {
constructor(props, context) {
super(props, context);
this.handleClick = this.handleClick.bind(this);
}
handleClick(ev) {
ev.preventDefault();
this.props.unpinSearch(this.props.query);
}
render() {
return (
<span className="search-item">
<span className="search-item-label">{this.props.query}</span>
<span className="search-item-icon fa fa-close" onClick={this.handleClick} />
</span>
);
}
}
export default connect(null, { unpinSearch })(SearchItem);

View File

@@ -1,19 +1,55 @@
import React from 'react';
import { connect } from 'react-redux';
import classnames from 'classnames';
import { debounce } from 'lodash';
import { isEmpty } from 'lodash';
import { Search } from 'weaveworks-ui-components';
import styled from 'styled-components';
import { blurSearch, doSearch, focusSearch, pinSearch, toggleHelp } from '../actions/app-actions';
import { blurSearch, focusSearch, updateSearch, toggleHelp } from '../actions/app-actions';
import { searchMatchCountByTopologySelector } from '../selectors/search';
import { isResourceViewModeSelector } from '../selectors/topology';
import { slugify } from '../utils/string-utils';
import { parseQuery } from '../utils/search-utils';
import { isTopologyNodeCountZero } from '../utils/topology-utils';
import { trackAnalyticsEvent } from '../utils/tracking-utils';
import SearchItem from './search-item';
import { ENTER_KEY_CODE } from '../constants/key-codes';
const SearchWrapper = styled.div`
margin: 0 8px;
min-width: 160px;
text-align: right;
`;
const SearchContainer = styled.div`
display: inline-block;
position: relative;
pointer-events: all;
line-height: 100%;
max-width: 400px;
width: 100%;
`;
const SearchHint = styled.div`
font-size: ${props => props.theme.fontSizes.tiny};
color: ${props => props.theme.colors.purple400};
transition: transform 0.3s 0s ease-in-out, opacity 0.3s 0s ease-in-out;
text-align: left;
margin-top: 3px;
padding: 0 1em;
opacity: 0;
${props => props.active && `
opacity: 1;
`};
`;
const SearchHintIcon = styled.span`
font-size: ${props => props.theme.fontSizes.normal};
cursor: pointer;
&:hover {
color: ${props => props.theme.colors.purple600};
}
`;
function shortenHintLabel(text) {
return text
.split(' ')[0]
@@ -21,7 +57,6 @@ function shortenHintLabel(text) {
.substr(0, 12);
}
// dynamic hint based on node names
function getHint(nodes) {
let label = 'mycontainer';
@@ -39,145 +74,46 @@ function getHint(nodes) {
}
}
return `Try "${label}", "${metadataLabel}:${metadataValue}", or "cpu > 2%".
Hit enter to apply the search as a filter.`;
return `Try "${label}", "${metadataLabel}:${metadataValue}", or "cpu > 2%".`;
}
class Search extends React.Component {
constructor(props, context) {
super(props, context);
this.handleBlur = this.handleBlur.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleFocus = this.handleFocus.bind(this);
this.saveQueryInputRef = this.saveQueryInputRef.bind(this);
this.doSearch = debounce(this.doSearch.bind(this), 200);
this.state = {
value: ''
};
}
handleBlur() {
this.props.blurSearch();
}
handleChange(ev) {
const inputValue = ev.target.value;
let value = inputValue;
// In render() props.searchQuery can be set from the outside, but state.value
// must have precedence for quick feedback. Now when the user backspaces
// quickly enough from `text`, a previouse doSearch(`text`) will come back
// via props and override the empty state.value. To detect this edge case
// we instead set value to null when backspacing.
if (this.state.value && value === '') {
value = null;
}
this.setState({ value });
this.doSearch(inputValue);
}
handleKeyUp(ev) {
// If the search query is parsable, pin it when ENTER key is hit.
if (ev.keyCode === ENTER_KEY_CODE && parseQuery(this.props.searchQuery)) {
trackAnalyticsEvent('scope.search.query.pin', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
this.props.pinSearch();
}
}
handleFocus() {
this.props.focusSearch();
}
doSearch(value) {
if (value !== '') {
trackAnalyticsEvent('scope.search.query.change', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
}
this.props.doSearch(value);
}
saveQueryInputRef(ref) {
this.queryInput = ref;
}
componentWillReceiveProps(nextProps) {
// when cleared from the outside, reset internal state
if (this.props.searchQuery !== nextProps.searchQuery && nextProps.searchQuery === '') {
this.setState({ value: '' });
}
}
componentDidUpdate() {
if (this.props.searchFocused) {
this.queryInput.focus();
} else if (!this.state.value) {
this.queryInput.blur();
}
class SearchComponent extends React.Component {
handleChange = (searchQuery, pinnedSearches) => {
trackAnalyticsEvent('scope.search.query.change', {
layout: this.props.topologyViewMode,
topologyId: this.props.currentTopology.get('id'),
parentTopologyId: this.props.currentTopology.get('parentId'),
});
this.props.updateSearch(searchQuery, pinnedSearches);
}
render() {
const {
nodes, pinnedSearches, searchFocused, searchMatchCountByTopology,
isResourceViewMode, searchQuery, topologiesLoaded, inputId = 'search'
searchHint, searchMatchesCount, searchQuery, pinnedSearches, topologiesLoaded,
isResourceViewMode, isTopologyEmpty,
} = this.props;
const hidden = !topologiesLoaded || isResourceViewMode;
const disabled = this.props.isTopologyNodeCountZero && !hidden;
const matchCount = searchMatchCountByTopology
.reduce((count, topologyMatchCount) => count + topologyMatchCount, 0);
const showPinnedSearches = pinnedSearches.size > 0;
// manual clear (null) has priority, then props, then state
const value = this.state.value === null ? '' : this.state.value || searchQuery || '';
const classNames = classnames('search', 'hideable', {
hide: hidden,
'search-pinned': showPinnedSearches,
'search-matched': matchCount,
'search-filled': value,
'search-focused': searchFocused,
'search-disabled': disabled
});
const title = matchCount ? `${matchCount} matches` : null;
return (
<div className="search-wrapper">
<div className={classNames} title={title}>
<div className="search-input">
{showPinnedSearches && pinnedSearches.toIndexedSeq()
.map(query => <SearchItem query={query} key={query} />)}
<input
className="search-input-field"
type="text"
id={inputId}
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
disabled={disabled}
ref={this.saveQueryInputRef} />
</div>
<div className="search-label">
<i className="fa fa-search search-label-icon" />
<span className="search-label-hint" htmlFor={inputId}>
Search
</span>
</div>
{!showPinnedSearches &&
<div className="search-hint">
{getHint(nodes)} <span
className="search-help-link fa fa-question-circle"
onMouseDown={this.props.toggleHelp} />
</div>
}
</div>
</div>
<SearchWrapper>
<SearchContainer title={searchMatchesCount ? `${searchMatchesCount} matches` : undefined}>
<Search
placeholder="search"
query={searchQuery}
pinnedTerms={pinnedSearches}
disabled={topologiesLoaded && !isResourceViewMode && isTopologyEmpty}
onChange={this.handleChange}
onFocus={this.props.focusSearch}
onBlur={this.props.blurSearch}
/>
<SearchHint active={this.props.searchFocused && isEmpty(pinnedSearches)}>
{searchHint} <SearchHintIcon
className="fa fa-question-circle"
onMouseDown={this.props.toggleHelp}
/>
</SearchHint>
</SearchContainer>
</SearchWrapper>
);
}
}
@@ -185,18 +121,19 @@ class Search extends React.Component {
export default connect(
state => ({
nodes: state.get('nodes'),
searchHint: getHint(state.get('nodes')),
searchFocused: state.get('searchFocused'),
topologyViewMode: state.get('topologyViewMode'),
isResourceViewMode: isResourceViewModeSelector(state),
isTopologyNodeCountZero: isTopologyNodeCountZero(state),
isTopologyEmpty: isTopologyNodeCountZero(state),
currentTopology: state.get('currentTopology'),
topologiesLoaded: state.get('topologiesLoaded'),
pinnedSearches: state.get('pinnedSearches'),
searchFocused: state.get('searchFocused'),
pinnedSearches: state.get('pinnedSearches').toJS(),
searchQuery: state.get('searchQuery'),
searchMatchCountByTopology: searchMatchCountByTopologySelector(state),
searchMatchesCount: searchMatchCountByTopologySelector(state)
.reduce((count, topologyMatchCount) => count + topologyMatchCount, 0),
}),
{
blurSearch, doSearch, focusSearch, pinSearch, toggleHelp
blurSearch, focusSearch, updateSearch, toggleHelp
}
)(Search);
)(SearchComponent);

View File

@@ -22,7 +22,6 @@ const ACTION_TYPES = [
'DO_CONTROL_ERROR',
'DO_CONTROL_SUCCESS',
'DO_CONTROL',
'DO_SEARCH',
'ENTER_EDGE',
'ENTER_NODE',
'FINISH_TIME_TRAVEL_TRANSITION',
@@ -37,7 +36,6 @@ const ACTION_TYPES = [
'PAUSE_TIME_AT_NOW',
'PIN_METRIC',
'PIN_NETWORK',
'PIN_SEARCH',
'RECEIVE_API_DETAILS',
'RECEIVE_CONTROL_NODE_REMOVED',
'RECEIVE_CONTROL_PIPE_STATUS',
@@ -67,7 +65,7 @@ const ACTION_TYPES = [
'UNHOVER_METRIC',
'UNPIN_METRIC',
'UNPIN_NETWORK',
'UNPIN_SEARCH',
'UPDATE_SEARCH',
];
export default zipObject(ACTION_TYPES, ACTION_TYPES);

View File

@@ -70,7 +70,6 @@ export const initialState = makeMap({
plugins: makeList(),
pinnedSearches: makeList(), // list of node filters
routeSet: false,
storeViewState: true,
searchFocused: false,
searchQuery: '',
selectedNetwork: null,
@@ -78,6 +77,7 @@ export const initialState = makeMap({
showingHelp: false,
showingTroubleshootingMenu: false,
showingNetworks: false,
storeViewState: true,
timeTravelTransitioning: false,
topologies: makeList(),
topologiesLoaded: false,
@@ -219,6 +219,10 @@ export function rootReducer(state = initialState, action) {
return state.set('searchFocused', false);
}
case ActionTypes.FOCUS_SEARCH: {
return state.set('searchFocused', true);
}
case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
// set option on parent topology
const topology = findTopologyById(state.get('topologies'), action.topologyId);
@@ -466,10 +470,6 @@ export function rootReducer(state = initialState, action) {
}));
}
case ActionTypes.DO_SEARCH: {
return state.set('searchQuery', action.searchQuery);
}
case ActionTypes.ENTER_EDGE: {
return state.set('mouseOverEdgeId', action.edgeId);
}
@@ -499,14 +499,9 @@ export function rootReducer(state = initialState, action) {
}));
}
case ActionTypes.FOCUS_SEARCH: {
return state.set('searchFocused', true);
}
case ActionTypes.PIN_SEARCH: {
const pinnedSearches = state.get('pinnedSearches');
state = state.setIn(['pinnedSearches', pinnedSearches.size], action.query);
state = state.set('searchQuery', '');
case ActionTypes.UPDATE_SEARCH: {
state = state.set('pinnedSearches', makeList(action.pinnedSearches));
state = state.set('searchQuery', action.searchQuery || '');
return applyPinnedSearches(state);
}
@@ -722,12 +717,6 @@ export function rootReducer(state = initialState, action) {
return state;
}
case ActionTypes.UNPIN_SEARCH: {
const pinnedSearches = state.get('pinnedSearches').filter(query => query !== action.query);
state = state.set('pinnedSearches', pinnedSearches);
return applyPinnedSearches(state);
}
case ActionTypes.DEBUG_TOOLBAR_INTERFERING: {
return action.fn(state);
}

View File

@@ -1410,153 +1410,11 @@ a {
}
.search {
pointer-events: all;
display: inline-block;
position: relative;
width: 10em;
transition: width 0.3s 0s $base-ease;
&-wrapper {
margin: 0 8px;
min-width: 160px;
text-align: right;
}
&-disabled {
opacity: 0.5;
cursor: disabled;
}
&-hint {
font-size: $font-size-tiny;
position: absolute;
padding: 0 1em;
color: $text-tertiary-color;
top: 0;
opacity: 0;
transition: transform 0.3s 0s $base-ease, opacity 0.3s 0s $base-ease;
text-align: left;
}
&-help-link {
@extend .btn-opacity;
cursor: pointer;
font-size: $font-size-normal;
}
&-label {
position: absolute;
pointer-events: none;
user-select: none;
top: 0;
left: 4px;
z-index: $layer-front;
padding: 4px;
color: $text-secondary-color;
&-icon {
margin-right: 0.5em;
}
&-hint {
font-size: $font-size-small;
transition: opacity 0.3s 0.5s $base-ease;
opacity: 1;
}
}
&-input {
overflow: hidden;
background: $color-white;
position: relative;
z-index: $layer-front;
display: flex;
border-radius: $border-radius-soft;
width: 100%;
border: $search-border-width solid $search-border-color;
padding: 2px 4px;
text-align: left;
flex-wrap: wrap;
&-field {
font-size: $font-size-small;
line-height: 150%;
position: relative;
padding: 1px 4px 1px 1.5em;
border: none;
border-radius: $border-radius-none;
background: transparent;
color: $text-color;
width: 100px;
&:focus {
outline: none;
}
}
}
&-focused &-label-hint,
&-pinned &-label-hint,
&-filled &-label-hint {
transition: opacity 0.1s 0s $base-ease;
opacity: 0;
}
&-focused &-hint,
&-filled &-hint,
&-pinned &-hint {
opacity: 1;
transform: translate3d(0, 2.75em, 0);
transition: transform 0.3s 0.3s $base-ease, opacity 0.3s 0.3s $base-ease;
}
&-focused &-input-field,
&-filled &-input-field,
&-pinned &-input-field {
flex: 1;
}
&-focused,
&-filled,
&-pinned {
width: 100%;
}
&-matched &-input {
border-color: $color-blue-400;
}
}
.search-item {
background-color: transparentize($color-blue-400, 0.2);
border-radius: $border-radius-soft;
margin: 1px 0 1px 1.5em;
padding: 2px 4px 2px 6px;
display: flex;
align-items: center;
& + .search-item {
margin-left: 4px;
}
& + .search-input-field {
padding-left: 4px;
}
&-label {
margin-right: 4px;
}
&-icon {
@extend .btn-opacity;
padding: 2px;
cursor: pointer;
font-size: $font-size-small;
position: relative;
top: -1px;
}
}
@keyframes blinking {
@@ -1683,6 +1541,9 @@ a {
&-term {
flex: 1;
color: $text-secondary-color;
i {
margin-right: 5px;
}
}
&-term-label {

View File

@@ -44,7 +44,7 @@
"reselect": "3.0.1",
"reselect-map": "1.0.3",
"styled-components": "2.2.4",
"weaveworks-ui-components": "0.11.14",
"weaveworks-ui-components": "0.11.31",
"whatwg-fetch": "2.0.3",
"xterm": "3.3.0"
},

View File

@@ -8401,9 +8401,9 @@ wd@^0.4.0:
underscore.string "~3.0.3"
vargs "~0.1.0"
weaveworks-ui-components@0.11.14:
version "0.11.14"
resolved "https://registry.yarnpkg.com/weaveworks-ui-components/-/weaveworks-ui-components-0.11.14.tgz#5f72f8a5059bc9f28295b91164c17e0780906f9e"
weaveworks-ui-components@0.11.31:
version "0.11.31"
resolved "https://registry.yarnpkg.com/weaveworks-ui-components/-/weaveworks-ui-components-0.11.31.tgz#90392fe5aafd6a911d985ef9ef554cb7178474d5"
dependencies:
classnames "2.2.5"
d3-drag "1.2.1"
@@ -8416,6 +8416,7 @@ weaveworks-ui-components@0.11.14:
react-input-autosize "2.2.1"
react-motion "0.5.2"
react-resize-aware "2.7.0"
react-router "3.2.0"
webidl-conversions@^3.0.0:
version "3.0.1"