diff --git a/client/app/scripts/actions/app-actions.js b/client/app/scripts/actions/app-actions.js index 4a468988f..698250fdd 100644 --- a/client/app/scripts/actions/app-actions.js +++ b/client/app/scripts/actions/app-actions.js @@ -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); diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js index 4588b2419..5c1b979a1 100644 --- a/client/app/scripts/components/app.js +++ b/client/app/scripts/components/app.js @@ -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(); diff --git a/client/app/scripts/components/search-item.js b/client/app/scripts/components/search-item.js deleted file mode 100644 index fc5e42ba6..000000000 --- a/client/app/scripts/components/search-item.js +++ /dev/null @@ -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 ( - - {this.props.query} - - - ); - } -} - -export default connect(null, { unpinSearch })(SearchItem); diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 6fc8bc2c9..2c3f44aaa 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -1,19 +1,54 @@ 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; + 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 +56,6 @@ function shortenHintLabel(text) { .substr(0, 12); } - // dynamic hint based on node names function getHint(nodes) { let label = 'mycontainer'; @@ -39,145 +73,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 ( -