mirror of https://github.com/grafana/grafana.git
Grafana UI: Card API refactor (#29034)
* Refactor API * Fix types * Cleanup * Remove useMemo * Update actions * Update story * Align secondary actions * Replace snapshot tests * Update docs * Update packages/grafana-ui/src/components/Card/Card.tsx Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com> * Allow overriding child props * Fix types and remove alpha tags Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
This commit is contained in:
parent
6d0a98b249
commit
2c4899a4cd
|
|
@ -2,6 +2,7 @@ import { Meta, Preview, Props } from "@storybook/addon-docs/blocks";
|
|||
import { Card } from "./Card";
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import {TagList} from '../Tags/TagList';
|
||||
|
||||
export const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png'
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ export const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png'
|
|||
## Usage
|
||||
|
||||
### Basic
|
||||
A basic Card component expects at least a heading to be used as title. Optionally a `metadata` prop is accepted, to provide some secondary information for the card. Multiple meta data elements can be provided as an array, in which case they will be separated by a horizontal line: `|`.
|
||||
A basic `Card` component expects at least a heading, used as a title.
|
||||
```jsx
|
||||
<Card
|
||||
heading="Filter by name"
|
||||
|
|
@ -28,47 +29,72 @@ A basic Card component expects at least a heading to be used as title. Optionall
|
|||
|
||||
|
||||
### Multiple metadata elements
|
||||
For providing metadata elements, which can be any extra information for the card, `Card.Meta` component should be used. If metadata consists of multiple strings, each of them has to be escaped (wrapped in brackets `{}`) or better passed in as an array.
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
heading="Test dashboard"
|
||||
metadata={['Folder: Test', 'Views: 100']}
|
||||
tags={['test', 'data', 'testdata']}
|
||||
/>
|
||||
<Card heading="Test dashboard">
|
||||
<Card.Meta>
|
||||
{['Folder: Test', 'Views: 100']}
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="Test dashboard"
|
||||
metadata={['Folder: Test', 'Views: 100']}
|
||||
tags={['test', 'data', 'testdata']}
|
||||
onTagClick={(tag) => console.log('clicked tag:', tag) }
|
||||
/>
|
||||
<Card heading="Test dashboard">
|
||||
<Card.Meta>
|
||||
{['Folder: Test', 'Views: 100']}
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
Metadata also accepts html elements, which could be links, for example. For elements, that are not strings, a `key` prop has to be manually specified.
|
||||
Metadata also accepts HTML elements, which could be links, for example. For elements, that are not strings, a `key` prop has to be manually specified.
|
||||
|
||||
```jsx
|
||||
<Card heading="Test dashboard">
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card heading="Test dashboard">
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
|
||||
### Tags
|
||||
Tags can be rendered inside the Card, by being wrapped in `Card.Tags` component. Note that this component does not provide any tag styling and that should be handled by the children. It is recommended to use it with Grafana-UI's `TagList` component.
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
heading="Test dashboard"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
/>
|
||||
description="Card with a list of tags"
|
||||
>
|
||||
<Card.Tags>
|
||||
<TagList tags={['tag1', 'tag2', 'tag3']} onClick={tag => console.log(tag)} />
|
||||
</Card.Tags>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="Test dashboard"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom-link-2' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
/>
|
||||
description="Card with a list of tags"
|
||||
>
|
||||
<Card.Tags>
|
||||
<TagList tags={['tag1', 'tag2', 'tag3']} onClick={tag => console.log(tag)} />
|
||||
</Card.Tags>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
### As a link
|
||||
|
||||
Card can be used as a clickable link item by specifying `href` prop. In this case the Card's content will be rendered inside `a`.
|
||||
|
||||
```jsx
|
||||
|
|
@ -148,135 +174,150 @@ To render cards in a list, it is possible to nest them inside `li` items.
|
|||
|
||||
### With media elements
|
||||
|
||||
Cards can also be rendered with media content such icons or images.
|
||||
Cards can also be rendered with media content such icons or images. Such elements need to be wrapped in `Card.Figure` component.
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
/>
|
||||
<Card heading="1-ops-tools1-fallback">
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
/>
|
||||
<Card heading="1-ops-tools1-fallback">
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
### Action Cards
|
||||
|
||||
Cards also accept primary and secondary actions. Usually the primary actions are displayed as buttons while secondary actions are displayed as icon buttons.
|
||||
Cards also accept primary and secondary actions. Usually the primary actions are displayed as buttons while secondary actions are displayed as icon buttons. The actions need to be wrappd in `Card.Actions` and `Card.SecondaryActions` components respectively.
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
actions={[<Button key="settings" variant="secondary">Settings</Button>, <Button key="explore" variant="secondary">Explore</Button>]}
|
||||
secondaryActions={[
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />,
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />,
|
||||
]}
|
||||
/>
|
||||
<Card heading="1-ops-tools1-fallback">
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Actions>
|
||||
<Button key="settings" variant="secondary">Settings</Button>
|
||||
<Button key="explore" variant="secondary">Explore</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom-link-3' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
actions={[<Button key="settings" variant="secondary">
|
||||
Settings
|
||||
</Button>,
|
||||
<Button key="explore" variant="secondary">
|
||||
Explore
|
||||
</Button>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />,
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />,
|
||||
]}
|
||||
/>
|
||||
<Card heading="1-ops-tools1-fallback">
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Actions>
|
||||
<Button key="settings" variant="secondary">Settings</Button>
|
||||
<Button key="explore" variant="secondary">Explore</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
### Disabled state
|
||||
|
||||
Card can have a disabled state, effectively making it and its actions non-clickable. If there is more than one primary action, disabled state will disable them instead of the whole card.
|
||||
Card can have a disabled state, effectively making it and its actions non-clickable. If there are any actions, they will be disabled instead of the whole card.
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
disabled
|
||||
/>
|
||||
>
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
disabled
|
||||
/>
|
||||
>
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
actions={[<Button key='settings' variant="secondary">Settings</Button>, <Button key='explore' variant="secondary">Explore</Button>]}
|
||||
secondaryActions={[
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />,
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />,
|
||||
]}
|
||||
disabled
|
||||
/>
|
||||
<Card heading="1-ops-tools1-fallback" disabled>
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Actions>
|
||||
<Button key="settings" variant="secondary">Settings</Button>
|
||||
<Button key="explore" variant="secondary">Explore</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Grafana',
|
||||
<a key='prom' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>,
|
||||
]}
|
||||
image={<img src={logo} alt="Grafana Logo" />}
|
||||
actions={[<Button key='settings' variant="secondary">Settings</Button>, <Button key='explore' variant="secondary">Explore</Button>]}
|
||||
secondaryActions={[
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />,
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />,
|
||||
]}
|
||||
disabled
|
||||
/>
|
||||
<Card heading="1-ops-tools1-fallback" disabled>
|
||||
<Card.Meta>
|
||||
Grafana
|
||||
<a key='prom-link' href="https://ops-us-east4.grafana.net/api/prom">https://ops-us-east4.grafana.net/api/prom</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Grafana Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Actions>
|
||||
<Button key="settings" variant="secondary">Settings</Button>
|
||||
<Button key="explore" variant="secondary">Explore</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
</Preview>
|
||||
|
||||
|
||||
### Props
|
||||
<Props of={Card}/>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { Card } from './Card';
|
||||
import mdx from './Card.mdx';
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { TagList } from '../Tags/TagList';
|
||||
|
||||
const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png';
|
||||
|
||||
|
|
@ -63,58 +65,57 @@ export const WithTooltip = () => {
|
|||
export const WithTags = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
heading="Elasticsearch – Custom Templated Query"
|
||||
metadata="Elastic Search"
|
||||
tags={['elasticsearch', 'test', 'testdata']}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Card heading="Elasticsearch – Custom Templated Query" disabled={disabled}>
|
||||
<Card.Meta>Elastic Search</Card.Meta>
|
||||
<Card.Tags>
|
||||
<TagList tags={['elasticsearch', 'test', 'testdata']} onClick={tag => console.log('tag', tag)} />
|
||||
</Card.Tags>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithMedia = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
href="https://ops-us-east4.grafana.net/api/prom"
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Prometheus',
|
||||
<Card href="https://ops-us-east4.grafana.net/api/prom" heading="1-ops-tools1-fallback" disabled={disabled}>
|
||||
<Card.Meta>
|
||||
Prometheus
|
||||
<a key="link2" href="https://ops-us-east4.grafana.net/api/prom">
|
||||
https://ops-us-east4.grafana.net/api/prom
|
||||
</a>,
|
||||
]}
|
||||
disabled={disabled}
|
||||
image={<img src={logo} alt="Prometheus Logo" />}
|
||||
/>
|
||||
</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Prometheus Logo" />
|
||||
</Card.Figure>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
export const WithActions = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'Prometheus',
|
||||
<a key="link" href="https://ops-us-east4.grafana.net/api/prom">
|
||||
<Card heading="1-ops-tools1-fallback" disabled={disabled}>
|
||||
<Card.Meta>
|
||||
Prometheus
|
||||
<a key="link2" href="https://ops-us-east4.grafana.net/api/prom">
|
||||
https://ops-us-east4.grafana.net/api/prom
|
||||
</a>,
|
||||
]}
|
||||
disabled={disabled}
|
||||
image={<img src={logo} alt="Prometheus Logo" />}
|
||||
actions={[
|
||||
</a>
|
||||
</Card.Meta>
|
||||
<Card.Figure>
|
||||
<img src={logo} alt="Prometheus Logo" />
|
||||
</Card.Figure>
|
||||
<Card.Actions>
|
||||
<Button key="settings" variant="secondary">
|
||||
Settings
|
||||
</Button>,
|
||||
</Button>
|
||||
<Button key="explore" variant="secondary">
|
||||
Explore
|
||||
</Button>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />,
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />,
|
||||
]}
|
||||
/>
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<IconButton key="showAll" name="apps" tooltip="Show all dashboards for this data source" />
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -124,33 +125,36 @@ export const Full = () => {
|
|||
return (
|
||||
<Card
|
||||
heading="Card title"
|
||||
metadata={[
|
||||
'Subtitle',
|
||||
'Meta info 1',
|
||||
'Meta info 2',
|
||||
disabled={disabled}
|
||||
description="Description, body text. Greetings! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
>
|
||||
<Card.Meta>
|
||||
{['Subtitle', 'Meta info 1', 'Meta info 2']}
|
||||
<a key="link" href="https://ops-us-east4.grafana.net/api/prom">
|
||||
https://ops-us-east4.grafana.net/api/prom
|
||||
</a>,
|
||||
]}
|
||||
disabled={disabled}
|
||||
image={<img src={logo} alt="Prometheus Logo" />}
|
||||
tags={['firing', 'active', 'test', 'testdata', 'prometheus']}
|
||||
description="Description, body text. Greetings! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
|
||||
actions={[
|
||||
</a>
|
||||
</Card.Meta>
|
||||
<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
|
||||
</Button>,
|
||||
</Button>
|
||||
<Button key="explore" variant="secondary">
|
||||
2nd action
|
||||
</Button>,
|
||||
]}
|
||||
secondaryActions={[
|
||||
<IconButton key="comment-alt" name="comment-alt" tooltip="Tooltip content" />,
|
||||
<IconButton key="copy" name="copy" tooltip="Tooltip content" />,
|
||||
<IconButton key="link" name="link" tooltip="Tooltip content" />,
|
||||
<IconButton key="star" name="star" tooltip="Tooltip content" />,
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />,
|
||||
]}
|
||||
/>
|
||||
</Button>
|
||||
</Card.Actions>
|
||||
<Card.SecondaryActions>
|
||||
<IconButton key="comment-alt" name="comment-alt" tooltip="Tooltip content" />
|
||||
<IconButton key="copy" name="copy" tooltip="Tooltip content" />
|
||||
<IconButton key="link" name="link" tooltip="Tooltip content" />
|
||||
<IconButton key="star" name="star" tooltip="Tooltip content" />
|
||||
<IconButton key="delete" name="trash-alt" tooltip="Delete this data source" />
|
||||
</Card.SecondaryActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
import React, { cloneElement, FC, HTMLAttributes, ReactElement, ReactNode, useCallback, useMemo } from 'react';
|
||||
import React, { memo, cloneElement, FC, HTMLAttributes, ReactNode, useCallback } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { useTheme, styleMixins, stylesFactory } from '../../themes';
|
||||
import { Tooltip, PopoverContent } from '../Tooltip/Tooltip';
|
||||
import { OnTagClick } from '../Tags/Tag';
|
||||
import { TagList } from '../Tags/TagList';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export interface ContainerProps extends HTMLAttributes<HTMLOrSVGElement> {
|
||||
/** Content for the card's tooltip */
|
||||
|
|
@ -25,7 +23,7 @@ const CardContainer: FC<ContainerProps> = ({ children, tooltip, ...props }) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export interface CardInnerProps {
|
||||
href?: string;
|
||||
|
|
@ -44,100 +42,88 @@ const CardInner: FC<CardInnerProps> = ({ children, href }) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export interface Props extends ContainerProps {
|
||||
/** Main heading for the Card **/
|
||||
heading: ReactNode;
|
||||
/** Additional data about the card. If array is supplied, elements will be rendered with vertical line separator */
|
||||
metadata?: ReactNode | ReactNode[];
|
||||
/** Card description text */
|
||||
description?: string;
|
||||
/** List of tags to display in the card */
|
||||
tags?: string[];
|
||||
/** Optional callback for tag onclick event */
|
||||
onTagClick?: OnTagClick;
|
||||
/** Indicates if the card and all its actions can be interacted with */
|
||||
disabled?: boolean;
|
||||
/** Image or icon to be displayed on the let side of the card */
|
||||
image?: ReactNode;
|
||||
/** Main card actions **/
|
||||
actions?: ReactElement[];
|
||||
/** Right-side actions */
|
||||
secondaryActions?: ReactElement[];
|
||||
/** Link to redirect to on card click. If provided, the Card inner content will be rendered inside `a` */
|
||||
href?: string;
|
||||
/** On click handler for the Card */
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export interface CardInterface extends FC<Props> {
|
||||
Tags: typeof Tags;
|
||||
Figure: typeof Figure;
|
||||
Meta: typeof Meta;
|
||||
Actions: typeof Actions;
|
||||
SecondaryActions: typeof SecondaryActions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic card component
|
||||
*
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const Card: FC<Props> = ({
|
||||
export const Card: CardInterface = ({
|
||||
heading,
|
||||
description,
|
||||
metadata,
|
||||
tags = [],
|
||||
onTagClick,
|
||||
disabled,
|
||||
image,
|
||||
actions = [],
|
||||
tooltip,
|
||||
secondaryActions = [],
|
||||
href,
|
||||
onClick,
|
||||
className,
|
||||
children,
|
||||
...htmlProps
|
||||
}) => {
|
||||
const hasActions = Boolean(actions.length || secondaryActions.length);
|
||||
const disableHover = disabled || actions.length > 1 || !onClick;
|
||||
const disableEvents = disabled && !actions.length;
|
||||
const theme = useTheme();
|
||||
const styles = getCardStyles(theme, disableEvents, disableHover);
|
||||
// Join meta data elements by '|'
|
||||
const meta = useMemo(
|
||||
() =>
|
||||
Array.isArray(metadata)
|
||||
? (metadata as ReactNode[]).filter(Boolean).reduce((prev, curr, i) => [
|
||||
prev,
|
||||
<span key={`separator_${i}`} className={styles.separator}>
|
||||
|
|
||||
</span>,
|
||||
curr,
|
||||
])
|
||||
: metadata,
|
||||
[metadata, styles.separator]
|
||||
const styles = getCardStyles(theme);
|
||||
const [tags, figure, meta, actions, secondaryActions] = ['Tags', 'Figure', 'Meta', 'Actions', 'SecondaryActions'].map(
|
||||
item => {
|
||||
const found = React.Children.toArray(children as React.ReactElement[]).find(child => {
|
||||
return child?.type && (child.type as any).displayName === item;
|
||||
});
|
||||
|
||||
if (found) {
|
||||
return React.cloneElement(found, { disabled, styles, ...found.props });
|
||||
}
|
||||
return found;
|
||||
}
|
||||
);
|
||||
|
||||
const hasActions = Boolean(actions || secondaryActions);
|
||||
const disableHover = disabled || !onClick;
|
||||
const disableEvents = disabled && !actions;
|
||||
|
||||
const containerStyles = getContainerStyles(theme, disableEvents, disableHover);
|
||||
const onCardClick = useCallback(() => (disableHover ? () => {} : onClick), [disableHover, onClick]);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
tooltip={tooltip}
|
||||
tabIndex={disableHover ? undefined : 0}
|
||||
className={cx(styles.container, className)}
|
||||
className={cx(containerStyles, className)}
|
||||
onClick={onCardClick}
|
||||
{...htmlProps}
|
||||
>
|
||||
<CardInner href={href}>
|
||||
{image && <div className={styles.media}>{image}</div>}
|
||||
{figure}
|
||||
<div className={styles.inner}>
|
||||
<div className={styles.heading}>{heading}</div>
|
||||
{meta && <div className={styles.metadata}>{meta}</div>}
|
||||
{!!tags.length && <TagList tags={tags} onClick={onTagClick} className={styles.tagList} />}
|
||||
<div className={styles.heading} role="heading">
|
||||
{heading}
|
||||
</div>
|
||||
{meta}
|
||||
{tags}
|
||||
{description && <p className={styles.description}>{description}</p>}
|
||||
{hasActions && (
|
||||
<div className={styles.actionRow}>
|
||||
{!!actions.length && (
|
||||
<div className={styles.actions}>{actions.map(action => cloneElement(action, { disabled }))}</div>
|
||||
)}
|
||||
{!!secondaryActions.length && (
|
||||
<div className={styles.secondaryActions}>
|
||||
{secondaryActions.map(action => cloneElement(action, { disabled }))}
|
||||
</div>
|
||||
)}
|
||||
{actions}
|
||||
{secondaryActions}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -147,44 +133,50 @@ export const Card: FC<Props> = ({
|
|||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @public
|
||||
*/
|
||||
export const getCardStyles = stylesFactory((theme: GrafanaTheme, disabled = false, disableHover = false) => {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: ${theme.colors.textStrong};
|
||||
background: ${theme.colors.bg2};
|
||||
export const getContainerStyles = stylesFactory((theme: GrafanaTheme, disabled = false, disableHover = false) => {
|
||||
return css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: ${theme.colors.textStrong};
|
||||
background: ${theme.colors.bg2};
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
padding: ${theme.spacing.md};
|
||||
position: relative;
|
||||
pointer-events: ${disabled ? 'none' : 'auto'};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: ${disabled ? 'block' : 'none'};
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
background: linear-gradient(180deg, rgba(75, 79, 84, 0.5) 0%, rgba(82, 84, 92, 0.5) 100%);
|
||||
width: calc(100% - 2px);
|
||||
height: calc(100% - 2px);
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
padding: ${theme.spacing.md};
|
||||
position: relative;
|
||||
pointer-events: ${disabled ? 'none' : 'auto'};
|
||||
margin-bottom: ${theme.spacing.sm};
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: ${disabled ? 'block' : 'none'};
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
bottom: 1px;
|
||||
background: linear-gradient(180deg, rgba(75, 79, 84, 0.5) 0%, rgba(82, 84, 92, 0.5) 100%);
|
||||
width: calc(100% - 2px);
|
||||
height: calc(100% - 2px);
|
||||
border-radius: ${theme.border.radius.sm};
|
||||
}
|
||||
&:hover {
|
||||
background: ${disableHover ? theme.colors.bg2 : styleMixins.hoverColor(theme.colors.bg2, theme)};
|
||||
cursor: ${disableHover ? 'default' : 'pointer'};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: ${disableHover ? theme.colors.bg2 : styleMixins.hoverColor(theme.colors.bg2, theme)};
|
||||
cursor: ${disableHover ? 'default' : 'pointer'};
|
||||
}
|
||||
&:focus {
|
||||
${styleMixins.focusCss(theme)};
|
||||
}
|
||||
`;
|
||||
});
|
||||
|
||||
&:focus {
|
||||
${styleMixins.focusCss(theme)};
|
||||
}
|
||||
`,
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const getCardStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||
return {
|
||||
inner: css`
|
||||
width: 100%;
|
||||
`,
|
||||
|
|
@ -227,6 +219,8 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme, disabled = fals
|
|||
display: flex;
|
||||
align-items: center;
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
// align to the right
|
||||
margin-left: auto;
|
||||
& > * {
|
||||
margin-right: ${theme.spacing.sm} !important;
|
||||
}
|
||||
|
|
@ -243,3 +237,73 @@ export const getCardStyles = stylesFactory((theme: GrafanaTheme, disabled = fals
|
|||
`,
|
||||
};
|
||||
});
|
||||
|
||||
interface ChildProps {
|
||||
styles?: ReturnType<typeof getCardStyles>;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Tags: FC<ChildProps> = ({ children, styles }) => {
|
||||
return <div className={styles?.tagList}>{children}</div>;
|
||||
};
|
||||
Tags.displayName = 'Tags';
|
||||
|
||||
const Figure: FC<ChildProps> = ({ children, styles }) => {
|
||||
return <div className={styles?.media}>{children}</div>;
|
||||
};
|
||||
|
||||
Figure.displayName = 'Figure';
|
||||
|
||||
const Meta: FC<ChildProps> = memo(({ children, styles }) => {
|
||||
let meta = children;
|
||||
|
||||
// Join meta data elements by '|'
|
||||
if (Array.isArray(children)) {
|
||||
meta = React.Children.toArray(children).reduce((prev, curr, i) => [
|
||||
prev,
|
||||
<span key={`separator_${i}`} className={styles?.separator}>
|
||||
|
|
||||
</span>,
|
||||
curr,
|
||||
]);
|
||||
}
|
||||
return <div className={styles?.metadata}>{meta}</div>;
|
||||
});
|
||||
|
||||
Meta.displayName = 'Meta';
|
||||
|
||||
interface ActionsProps extends ChildProps {
|
||||
children: JSX.Element[];
|
||||
variant?: 'primary' | 'secondary';
|
||||
}
|
||||
|
||||
const BaseActions: FC<ActionsProps> = ({ children, styles, disabled, variant }) => {
|
||||
const css = variant === 'primary' ? styles?.actions : styles?.secondaryActions;
|
||||
return <div className={css}>{React.Children.map(children, child => cloneElement(child, { disabled }))}</div>;
|
||||
};
|
||||
|
||||
const Actions: FC<ActionsProps> = ({ children, styles, disabled }) => {
|
||||
return (
|
||||
<BaseActions variant="primary" disabled={disabled} styles={styles}>
|
||||
{children}
|
||||
</BaseActions>
|
||||
);
|
||||
};
|
||||
|
||||
Actions.displayName = 'Actions';
|
||||
|
||||
const SecondaryActions: FC<ActionsProps> = ({ children, styles, disabled }) => {
|
||||
return (
|
||||
<BaseActions variant="secondary" disabled={disabled} styles={styles}>
|
||||
{children}
|
||||
</BaseActions>
|
||||
);
|
||||
};
|
||||
|
||||
SecondaryActions.displayName = 'SecondaryActions';
|
||||
|
||||
Card.Tags = Tags;
|
||||
Card.Figure = Figure;
|
||||
Card.Meta = Meta;
|
||||
Card.Actions = Actions;
|
||||
Card.SecondaryActions = SecondaryActions;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import AlertRuleItem, { Props } from './AlertRuleItem';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
connect: () => (params: any) => params,
|
||||
}));
|
||||
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
rule: {
|
||||
|
|
@ -26,13 +22,20 @@ const setup = (propOverrides?: object) => {
|
|||
|
||||
Object.assign(props, propOverrides);
|
||||
|
||||
return shallow(<AlertRuleItem {...props} />);
|
||||
return render(<AlertRuleItem {...props} />);
|
||||
};
|
||||
|
||||
describe('Render', () => {
|
||||
describe('AlertRuleItem', () => {
|
||||
it('should render component', () => {
|
||||
const wrapper = setup();
|
||||
const mockToggle = jest.fn();
|
||||
setup({ onTogglePause: mockToggle });
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
expect(screen.getByText('Some rule')).toBeInTheDocument();
|
||||
expect(screen.getByText('state text')).toBeInTheDocument();
|
||||
expect(screen.getByText('Pause')).toBeInTheDocument();
|
||||
expect(screen.getByText('Edit alert')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Pause'));
|
||||
expect(mockToggle).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,21 +31,20 @@ const AlertRuleItem = ({ rule, search, onTogglePause }: Props) => {
|
|||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<Card
|
||||
heading={<a href={ruleUrl}>{renderText(rule.name)}</a>}
|
||||
image={
|
||||
<Card heading={<a href={ruleUrl}>{renderText(rule.name)}</a>}>
|
||||
<Card.Figure>
|
||||
<Icon size="xl" name={rule.stateIcon as IconName} className={`alert-rule-item__icon ${rule.stateClass}`} />
|
||||
}
|
||||
metadata={[
|
||||
</Card.Figure>
|
||||
<Card.Meta>
|
||||
<span key="state">
|
||||
<span key="text" className={`${rule.stateClass}`}>
|
||||
{renderText(rule.stateText)}{' '}
|
||||
</span>
|
||||
for {rule.stateAge}
|
||||
</span>,
|
||||
rule.info ? renderText(rule.info) : null,
|
||||
]}
|
||||
actions={[
|
||||
</span>
|
||||
{rule.info ? renderText(rule.info) : null}
|
||||
</Card.Meta>
|
||||
<Card.Actions>
|
||||
<Button
|
||||
key="play"
|
||||
variant="secondary"
|
||||
|
|
@ -53,12 +52,12 @@ const AlertRuleItem = ({ rule, search, onTogglePause }: Props) => {
|
|||
onClick={onTogglePause}
|
||||
>
|
||||
{rule.state === 'paused' ? 'Resume' : 'Pause'}
|
||||
</Button>,
|
||||
</Button>
|
||||
<LinkButton key="edit" variant="secondary" href={ruleUrl} icon="cog">
|
||||
Edit alert
|
||||
</LinkButton>,
|
||||
]}
|
||||
/>
|
||||
</LinkButton>
|
||||
</Card.Actions>
|
||||
</Card>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<li
|
||||
className="css-1aih96i"
|
||||
>
|
||||
<Component
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
icon="pause"
|
||||
onClick={[MockFunction]}
|
||||
variant="secondary"
|
||||
>
|
||||
Pause
|
||||
</Button>,
|
||||
<LinkButton
|
||||
href="https://something.something.darkside?editPanel=1&tab=alert"
|
||||
icon="cog"
|
||||
variant="secondary"
|
||||
>
|
||||
Edit alert
|
||||
</LinkButton>,
|
||||
]
|
||||
}
|
||||
heading={
|
||||
<a
|
||||
href="https://something.something.darkside?editPanel=1&tab=alert"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
searchWords={
|
||||
Array [
|
||||
"",
|
||||
]
|
||||
}
|
||||
textToHighlight="Some rule"
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
image={
|
||||
<Icon
|
||||
className="alert-rule-item__icon state class"
|
||||
name="icon"
|
||||
size="xl"
|
||||
/>
|
||||
}
|
||||
metadata={
|
||||
Array [
|
||||
<span>
|
||||
<span
|
||||
className="state class"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
searchWords={
|
||||
Array [
|
||||
"",
|
||||
]
|
||||
}
|
||||
textToHighlight="state text"
|
||||
/>
|
||||
|
||||
</span>
|
||||
for
|
||||
age
|
||||
</span>,
|
||||
null,
|
||||
]
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
`;
|
||||
Loading…
Reference in New Issue