From 32492dd6501ef4869ddf86f03d64289fb6772ec8 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Mon, 27 Apr 2020 19:16:03 +0300 Subject: [PATCH] Search/ui issues (#23945) * Search: Move layout to query reducer/hook * Search: Move extra layout/sort logic to reducer * Search: Tweak action row spacing * Search: Update TagOption * Search: Remove duplicate function * Search: Add Clear tags button * Search: Align checkbox * Search: Add TagFilter.displayName * Search: Update default placeholder * Search: Return all dashboards for list view * Search: Apply custom line-height to ActionRow checkbox --- .../core/components/TagFilter/TagFilter.tsx | 136 ++++++++++-------- .../core/components/TagFilter/TagOption.tsx | 44 ++++-- public/app/core/services/search_srv.ts | 8 +- .../features/search/components/ActionRow.tsx | 41 +++--- .../search/components/SearchResults.tsx | 3 +- .../components/SearchResultsFilter.test.tsx | 4 +- 6 files changed, 146 insertions(+), 90 deletions(-) diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index c00d8b5596e..b5daffe2440 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -1,11 +1,10 @@ // Libraries -import React from 'react'; +import React, { FC } from 'react'; import { css } from 'emotion'; // @ts-ignore import { components } from '@torkelo/react-select'; -import { AsyncSelect, stylesFactory } from '@grafana/ui'; -import { Icon } from '@grafana/ui'; -import { escapeStringForRegex } from '@grafana/data'; +import { AsyncSelect, stylesFactory, useTheme, resetSelectStyles, Icon } from '@grafana/ui'; +import { escapeStringForRegex, GrafanaTheme } from '@grafana/data'; // Components import { TagOption } from './TagOption'; import { TagBadge } from './TagBadge'; @@ -31,17 +30,20 @@ const filterOption = (option: any, searchQuery: string) => { return regex.test(option.value); }; -export class TagFilter extends React.Component { - static defaultProps = { - placeholder: 'Tags', - }; +export const TagFilter: FC = ({ + hideValues, + isClearable, + onChange, + placeholder = 'Filter by tag', + tagOptions, + tags, + width, +}) => { + const theme = useTheme(); + const styles = getStyles(theme); - constructor(props: Props) { - super(props); - } - - onLoadOptions = (query: string) => { - return this.props.tagOptions().then(options => { + const onLoadOptions = (query: string) => { + return tagOptions().then(options => { return options.map(option => ({ value: option.term, label: option.term, @@ -50,61 +52,64 @@ export class TagFilter extends React.Component { }); }; - onChange = (newTags: any[]) => { + const onTagChange = (newTags: any[]) => { // On remove with 1 item returns null, so we need to make sure it's an empty array in that case // https://github.com/JedWatson/react-select/issues/3632 - this.props.onChange((newTags || []).map(tag => tag.value)); + onChange((newTags || []).map(tag => tag.value)); }; - render() { - const styles = getStyles(); + const value = tags.map(tag => ({ value: tag, label: tag, count: 0 })); - const tags = this.props.tags.map(tag => ({ value: tag, label: tag, count: 0 })); - const { width, placeholder, hideValues, isClearable } = this.props; - - const selectOptions = { - defaultOptions: true, - filterOption, - getOptionLabel: (i: any) => i.label, - getOptionValue: (i: any) => i.value, - isClearable, - isMulti: true, - loadOptions: this.onLoadOptions, - loadingMessage: 'Loading...', - noOptionsMessage: 'No tags found', - onChange: this.onChange, - placeholder, - value: tags, - width, - components: { - Option: TagOption, - MultiValueLabel: (): any => { - return null; // We want the whole tag to be clickable so we use MultiValueRemove instead - }, - MultiValueRemove: (props: any) => { - const { data } = props; - - return ( - - - - ); - }, - MultiValueContainer: hideValues ? (): any => null : components.MultiValueContainer, + const selectOptions = { + defaultOptions: true, + filterOption, + getOptionLabel: (i: any) => i.label, + getOptionValue: (i: any) => i.value, + isMulti: true, + loadOptions: onLoadOptions, + loadingMessage: 'Loading...', + noOptionsMessage: 'No tags found', + onChange: onTagChange, + placeholder, + styles: resetSelectStyles(), + value, + width, + components: { + Option: TagOption, + MultiValueLabel: (): any => { + return null; // We want the whole tag to be clickable so we use MultiValueRemove instead }, - }; + MultiValueRemove: (props: any) => { + const { data } = props; - return ( -
- } /> -
- ); - } -} + return ( + + + + ); + }, + MultiValueContainer: hideValues ? (): any => null : components.MultiValueContainer, + }, + }; -const getStyles = stylesFactory(() => { + return ( +
+ {isClearable && tags.length > 0 && ( + onTagChange([])}> + Clear tags + + )} + } /> +
+ ); +}; + +TagFilter.displayName = 'TagFilter'; + +const getStyles = stylesFactory((theme: GrafanaTheme) => { return { tagFilter: css` + position: relative; min-width: 180px; flex-grow: 1; @@ -113,5 +118,18 @@ const getStyles = stylesFactory(() => { cursor: pointer; } `, + clear: css` + text-decoration: underline; + font-size: 12px; + position: absolute; + top: -22px; + right: 0; + cursor: pointer; + color: ${theme.colors.textWeak}; + + &:hover { + color: ${theme.colors.textStrong}; + } + `, }; }); diff --git a/public/app/core/components/TagFilter/TagOption.tsx b/public/app/core/components/TagFilter/TagOption.tsx index 1171485b0bf..07a3e9e8037 100644 --- a/public/app/core/components/TagFilter/TagOption.tsx +++ b/public/app/core/components/TagFilter/TagOption.tsx @@ -1,7 +1,8 @@ -// Libraries -import React from 'react'; -// @ts-ignore -import { components } from '@torkelo/react-select'; +import React, { FC } from 'react'; +import { css, cx } from 'emotion'; +import { useTheme, stylesFactory } from '@grafana/ui'; +import { GrafanaTheme } from '@grafana/data'; + import { OptionProps } from 'react-select/src/components/Option'; import { TagBadge } from './TagBadge'; @@ -10,15 +11,40 @@ interface ExtendedOptionProps extends OptionProps { data: any; } -export const TagOption = (props: ExtendedOptionProps) => { - const { data, className, label } = props; +export const TagOption: FC = ({ data, className, label, isFocused, innerProps }) => { + const theme = useTheme(); + const styles = getStyles(theme); + return ( - +
- +
); }; -export default TagOption; +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + option: css` + padding: 8px; + display: flex; + align-items: center; + flex-direction: row; + white-space: nowrap; + cursor: pointer; + border-left: 2px solid transparent; + &:hover { + background: ${theme.colors.dropdownOptionHoverBg}; + } + `, + optionFocused: css` + background: ${theme.colors.dropdownOptionHoverBg}; + border-style: solid; + border-top: 0; + border-right: 0; + border-bottom: 0; + border-left-width: 2px; + `, + }; +}); diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 370638a586e..8b00995f7ba 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -74,13 +74,15 @@ export class SearchSrv { const filters = hasFilters(options) || query.folderIds?.length > 0; query.folderIds = query.folderIds || []; - if (!filters) { - query.folderIds = [0]; - } + if (query.layout === SearchLayout.List) { return backendSrv.search({ ...query, type: DashboardSearchItemType.DashDB }); } + if (!filters) { + query.folderIds = [0]; + } + if (!options.skipRecent && !filters) { promises.push(this.getRecentDashboards(sections)); } diff --git a/public/app/features/search/components/ActionRow.tsx b/public/app/features/search/components/ActionRow.tsx index 02bee317390..ac6e9b2a98e 100644 --- a/public/app/features/search/components/ActionRow.tsx +++ b/public/app/features/search/components/ActionRow.tsx @@ -8,8 +8,8 @@ import { SearchSrv } from 'app/core/services/search_srv'; import { DashboardQuery, SearchLayout } from '../types'; export const layoutOptions = [ - { label: 'Folders', value: SearchLayout.Folders, icon: 'folder' }, - { label: 'List', value: SearchLayout.List, icon: 'list-ul' }, + { value: SearchLayout.Folders, icon: 'folder' }, + { value: SearchLayout.List, icon: 'list-ul' }, ]; const searchSrv = new SearchSrv(); @@ -39,20 +39,21 @@ export const ActionRow: FC = ({ return (
+
+ + {!hideLayout ? ( + + ) : null} + + +
- {!hideLayout ? ( - - ) : null} - - - - {showStarredFilter && } - + {showStarredFilter && ( +
+ +
+ )} +
); @@ -69,9 +70,17 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => { display: flex; justify-content: space-between; align-items: center; - padding: ${theme.spacing.md} 0; + padding: ${theme.spacing.lg} 0; width: 100%; } `, + rowContainer: css` + margin-right: ${theme.spacing.md}; + `, + checkboxWrapper: css` + label { + line-height: 1.2; + } + `, }; }); diff --git a/public/app/features/search/components/SearchResults.tsx b/public/app/features/search/components/SearchResults.tsx index 0620b590dfc..aea5f6b2f5a 100644 --- a/public/app/features/search/components/SearchResults.tsx +++ b/public/app/features/search/components/SearchResults.tsx @@ -127,7 +127,8 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => { noResults: css` padding: ${md}; background: ${theme.colors.bg2}; - text-style: italic; + font-style: italic; + margin-top: ${theme.spacing.md}; `, listModeWrapper: css` position: relative; diff --git a/public/app/features/search/components/SearchResultsFilter.test.tsx b/public/app/features/search/components/SearchResultsFilter.test.tsx index 056c8f0387a..4d2b62554f2 100644 --- a/public/app/features/search/components/SearchResultsFilter.test.tsx +++ b/public/app/features/search/components/SearchResultsFilter.test.tsx @@ -83,8 +83,8 @@ describe('SearchResultsFilter', () => { wrapper .find({ placeholder: 'Filter by tag' }) .at(0) - .prop('onChange')(tags[0]); + .prop('onChange')([tags[0]]); expect(mockFilterByTags).toHaveBeenCalledTimes(1); - expect(mockFilterByTags).toHaveBeenCalledWith(tags[0]); + expect(mockFilterByTags).toHaveBeenCalledWith(['tag1']); }); });