From 3c6cced4d3e34d3e106036acb865ed846ae420a4 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Mon, 3 Sep 2018 16:40:56 +0200 Subject: [PATCH 1/4] Use new Search component. --- client/app/scripts/actions/app-actions.js | 76 ++----- client/app/scripts/components/app.js | 4 - client/app/scripts/components/search-item.js | 27 --- client/app/scripts/components/search.js | 222 +++++++------------ client/app/scripts/constants/action-types.js | 4 +- client/app/scripts/reducers/root.js | 27 +-- client/app/styles/_base.scss | 145 +----------- client/package.json | 2 +- client/yarn.lock | 7 +- 9 files changed, 111 insertions(+), 403 deletions(-) delete mode 100644 client/app/scripts/components/search-item.js 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 ( -
-
-
- {showPinnedSearches && pinnedSearches.toIndexedSeq() - .map(query => )} - -
-
- - - Search - -
- {!showPinnedSearches && -
- {getHint(nodes)} -
- } -
-
+ + + + + {searchHint} + + + ); } } @@ -185,18 +120,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); diff --git a/client/app/scripts/constants/action-types.js b/client/app/scripts/constants/action-types.js index 47ae93a39..1fc726bda 100644 --- a/client/app/scripts/constants/action-types.js +++ b/client/app/scripts/constants/action-types.js @@ -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); diff --git a/client/app/scripts/reducers/root.js b/client/app/scripts/reducers/root.js index 6ab9b007f..37c4fb1e7 100644 --- a/client/app/scripts/reducers/root.js +++ b/client/app/scripts/reducers/root.js @@ -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); } diff --git a/client/app/styles/_base.scss b/client/app/styles/_base.scss index 41112a716..bbb2d468f 100644 --- a/client/app/styles/_base.scss +++ b/client/app/styles/_base.scss @@ -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 { diff --git a/client/package.json b/client/package.json index 354de17e9..a9f24ec36 100644 --- a/client/package.json +++ b/client/package.json @@ -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.29", "whatwg-fetch": "2.0.3", "xterm": "3.3.0" }, diff --git a/client/yarn.lock b/client/yarn.lock index 5ec4bcb91..52d68246f 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -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.29: + version "0.11.29" + resolved "https://registry.yarnpkg.com/weaveworks-ui-components/-/weaveworks-ui-components-0.11.29.tgz#f77d8c9f3c9fa398a73fb0ec98ee11205f035610" 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" From a42f0f087e900314fa4495228dd7d4e94c011c4e Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Wed, 5 Sep 2018 12:47:15 +0200 Subject: [PATCH 2/4] Bump ui-components to v0.11.30 --- client/app/scripts/components/matched-results.js | 1 - client/package.json | 2 +- client/yarn.lock | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/app/scripts/components/matched-results.js b/client/app/scripts/components/matched-results.js index 996056636..72bae1a7b 100644 --- a/client/app/scripts/components/matched-results.js +++ b/client/app/scripts/components/matched-results.js @@ -12,7 +12,6 @@ const Match = match => ( {match.label}:
Date: Thu, 6 Sep 2018 17:12:30 +0200 Subject: [PATCH 3/4] Bump ui-components to v0.11.31 --- client/package.json | 2 +- client/yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/package.json b/client/package.json index 32a8c61b5..0ee7ae0ad 100644 --- a/client/package.json +++ b/client/package.json @@ -44,7 +44,7 @@ "reselect": "3.0.1", "reselect-map": "1.0.3", "styled-components": "2.2.4", - "weaveworks-ui-components": "0.11.30", + "weaveworks-ui-components": "0.11.31", "whatwg-fetch": "2.0.3", "xterm": "3.3.0" }, diff --git a/client/yarn.lock b/client/yarn.lock index 67ef3c749..6618af758 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -8401,9 +8401,9 @@ wd@^0.4.0: underscore.string "~3.0.3" vargs "~0.1.0" -weaveworks-ui-components@0.11.30: - version "0.11.30" - resolved "https://registry.yarnpkg.com/weaveworks-ui-components/-/weaveworks-ui-components-0.11.30.tgz#a8537c4c9b6903a02f0126e78ea4ef9991a43860" +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" From 0fcc9f151785ba280e78732933a2d455e4dcbd07 Mon Sep 17 00:00:00 2001 From: Filip Barl Date: Thu, 6 Sep 2018 17:17:09 +0200 Subject: [PATCH 4/4] Added some top margin to Search hint. --- client/app/scripts/components/search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/app/scripts/components/search.js b/client/app/scripts/components/search.js index 2c3f44aaa..90d0dc0d3 100644 --- a/client/app/scripts/components/search.js +++ b/client/app/scripts/components/search.js @@ -32,6 +32,7 @@ const SearchHint = styled.div` 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;