Commit 0fceca5f by Alex Khomenko Committed by GitHub

Search: use Card component (#29892)

* Search: Use Card component for search items

* Search: Set min height of search item

* Search: Adjust item height

* Search: Move tags to the right

* Search: Center item content

* Search: Align tags

* Search: Adjust Card spacing

* Search: Adjust dimensions
parent 77ef9abe
...@@ -114,12 +114,14 @@ export const Card: CardInterface = ({ ...@@ -114,12 +114,14 @@ export const Card: CardInterface = ({
<CardInner href={href}> <CardInner href={href}>
{figure} {figure}
<div className={styles.inner}> <div className={styles.inner}>
<div className={styles.info}>
<div className={styles.heading} role="heading"> <div className={styles.heading} role="heading">
{heading} {heading}
{tags}
</div> </div>
{meta} {meta}
{tags}
{description && <p className={styles.description}>{description}</p>} {description && <p className={styles.description}>{description}</p>}
</div>
{hasActions && ( {hasActions && (
<div className={styles.actionRow}> <div className={styles.actionRow}>
{actions} {actions}
...@@ -178,20 +180,37 @@ export const getContainerStyles = stylesFactory((theme: GrafanaTheme, disabled = ...@@ -178,20 +180,37 @@ export const getContainerStyles = stylesFactory((theme: GrafanaTheme, disabled =
export const getCardStyles = stylesFactory((theme: GrafanaTheme) => { export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
return { return {
inner: css` inner: css`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%; width: 100%;
flex-wrap: wrap;
`, `,
heading: css` heading: css`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 0; margin-bottom: 0;
font-size: ${theme.typography.size.md}; font-size: ${theme.typography.size.md};
line-height: ${theme.typography.lineHeight.xs}; line-height: ${theme.typography.lineHeight.xs};
`, `,
info: css`
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 100%;
`,
metadata: css` metadata: css`
width: 100%;
font-size: ${theme.typography.size.sm}; font-size: ${theme.typography.size.sm};
color: ${theme.colors.textSemiWeak}; color: ${theme.colors.textSemiWeak};
margin: ${theme.spacing.sm} 0 0; margin: ${theme.spacing.xs} 0 0;
line-height: ${theme.typography.lineHeight.xs}; line-height: ${theme.typography.lineHeight.xs};
`, `,
description: css` description: css`
width: 100%;
margin: ${theme.spacing.sm} 0 0; margin: ${theme.spacing.sm} 0 0;
color: ${theme.colors.textSemiWeak}; color: ${theme.colors.textSemiWeak};
line-height: ${theme.typography.lineHeight.md}; line-height: ${theme.typography.lineHeight.md};
...@@ -202,6 +221,10 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -202,6 +221,10 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
& > * { & > * {
width: 100%; width: 100%;
} }
&:empty {
display: none;
}
`, `,
actionRow: css` actionRow: css`
display: flex; display: flex;
...@@ -233,7 +256,7 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => { ...@@ -233,7 +256,7 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
width: 100%; width: 100%;
`, `,
tagList: css` tagList: css`
margin-top: ${theme.spacing.sm}; max-width: 50%;
`, `,
}; };
}); });
...@@ -248,8 +271,21 @@ const Tags: FC<ChildProps> = ({ children, styles }) => { ...@@ -248,8 +271,21 @@ const Tags: FC<ChildProps> = ({ children, styles }) => {
}; };
Tags.displayName = 'Tags'; Tags.displayName = 'Tags';
const Figure: FC<ChildProps> = ({ children, styles }) => { const Figure: FC<ChildProps & { align?: 'top' | 'center' }> = ({ children, styles, align = 'top' }) => {
return <div className={styles?.media}>{children}</div>; return (
<div
className={cx(
styles?.media,
align === 'center' &&
css`
display: flex;
align-items: center;
`
)}
>
{children}
</div>
);
}; };
Figure.displayName = 'Figure'; Figure.displayName = 'Figure';
......
import React, { FC, memo } from 'react'; import React, { FC, memo } from 'react';
import { cx, css } from 'emotion'; import { css, cx } from 'emotion';
import { OnTagClick, Tag } from './Tag'; import { OnTagClick, Tag } from './Tag';
export interface Props { export interface Props {
...@@ -29,11 +29,11 @@ const getStyles = () => { ...@@ -29,11 +29,11 @@ const getStyles = () => {
display: flex; display: flex;
flex: 1 1 auto; flex: 1 1 auto;
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: -6px;
justify-content: flex-end;
`, `,
tag: css` tag: css`
&:not(:first-child) { margin: 0 0 6px 6px;
margin-left: 6px;
}
`, `,
}; };
}; };
...@@ -19,12 +19,14 @@ export const SearchCheckbox: FC<Props> = memo(({ onClick, checked = false, edita ...@@ -19,12 +19,14 @@ export const SearchCheckbox: FC<Props> = memo(({ onClick, checked = false, edita
}); });
const getStyles = stylesFactory(() => ({ const getStyles = stylesFactory(() => ({
// Vertically align absolutely positioned checkbox element
wrapper: css` wrapper: css`
height: 21px; height: 21px;
margin-right: 12px;
& > label { & > label {
height: 100%; height: 100%;
& > input {
position: relative;
}
} }
`, `,
})); }));
......
import React, { FC, useCallback, CSSProperties } from 'react'; import React, { FC, useCallback } from 'react';
import { css, cx } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors'; import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { useTheme, TagList, styleMixins, stylesFactory } from '@grafana/ui'; import { TagList, Card, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { DashboardSectionItem, OnToggleChecked } from '../types'; import { DashboardSectionItem, OnToggleChecked } from '../types';
import { SearchCheckbox } from './SearchCheckbox'; import { SearchCheckbox } from './SearchCheckbox';
import { SEARCH_ITEM_HEIGHT, SEARCH_ITEM_MARGIN } from '../constants'; import { SEARCH_ITEM_HEIGHT } from '../constants';
export interface Props { export interface Props {
item: DashboardSectionItem; item: DashboardSectionItem;
editable?: boolean; editable?: boolean;
onTagSelected: (name: string) => any; onTagSelected: (name: string) => any;
onToggleChecked?: OnToggleChecked; onToggleChecked?: OnToggleChecked;
style?: CSSProperties;
} }
const selectors = e2eSelectors.pages.Dashboards; const selectors = e2eSelectors.pages.Dashboards;
export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected, style }) => { export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSelected }) => {
const theme = useTheme(); const styles = useStyles(getStyles);
const styles = getResultsItemStyles(theme);
const tagSelected = useCallback((tag: string, event: React.MouseEvent<HTMLElement>) => { const tagSelected = useCallback((tag: string, event: React.MouseEvent<HTMLElement>) => {
onTagSelected(tag); onTagSelected(tag);
}, []); }, []);
...@@ -36,71 +33,28 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe ...@@ -36,71 +33,28 @@ export const SearchItem: FC<Props> = ({ item, editable, onToggleChecked, onTagSe
); );
return ( return (
<div <Card
style={style}
aria-label={selectors.dashboards(item.title)} aria-label={selectors.dashboards(item.title)}
className={cx(styles.wrapper, { [styles.selected]: item.selected })} heading={item.title}
href={item.url}
style={{ minHeight: SEARCH_ITEM_HEIGHT }}
className={styles.container}
> >
<Card.Figure align={'center'}>
<SearchCheckbox editable={editable} checked={item.checked} onClick={toggleItem} /> <SearchCheckbox editable={editable} checked={item.checked} onClick={toggleItem} />
</Card.Figure>
<a href={item.url} className={styles.link}> {item.folderTitle && <Card.Meta>{item.folderTitle}</Card.Meta>}
<div className={styles.body}> <Card.Tags>
<span>{item.title}</span> <TagList tags={item.tags} onClick={tagSelected} />
<span className={styles.folderTitle}>{item.folderTitle}</span> </Card.Tags>
</div> </Card>
</a>
<TagList tags={item.tags} onClick={tagSelected} className={styles.tags} />
</div>
); );
}; };
const getResultsItemStyles = stylesFactory((theme: GrafanaTheme) => ({ const getStyles = (theme: GrafanaTheme) => {
wrapper: css` return {
${styleMixins.listItem(theme)}; container: css`
display: flex; padding: ${theme.spacing.sm} ${theme.spacing.md};
align-items: center;
height: ${SEARCH_ITEM_HEIGHT}px;
margin-bottom: ${SEARCH_ITEM_MARGIN}px;
padding: 0 ${theme.spacing.md};
&:last-child {
margin-bottom: ${SEARCH_ITEM_MARGIN * 2}px;
}
:hover {
cursor: pointer;
}
`, `,
selected: css` };
${styleMixins.listItemSelected(theme)}; };
`,
body: css`
display: flex;
flex-direction: column;
justify-content: center;
flex: 1 1 auto;
overflow: hidden;
`,
folderTitle: css`
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.sm};
`,
icon: css`
margin-left: 10px;
`,
tags: css`
flex-grow: 0;
justify-content: flex-end;
@media only screen and (max-width: ${theme.breakpoints.md}) {
display: none;
}
`,
link: css`
display: flex;
align-items: center;
flex-shrink: 0;
flex-grow: 1;
height: 100%;
`,
}));
export const NO_ID_SECTIONS = ['Recent', 'Starred']; export const NO_ID_SECTIONS = ['Recent', 'Starred'];
// Height of the search result item // Height of the search result item
export const SEARCH_ITEM_HEIGHT = 48; export const SEARCH_ITEM_HEIGHT = 62;
export const SEARCH_ITEM_MARGIN = 4; export const SEARCH_ITEM_MARGIN = 8;
export const DEFAULT_SORT = { label: 'A-Z', value: 'alpha-asc' }; export const DEFAULT_SORT = { label: 'A-Z', 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;
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