Commit 6f02b515 by Torkel Ödegaard Committed by GitHub

Search: Improving search look and feel (#23854)

* Search: Improving search look and feel

* Fixed issue with tag filter beeing cramped and wrapping tags

* Minor tag polish

* fixed type
parent 76b184b9
......@@ -236,7 +236,6 @@ export interface GrafanaTheme extends GrafanaThemeCommons {
formSwitchBgHover: string;
formSwitchBgDisabled: string;
formSwitchDot: string;
formCheckboxBg: string;
formCheckboxBgChecked: string;
formCheckboxBgCheckedHover: string;
formCheckboxCheckmark: string;
......
......@@ -50,7 +50,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
* */
&:checked + span {
background: blue;
background: ${theme.colors.formCheckboxBgChecked};
background: ${theme.colors.formInputBg};
border: none;
&:hover {
background: ${theme.colors.formCheckboxBgCheckedHover};
......@@ -74,7 +74,7 @@ export const getCheckboxStyles = stylesFactory((theme: GrafanaTheme) => {
height: ${checkboxSize};
border-radius: ${theme.border.radius.sm};
margin-right: ${theme.spacing.formSpacingBase}px;
background: ${theme.colors.formCheckboxBg};
background: ${theme.colors.formInputBg};
border: 1px solid ${theme.colors.formInputBorder};
position: absolute;
top: 1px;
......
......@@ -17,7 +17,7 @@ export interface RadioButtonProps {
}
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize, fullWidth?: boolean) => {
const { fontSize, height } = getPropertiesForButtonSize({
const { fontSize, height, padding } = getPropertiesForButtonSize({
theme,
size,
hasIcon: false,
......@@ -25,7 +25,6 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
variant: 'secondary',
});
const horizontalPadding = theme.spacing[size] ?? theme.spacing.md;
const c = theme.palette;
const textColor = theme.colors.textSemiWeak;
const textColorHover = theme.colors.text;
......@@ -75,7 +74,7 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
// Deduct border from line-height for perfect vertical centering on windows and linux
line-height: ${height - 2}px;
color: ${textColor};
padding: 0 ${horizontalPadding};
padding: ${padding};
margin-left: -1px;
border-radius: ${theme.border.radius.sm};
border: ${border};
......
......@@ -45,11 +45,10 @@ const getTagStyles = (theme: GrafanaTheme, name: string, colorIndex?: number) =>
line-height: ${theme.typography.lineHeight.xs};
vertical-align: baseline;
background-color: ${colors.color};
color: ${theme.palette.white};
color: ${theme.colors.textStrong};
white-space: nowrap;
text-shadow: none;
padding: 3px 6px;
border: 1px solid ${colors.borderColor};
border-radius: ${theme.border.radius.md};
:hover {
......
......@@ -27,12 +27,9 @@ const getStyles = () => {
display: flex;
flex: 1 1 auto;
flex-wrap: wrap;
padding: 10px;
`,
tag: css`
margin-left: 6px;
font-size: 11px;
padding: 2px 6px;
`,
};
};
......@@ -81,7 +81,6 @@ const form = {
formSwitchBgActiveHover: basicColors.blue80,
formSwitchBgDisabled: basicColors.gray25,
formSwitchDot: basicColors.gray15,
formCheckboxBg: basicColors.dark5,
formCheckboxBgChecked: basicColors.blue95,
formCheckboxBgCheckedHover: basicColors.blue80,
formCheckboxCheckmark: basicColors.gray25,
......
......@@ -80,7 +80,6 @@ const form = {
formSwitchBgActiveHover: basicColors.blue80,
formSwitchBgDisabled: basicColors.gray4,
formSwitchDot: basicColors.white,
formCheckboxBg: basicColors.white,
formCheckboxBgChecked: basicColors.blue77,
formCheckboxBgCheckedHover: basicColors.blue80,
formCheckboxCheckmark: basicColors.white,
......
......@@ -15,11 +15,12 @@ export class TagBadge extends React.Component<Props, any> {
render() {
const { label, removeIcon, count } = this.props;
const { color, borderColor } = getTagColorsFromName(label);
const { color } = getTagColorsFromName(label);
const tagStyle = {
backgroundColor: color,
borderColor: borderColor,
};
const countLabel = count !== 0 && <span className="tag-count-label">{`(${count})`}</span>;
return (
......
......@@ -107,17 +107,11 @@ const getStyles = stylesFactory(() => {
return {
tagFilter: css`
min-width: 180px;
line-height: 22px;
flex-grow: 1;
.label-tag {
margin-left: 6px;
font-size: 11px;
cursor: pointer;
.fa.fa-remove {
margin-right: 3px;
}
}
`,
};
......
......@@ -14,7 +14,7 @@ export const TagOption = (props: ExtendedOptionProps) => {
const { data, className, label } = props;
return (
<components.Option {...props}>
<div className={`tag-filter-option btn btn-link ${className || ''}`}>
<div className={`tag-filter-option ${className || ''}`}>
<TagBadge label={label} removeIcon={false} count={data.count} />
</div>
</components.Option>
......
......@@ -44,7 +44,7 @@ export const ActionRow: FC<Props> = ({
return (
<div className={styles.actionRow}>
<HorizontalGroup spacing="md" width="100%">
<HorizontalGroup spacing="md">
{!hideLayout ? <RadioButtonGroup options={layoutOptions} onChange={onLayoutChange} value={layout} /> : null}
<SortPicker onChange={onSortChange} value={query.sort} />
</HorizontalGroup>
......
......@@ -192,10 +192,6 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
height: 100%;
.results-container {
padding: 5px 0 0;
}
`,
searchField: css`
height: auto;
......
......@@ -22,6 +22,7 @@ const getStyles = stylesFactory(() => ({
// Vertically align absolutely positioned checkbox element
wrapper: css`
height: 21px;
margin-right: 12px;
& > label {
height: 100%;
}
......
import React from 'react';
import { shallow, mount } from 'enzyme';
import { mount } from 'enzyme';
import { Tag } from '@grafana/ui';
import { SearchItem, Props } from './SearchItem';
import { DashboardSearchItemType } from '../types';
......@@ -17,7 +17,7 @@ const data = {
checked: false,
};
const setup = (propOverrides?: Partial<Props>, renderMethod = shallow) => {
const setup = (propOverrides?: Partial<Props>) => {
const props: Props = {
item: data,
onTagSelected: jest.fn(),
......@@ -28,7 +28,7 @@ const setup = (propOverrides?: Partial<Props>, renderMethod = shallow) => {
Object.assign(props, propOverrides);
const wrapper = renderMethod(<SearchItem {...props} />);
const wrapper = mount(<SearchItem {...props} />);
const instance = wrapper.instance();
return {
......@@ -39,14 +39,14 @@ const setup = (propOverrides?: Partial<Props>, renderMethod = shallow) => {
describe('SearchItem', () => {
it('should render the item', () => {
const { wrapper } = setup();
const { wrapper } = setup({});
expect(wrapper.find({ 'aria-label': 'Dashboard search item Test 1' })).toHaveLength(1);
expect(wrapper.findWhere(comp => comp.type() === 'div' && comp.text() === 'Test 1')).toHaveLength(1);
});
it("should render item's tags", () => {
// @ts-ignore
const { wrapper } = setup({}, mount);
const { wrapper } = setup({});
expect(wrapper.find(Tag)).toHaveLength(2);
});
});
import React, { FC, useCallback, useRef, useEffect, CSSProperties } from 'react';
import React, { FC, useCallback, CSSProperties } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { e2e } from '@grafana/e2e';
import { Icon, useTheme, TagList, styleMixins, stylesFactory } from '@grafana/ui';
import { updateLocation } from 'app/core/reducers/location';
import { useTheme, TagList, styleMixins, stylesFactory } from '@grafana/ui';
import { DashboardSectionItem, OnToggleChecked } from '../types';
import { SearchCheckbox } from './SearchCheckbox';
import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants';
export interface Props {
item: DashboardSectionItem;
......@@ -20,30 +20,6 @@ const { selectors } = e2e.pages.Dashboards;
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected, style }) => {
const theme = useTheme();
const styles = getResultsItemStyles(theme);
const inputEl = useRef<HTMLInputElement>(null);
useEffect(() => {
const preventDef = (event: MouseEvent) => {
// manually prevent default on TagList click, as doing it via normal onClick doesn't work inside angular
event.preventDefault();
};
if (inputEl.current) {
inputEl.current.addEventListener('click', preventDef);
}
return () => {
inputEl.current!.removeEventListener('click', preventDef);
};
}, []);
const onItemClick = () => {
//Check if one string can be found in the other
if (window.location.pathname.includes(item.url) || item.url.includes(window.location.pathname)) {
updateLocation({
query: { search: null },
partial: true,
});
}
};
const tagSelected = useCallback((tag: string, event: React.MouseEvent<HTMLElement>) => {
onTagSelected(tag);
......@@ -60,23 +36,21 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
);
return (
<li
<div
style={style}
aria-label={selectors.dashboards(item.title)}
className={cx(styles.wrapper, { [styles.selected]: item.selected })}
>
<SearchCheckbox editable={editable} checked={item.checked} onClick={toggleItem} />
<a href={item.url} className={styles.link}>
<Icon className={styles.icon} name="apps" size="lg" />
<div className={styles.body} onClick={onItemClick}>
<div className={styles.body}>
<span>{item.title}</span>
<span className={styles.folderTitle}>{item.folderTitle}</span>
</div>
<span ref={inputEl}>
<TagList tags={item.tags} onClick={tagSelected} className={styles.tags} />
</span>
</a>
</li>
<TagList tags={item.tags} onClick={tagSelected} className={styles.tags} />
</div>
);
};
......@@ -85,8 +59,9 @@ const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({
${styleMixins.listItem(theme)};
display: flex;
align-items: center;
padding: 0 ${theme.spacing.sm};
min-height: 37px;
height: ${SEARCH_ITEM_HEIGHT}px;
margin-bottom: ${SEARCH_ITEM_MARGIN}px;
padding: 0 ${theme.spacing.md};
:hover {
cursor: pointer;
......@@ -101,7 +76,6 @@ const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({
justify-content: center;
flex: 1 1 auto;
overflow: hidden;
padding: 0 10px;
`,
folderTitle: css`
color: ${theme.colors.textWeak};
......@@ -114,6 +88,7 @@ const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({
margin-left: 10px;
`,
tags: css`
flex-grow: 0;
justify-content: flex-end;
@media only screen and (max-width: ${theme.breakpoints.md}) {
display: none;
......@@ -122,6 +97,7 @@ const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({
link: css`
display: flex;
align-items: center;
width: 100%;
flex-shrink: 0;
flex-grow: 1;
`,
}));
import React, { FC } from 'react';
import { css, cx } from 'emotion';
import { css } from 'emotion';
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme } from '@grafana/data';
import { stylesFactory, useTheme, Spinner } from '@grafana/ui';
import { DashboardSection, OnToggleChecked, SearchLayout } from '../types';
import { getVisibleItems } from '../utils';
import { ITEM_HEIGHT } from '../constants';
import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants';
import { SearchItem } from './SearchItem';
import { SectionHeader } from './SectionHeader';
......@@ -35,18 +35,18 @@ export const SearchResults: FC<Props> = ({
const renderFolders = () => {
return (
<ul className={styles.wrapper}>
<div className={styles.wrapper}>
{results.map(section => {
return (
<li aria-label="Search section" className={styles.section} key={section.title}>
<div aria-label="Search section" className={styles.section} key={section.title}>
<SectionHeader onSectionClick={onToggleSection} {...{ onToggleChecked, editable, section }} />
<ul aria-label="Search items">
<div aria-label="Search items" className={styles.sectionItems}>
{section.expanded && section.items.map(item => <SearchItem key={item.id} {...itemProps} item={item} />)}
</ul>
</li>
</div>
</div>
);
})}
</ul>
</div>
);
};
......@@ -54,24 +54,26 @@ export const SearchResults: FC<Props> = ({
const renderDashboards = () => {
return (
<AutoSizer disableWidth>
{({ height }) => (
<FixedSizeList
aria-label="Search items"
className={styles.wrapper}
innerElementType="ul"
itemSize={ITEM_HEIGHT}
height={height}
itemCount={items.length}
width="100%"
>
{({ index, style }) => {
const item = items[index];
return <SearchItem key={item.id} {...itemProps} item={item} style={style} />;
}}
</FixedSizeList>
)}
</AutoSizer>
<div className={styles.listModeWrapper}>
<AutoSizer disableWidth>
{({ height }) => (
<FixedSizeList
aria-label="Search items"
className={styles.wrapper}
innerElementType="ul"
itemSize={SEARCH_ITEM_HEIGHT + SEARCH_ITEM_MARGIN}
height={height}
itemCount={items.length}
width="100%"
>
{({ index, style }) => {
const item = items[index];
return <SearchItem key={item.id} {...itemProps} item={item} style={style} />;
}}
</FixedSizeList>
)}
</AutoSizer>
</div>
);
};
......@@ -80,24 +82,28 @@ export const SearchResults: FC<Props> = ({
} else if (!results || !results.length) {
return <h6>No dashboards matching your query were found.</h6>;
}
return (
<div className={cx('results-container', styles.resultsContainer)}>
{layout !== SearchLayout.List ? renderFolders() : renderDashboards()}
</div>
<div className={styles.resultsContainer}>{layout !== SearchLayout.List ? renderFolders() : renderDashboards()}</div>
);
};
const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
const { xs, sm, md } = theme.spacing;
const { md } = theme.spacing;
return {
wrapper: css`
list-style: none;
display: flex;
flex-direction: column;
`,
section: css`
display: flex;
flex-direction: column;
background: ${theme.colors.panelBg};
border-bottom: solid 1px ${theme.isLight ? theme.palette.gray95 : theme.palette.gray25};
padding: 0px ${xs} ${xs};
margin-bottom: 3px;
border-bottom: solid 1px ${theme.colors.border2};
`,
sectionItems: css`
margin: 0 24px 0 32px;
`,
spinner: css`
display: flex;
......@@ -106,14 +112,18 @@ const getSectionStyles = stylesFactory((theme: GrafanaTheme) => {
min-height: 100px;
`,
resultsContainer: css`
padding: ${sm};
position: relative;
flex-grow: 10;
margin-bottom: ${md};
background: ${theme.palette.gray10};
border: 1px solid ${theme.palette.gray15};
background: ${theme.colors.bg1};
border: 1px solid ${theme.colors.border1};
border-radius: 3px;
height: 100%;
`,
listModeWrapper: css`
position: relative;
height: 100%;
padding: ${md};
`,
};
});
......@@ -19,7 +19,7 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
editable = false,
}) => {
const theme = useTheme();
const styles = getSectionHeaderStyles(theme, section.selected);
const styles = getSectionHeaderStyles(theme, section.selected, editable);
const onSectionExpand = () => {
onSectionClick(section);
......@@ -39,7 +39,10 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
return (
<div className={styles.wrapper} onClick={onSectionExpand}>
<SearchCheckbox editable={editable} checked={section.checked} onClick={onSectionChecked} />
<Icon className={styles.icon} name={section.icon as IconName} />
<div className={styles.icon}>
<Icon name={section.icon as IconName} />
</div>
<span className={styles.text}>{section.title}</span>
{section.url && (
......@@ -52,15 +55,15 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
);
};
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false) => {
const { sm, xs } = theme.spacing;
const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = false, editable: boolean) => {
const { sm } = theme.spacing;
return {
wrapper: cx(
css`
display: flex;
align-items: center;
font-size: ${theme.typography.size.base};
padding: ${sm} ${xs} ${xs};
padding: 12px;
color: ${theme.colors.textWeak};
&:hover,
......@@ -78,7 +81,7 @@ const getSectionHeaderStyles = stylesFactory((theme: GrafanaTheme, selected = fa
{ selected }
),
icon: css`
width: 43px;
padding: 0 ${sm} 0 ${editable ? 0 : sm};
`,
text: css`
flex-grow: 1;
......
export const NO_ID_SECTIONS = ['Recent', 'Starred'];
// Height of the search result item
export const ITEM_HEIGHT = 40;
export const SEARCH_ITEM_HEIGHT = 48;
export const SEARCH_ITEM_MARGIN = 4;
export const DEFAULT_SORT = { label: 'A-Z', value: 'alpha-asc' };
......@@ -20,9 +20,14 @@
white-space: nowrap;
border-radius: 3px;
text-shadow: none;
font-size: 13px;
font-size: 12px;
padding: 0px 6px;
border: 1px solid lighten($purple, 10%);
line-height: 20px;
height: 20px;
svg {
margin-bottom: 0;
}
.icon-tag {
position: relative;
......
......@@ -46,30 +46,30 @@
}
.tag-filter {
line-height: 22px;
flex-grow: 1;
.label-tag {
margin-left: 6px;
font-size: 11px;
cursor: pointer;
.fa.fa-remove {
margin-right: 3px;
}
}
}
.tag-filter-option {
position: relative;
text-align: left;
width: 100%;
display: block;
border-radius: 0;
}
.tag-filter-option {
position: relative;
text-align: left;
width: 100%;
display: block;
border-radius: 0;
cursor: pointer;
padding: 2px 0;
}
.tag-count-label {
margin-left: 3px;
}
.tag-count-label {
margin-left: 3px;
}
.tag-filter-values {
......
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