Commit 32492dd6 by Alex Khomenko Committed by GitHub

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
parent 295e1524
// 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<Props, any> {
static defaultProps = {
placeholder: 'Tags',
};
constructor(props: Props) {
super(props);
}
export const TagFilter: FC<Props> = ({
hideValues,
isClearable,
onChange,
placeholder = 'Filter by tag',
tagOptions,
tags,
width,
}) => {
const theme = useTheme();
const styles = getStyles(theme);
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<Props, any> {
});
};
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 tags = this.props.tags.map(tag => ({ value: tag, label: tag, count: 0 }));
const { width, placeholder, hideValues, isClearable } = this.props;
const value = tags.map(tag => ({ value: tag, label: tag, count: 0 }));
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;
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 (
<components.MultiValueRemove {...props}>
<TagBadge key={data.label} label={data.label} removeIcon={true} count={data.count} />
</components.MultiValueRemove>
);
},
MultiValueContainer: hideValues ? (): any => null : components.MultiValueContainer,
return (
<components.MultiValueRemove {...props}>
<TagBadge key={data.label} label={data.label} removeIcon={true} count={data.count} />
</components.MultiValueRemove>
);
},
};
MultiValueContainer: hideValues ? (): any => null : components.MultiValueContainer,
},
};
return (
<div className={styles.tagFilter}>
<AsyncSelect {...selectOptions} prefix={<Icon name="tag-alt" />} />
</div>
);
}
}
return (
<div className={styles.tagFilter}>
{isClearable && tags.length > 0 && (
<span className={styles.clear} onClick={() => onTagChange([])}>
Clear tags
</span>
)}
<AsyncSelect {...selectOptions} prefix={<Icon name="tag-alt" />} />
</div>
);
};
TagFilter.displayName = 'TagFilter';
const getStyles = stylesFactory(() => {
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};
}
`,
};
});
// 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<any> {
data: any;
}
export const TagOption = (props: ExtendedOptionProps) => {
const { data, className, label } = props;
export const TagOption: FC<ExtendedOptionProps> = ({ data, className, label, isFocused, innerProps }) => {
const theme = useTheme();
const styles = getStyles(theme);
return (
<components.Option {...props}>
<div className={cx(styles.option, isFocused && styles.optionFocused)} aria-label="Tag option" {...innerProps}>
<div className={`tag-filter-option ${className || ''}`}>
<TagBadge label={label} removeIcon={false} count={data.count} />
</div>
</components.Option>
</div>
);
};
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;
`,
};
});
......@@ -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));
}
......
......@@ -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<Props> = ({
return (
<div className={styles.actionRow}>
<div className={styles.rowContainer}>
<HorizontalGroup spacing="md" width="auto">
{!hideLayout ? (
<RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={query.layout} />
) : null}
<SortPicker onChange={onSortChange} value={query.sort} />
</HorizontalGroup>
</div>
<HorizontalGroup spacing="md" width="auto">
{!hideLayout ? (
<RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={query.layout} />
) : null}
<SortPicker onChange={onSortChange} value={query.sort} />
</HorizontalGroup>
<HorizontalGroup spacing="md" width="auto">
{showStarredFilter && <Checkbox label="Filter by starred" onChange={onStarredFilterChange} />}
<TagFilter
placeholder="Filter by tag"
tags={query.tag}
tagOptions={searchSrv.getDashboardTags}
onChange={onTagFilterChange}
/>
{showStarredFilter && (
<div className={styles.checkboxWrapper}>
<Checkbox label="Filter by starred" onChange={onStarredFilterChange} />
</div>
)}
<TagFilter isClearable tags={query.tag} tagOptions={searchSrv.getDashboardTags} onChange={onTagFilterChange} />
</HorizontalGroup>
</div>
);
......@@ -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;
}
`,
};
});
......@@ -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;
......
......@@ -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']);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment