Commit c21e45e4 by Alex Khomenko Committed by GitHub

Search: display sort metadata (#31167)

* Search: display metadata

* Search: update SortPicker icon

* Search: display folder meta data

* Search: reset sort picker on layout change

* Search: align tags in Card component

* Search: replace hyphen with dash

* Search: preserve sort state on layout change

* Search: update tests

* Search: fix tests

* Update pkg/services/search/hits.go

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update public/app/features/search/components/SearchItem.tsx

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update public/app/features/search/components/SearchItem.tsx

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Update public/app/features/search/types.ts

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>

* Search: fix type error

* Search: add General folder name and adjust icon margin

Co-authored-by: Agnès Toulet <35176601+AgnesToulet@users.noreply.github.com>
parent c0015034
import React from 'react'; import React from 'react';
import { action } from '@storybook/addon-actions';
import { Story } from '@storybook/react'; import { Story } from '@storybook/react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { Card, Props } from './Card'; import { Card, Props } from './Card';
...@@ -133,9 +132,6 @@ export const Full: Story<Props> = ({ disabled }) => { ...@@ -133,9 +132,6 @@ export const Full: Story<Props> = ({ disabled }) => {
<Card.Figure> <Card.Figure>
<img src={logo} alt="Prometheus Logo" /> <img src={logo} alt="Prometheus Logo" />
</Card.Figure> </Card.Figure>
<Card.Tags>
<TagList tags={['firing', 'active', 'test', 'testdata', 'prometheus']} onClick={action('Clicked tag')} />
</Card.Tags>
<Card.Actions> <Card.Actions>
<Button key="settings" variant="secondary"> <Button key="settings" variant="secondary">
Main action Main action
......
...@@ -115,12 +115,14 @@ export const Card: CardInterface = ({ ...@@ -115,12 +115,14 @@ export const Card: CardInterface = ({
{figure} {figure}
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.info}> <div className={styles.info}>
<div className={styles.heading} role="heading"> <div>
{heading} <div className={styles.heading} role="heading">
{tags} {heading}
</div>
{meta}
{description && <p className={styles.description}>{description}</p>}
</div> </div>
{meta} {tags}
{description && <p className={styles.description}>{description}</p>}
</div> </div>
{hasActions && ( {hasActions && (
<div className={styles.actionRow}> <div className={styles.actionRow}>
...@@ -197,12 +199,14 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -197,12 +199,14 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
`, `,
info: css` info: css`
display: flex; display: flex;
flex-direction: column; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
width: 100%; width: 100%;
`, `,
metadata: css` metadata: css`
display: flex;
align-items: center;
width: 100%; width: 100%;
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
color: ${theme.colors.textSemiWeak}; color: ${theme.colors.textSemiWeak};
...@@ -294,7 +298,7 @@ const Meta: FC<ChildProps & { separator?: string }> = memo(({ children, styles, ...@@ -294,7 +298,7 @@ const Meta: FC<ChildProps & { separator?: string }> = memo(({ children, styles,
let meta = children; let meta = children;
// Join meta data elements by separator // Join meta data elements by separator
if (Array.isArray(children)) { if (Array.isArray(children) && separator) {
meta = React.Children.toArray(children).reduce((prev, curr, i) => [ meta = React.Children.toArray(children).reduce((prev, curr, i) => [
prev, prev,
<span key={`separator_${i}`} className={styles?.separator}> <span key={`separator_${i}`} className={styles?.separator}>
......
...@@ -11,20 +11,21 @@ const ( ...@@ -11,20 +11,21 @@ const (
) )
type Hit struct { type Hit struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UID string `json:"uid"` UID string `json:"uid"`
Title string `json:"title"` Title string `json:"title"`
URI string `json:"uri"` URI string `json:"uri"`
URL string `json:"url"` URL string `json:"url"`
Slug string `json:"slug"` Slug string `json:"slug"`
Type HitType `json:"type"` Type HitType `json:"type"`
Tags []string `json:"tags"` Tags []string `json:"tags"`
IsStarred bool `json:"isStarred"` IsStarred bool `json:"isStarred"`
FolderID int64 `json:"folderId,omitempty"` FolderID int64 `json:"folderId,omitempty"`
FolderUID string `json:"folderUid,omitempty"` FolderUID string `json:"folderUid,omitempty"`
FolderTitle string `json:"folderTitle,omitempty"` FolderTitle string `json:"folderTitle,omitempty"`
FolderURL string `json:"folderUrl,omitempty"` FolderURL string `json:"folderUrl,omitempty"`
SortMeta string `json:"sortMeta,omitempty"` SortMeta int64 `json:"sortMeta"`
SortMetaName string `json:"sortMetaName,omitempty"`
} }
type HitList []*Hit type HitList []*Hit
......
package sqlstore package sqlstore
import ( import (
"fmt"
"strings" "strings"
"time" "time"
...@@ -331,7 +330,8 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard ...@@ -331,7 +330,8 @@ func makeQueryResult(query *search.FindPersistedDashboardsQuery, res []Dashboard
} }
if query.Sort.MetaName != "" { if query.Sort.MetaName != "" {
hit.SortMeta = strings.TrimSpace(fmt.Sprintf("%d %s", item.SortMeta, query.Sort.MetaName)) hit.SortMeta = item.SortMeta
hit.SortMetaName = query.Sort.MetaName
} }
query.Result = append(query.Result, hit) query.Result = append(query.Result, hit)
......
import React, { FC } from 'react'; import React, { FC } from 'react';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { Select, Icon } from '@grafana/ui'; import { Select, Icon, IconName } from '@grafana/ui';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { DEFAULT_SORT } from 'app/features/search/constants'; import { DEFAULT_SORT } from 'app/features/search/constants';
import { SearchSrv } from '../../services/search_srv'; import { SearchSrv } from '../../services/search_srv';
...@@ -9,7 +9,7 @@ const searchSrv = new SearchSrv(); ...@@ -9,7 +9,7 @@ const searchSrv = new SearchSrv();
export interface Props { export interface Props {
onChange: (sortValue: SelectableValue) => void; onChange: (sortValue: SelectableValue) => void;
value?: SelectableValue | null; value?: string;
placeholder?: string; placeholder?: string;
} }
...@@ -23,14 +23,16 @@ export const SortPicker: FC<Props> = ({ onChange, value, placeholder }) => { ...@@ -23,14 +23,16 @@ export const SortPicker: FC<Props> = ({ onChange, value, placeholder }) => {
// Using sync Select and manual options fetching here since we need to find the selected option by value // Using sync Select and manual options fetching here since we need to find the selected option by value
const { loading, value: options } = useAsync<SelectableValue[]>(getSortOptions, []); const { loading, value: options } = useAsync<SelectableValue[]>(getSortOptions, []);
const selected = options?.filter((opt) => opt.value === value);
return !loading ? ( return !loading ? (
<Select <Select
key={value}
width={25} width={25}
onChange={onChange} onChange={onChange}
value={options?.filter((opt) => opt.value === value)} value={selected?.length ? selected : null}
options={options} options={options}
placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`} placeholder={placeholder ?? `Sort (Default ${DEFAULT_SORT.label})`}
prefix={<Icon name="sort-amount-down" />} prefix={<Icon name={(value?.includes('asc') ? 'sort-amount-up' : 'sort-amount-down') as IconName} />}
/> />
) : null; ) : null;
}; };
...@@ -50,6 +50,7 @@ describe('DashboardSearch', () => { ...@@ -50,6 +50,7 @@ describe('DashboardSearch', () => {
folderIds: [], folderIds: [],
layout: SearchLayout.Folders, layout: SearchLayout.Folders,
sort: undefined, sort: undefined,
prevSort: null,
}); });
}); });
...@@ -71,6 +72,7 @@ describe('DashboardSearch', () => { ...@@ -71,6 +72,7 @@ describe('DashboardSearch', () => {
folderIds: [], folderIds: [],
layout: SearchLayout.Folders, layout: SearchLayout.Folders,
sort: undefined, sort: undefined,
prevSort: null,
}); });
}); });
...@@ -110,6 +112,7 @@ describe('DashboardSearch', () => { ...@@ -110,6 +112,7 @@ describe('DashboardSearch', () => {
folderIds: [], folderIds: [],
layout: SearchLayout.Folders, layout: SearchLayout.Folders,
sort: undefined, sort: undefined,
prevSort: null,
}) })
); );
}); });
......
import React, { FC, useCallback } from 'react'; import React, { FC, useCallback } from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { TagList, Card, useStyles } from '@grafana/ui'; import { TagList, Card, useStyles, Icon, IconName } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { DashboardSectionItem, OnToggleChecked } from '../types'; import { DashboardSectionItem, OnToggleChecked } from '../types';
import { SearchCheckbox } from './SearchCheckbox'; import { SearchCheckbox } from './SearchCheckbox';
...@@ -16,6 +16,15 @@ export interface Props { ...@@ -16,6 +16,15 @@ export interface Props {
const selectors = e2eSelectors.pages.Dashboards; const selectors = e2eSelectors.pages.Dashboards;
const getIconFromMeta = (meta = ''): IconName => {
const metaIconMap = new Map<string, IconName>([
['errors', 'info-circle'],
['views', 'eye'],
]);
return metaIconMap.has(meta) ? metaIconMap.get(meta)! : 'sort-amount-down';
};
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected }) => { export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected }) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const tagSelected = useCallback((tag: string, event: React.MouseEvent<HTMLElement>) => { const tagSelected = useCallback((tag: string, event: React.MouseEvent<HTMLElement>) => {
...@@ -32,6 +41,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe ...@@ -32,6 +41,7 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
[item] [item]
); );
const folderTitle = item.folderTitle || 'General';
return ( return (
<Card <Card
aria-label={selectors.dashboards(item.title)} aria-label={selectors.dashboards(item.title)}
...@@ -43,7 +53,18 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe ...@@ -43,7 +53,18 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
<Card.Figure align={'center'}> <Card.Figure align={'center'}>
<SearchCheckbox editable={editable} checked={item.checked} onClick={toggleItem} /> <SearchCheckbox editable={editable} checked={item.checked} onClick={toggleItem} />
</Card.Figure> </Card.Figure>
{item.folderTitle && <Card.Meta>{item.folderTitle}</Card.Meta>} <Card.Meta separator={''}>
<span className={styles.metaContainer}>
<Icon name={'folder'} />
{folderTitle}
</span>
{item.sortMetaName && (
<span className={styles.metaContainer}>
<Icon name={getIconFromMeta(item.sortMetaName)} />
{item.sortMeta} {item.sortMetaName}
</span>
)}
</Card.Meta>
<Card.Tags> <Card.Tags>
<TagList tags={item.tags} onClick={tagSelected} /> <TagList tags={item.tags} onClick={tagSelected} />
</Card.Tags> </Card.Tags>
...@@ -56,5 +77,15 @@ const getStyles = (theme: GrafanaTheme) => { ...@@ -56,5 +77,15 @@ const getStyles = (theme: GrafanaTheme) => {
container: css` container: css`
padding: ${theme.spacing.sm} ${theme.spacing.md}; padding: ${theme.spacing.sm} ${theme.spacing.md};
`, `,
metaContainer: css`
display: flex;
align-items: center;
margin-right: ${theme.spacing.sm};
svg {
margin-right: ${theme.spacing.xs};
margin-bottom: 0;
}
`,
}; };
}; };
...@@ -15,6 +15,7 @@ beforeEach(() => { ...@@ -15,6 +15,7 @@ beforeEach(() => {
const searchQuery = { const searchQuery = {
starred: false, starred: false,
sort: null, sort: null,
prevSort: null,
tag: ['tag'], tag: ['tag'],
query: '', query: '',
skipRecent: true, skipRecent: true,
......
...@@ -60,7 +60,6 @@ export const SectionHeader: FC<SectionHeaderProps> = ({ ...@@ -60,7 +60,6 @@ export const SectionHeader: FC<SectionHeaderProps> = ({
</a> </a>
)} )}
</div> </div>
{section.itemsFetching ? <Spinner /> : <Icon name={section.expanded ? 'angle-down' : 'angle-right'} />} {section.itemsFetching ? <Spinner /> : <Icon name={section.expanded ? 'angle-down' : 'angle-right'} />}
</div> </div>
); );
......
...@@ -2,6 +2,6 @@ export const NO_ID_SECTIONS = ['Recent', 'Starred']; ...@@ -2,6 +2,6 @@ export const NO_ID_SECTIONS = ['Recent', 'Starred'];
// Height of the search result item // Height of the search result item
export const SEARCH_ITEM_HEIGHT = 62; export const SEARCH_ITEM_HEIGHT = 62;
export const SEARCH_ITEM_MARGIN = 8; export const SEARCH_ITEM_MARGIN = 8;
export const DEFAULT_SORT = { label: 'A-Z', value: 'alpha-asc' }; export const DEFAULT_SORT = { label: 'A\u2013Z', value: 'alpha-asc' };
export const SECTION_STORAGE_KEY = 'search.sections'; export const SECTION_STORAGE_KEY = 'search.sections';
export const GENERAL_FOLDER_ID = 0; export const GENERAL_FOLDER_ID = 0;
...@@ -51,6 +51,10 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>, updateLocat ...@@ -51,6 +51,10 @@ export const useSearchQuery = (queryParams: Partial<DashboardQuery>, updateLocat
const onLayoutChange = (layout: SearchLayout) => { const onLayoutChange = (layout: SearchLayout) => {
dispatch({ type: LAYOUT_CHANGE, payload: layout }); dispatch({ type: LAYOUT_CHANGE, payload: layout });
if (layout === SearchLayout.Folders) {
updateLocationQuery({ layout, sort: null });
return;
}
updateLocationQuery({ layout }); updateLocationQuery({ layout });
}; };
......
...@@ -20,6 +20,7 @@ export const defaultQuery: DashboardQuery = { ...@@ -20,6 +20,7 @@ export const defaultQuery: DashboardQuery = {
folderIds: [], folderIds: [],
sort: null, sort: null,
layout: SearchLayout.Folders, layout: SearchLayout.Folders,
prevSort: null,
}; };
export const defaultQueryParams: RouteParams = { export const defaultQueryParams: RouteParams = {
...@@ -58,9 +59,9 @@ export const queryReducer = (state: DashboardQuery, action: SearchAction) => { ...@@ -58,9 +59,9 @@ export const queryReducer = (state: DashboardQuery, action: SearchAction) => {
case LAYOUT_CHANGE: { case LAYOUT_CHANGE: {
const layout = action.payload; const layout = action.payload;
if (state.sort && layout === SearchLayout.Folders) { if (state.sort && layout === SearchLayout.Folders) {
return { ...state, layout, sort: null }; return { ...state, layout, sort: null, prevSort: state.sort };
} }
return { ...state, layout }; return { ...state, layout, sort: state.prevSort };
} }
default: default:
return state; return state;
......
...@@ -41,6 +41,8 @@ export interface DashboardSectionItem { ...@@ -41,6 +41,8 @@ export interface DashboardSectionItem {
uid?: string; uid?: string;
uri: string; uri: string;
url: string; url: string;
sortMeta?: number;
sortMetaName?: string;
} }
export interface DashboardSearchHit extends DashboardSectionItem, DashboardSection {} export interface DashboardSearchHit extends DashboardSectionItem, DashboardSection {}
...@@ -67,6 +69,8 @@ export interface DashboardQuery { ...@@ -67,6 +69,8 @@ export interface DashboardQuery {
skipStarred: boolean; skipStarred: boolean;
folderIds: number[]; folderIds: number[];
sort: SelectableValue | null; sort: SelectableValue | null;
// Save sorting data between layouts
prevSort: SelectableValue | null;
layout: SearchLayout; layout: SearchLayout;
} }
......
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