mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
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
|
||||
|
@@ -1,7 +1,6 @@
|
||||
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)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
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,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
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;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user