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