mirror of
https://github.com/grafana/grafana.git
synced 2025-02-20 11:48:34 -06:00
Grafana-UI: Add Card component (#28216)
* Base card * Add disabled state * Expand knobs * Add card actions * Add meta data * Allow custom tags * Extend container props * Add inner link * Add docs * Add missing keys * Update margins * Add description * Add full card example * Tweak disabld state * Export Card * title => heading * Filter out empty content * Add disableEvents * Move tooltip to container * Use new Card for AlertRuleItem * Cleanup * Update snapshot * Rename props * Rename props[2] * Disable hover is onClick is missing * Fix alert rule item * Update snapshot * Export CardProps * Replace logo * Remove tag prop * Remove extra div * Add @public * Update AlertRuleItem * Simplify disabled logic * Export Card styles * Remove AlertRuleItem tooltips * Revert to old button design * Make component internal Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
33ef71d81e
commit
c96ef2676e
282
packages/grafana-ui/src/components/Card/Card.mdx
Normal file
282
packages/grafana-ui/src/components/Card/Card.mdx
Normal file
@ -0,0 +1,282 @@
|
||||
import { Meta, Preview, Props } from "@storybook/addon-docs/blocks";
|
||||
import { Card } from "./Card";
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
export const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png'
|
||||
|
||||
<Meta title="MDX/Card" component={Card}/>
|
||||
|
||||
# Card
|
||||
|
||||
## 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: `|`.
|
||||
```jsx
|
||||
<Card
|
||||
heading="Filter by name"
|
||||
description="Filter data by query."
|
||||
/>
|
||||
```
|
||||
<Preview>
|
||||
<Card
|
||||
heading="Filter by name"
|
||||
description="Filter data by query."
|
||||
/>
|
||||
</Preview>
|
||||
|
||||
|
||||
### Multiple metadata elements
|
||||
|
||||
```jsx
|
||||
<Card
|
||||
heading="Test dashboard"
|
||||
metadata={['Folder: Test', 'Views: 100']}
|
||||
tags={['test', 'data', 'testdata']}
|
||||
/>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Card
|
||||
heading="Test dashboard"
|
||||
metadata={['Folder: Test', 'Views: 100']}
|
||||
tags={['test', 'data', 'testdata']}
|
||||
onTagClick={(tag) => console.log('clicked tag:', tag) }
|
||||
/>
|
||||
</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.
|
||||
|
||||
```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>,
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
<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>,
|
||||
]}
|
||||
/>
|
||||
</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
|
||||
<Card
|
||||
heading="Redirect to Grafana"
|
||||
description="Clicking this card will redirect to grafana website"
|
||||
href='https://grafana.com'
|
||||
/>
|
||||
```
|
||||
<Preview>
|
||||
<Card
|
||||
heading="Redirect to Grafana"
|
||||
description="Clicking this card will redirect to grafana website"
|
||||
href='https://grafana.com'
|
||||
/>
|
||||
</Preview>
|
||||
|
||||
### Inside a list item
|
||||
|
||||
To render cards in a list, it is possible to nest them inside `li` items.
|
||||
```jsx
|
||||
<ul>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
<Preview>
|
||||
<ul style={{padding: '20px', maxWidth: '800px', listStyle: 'none'}}>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Card
|
||||
heading="List card item"
|
||||
description="Card that is rendered inside li element."
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</Preview>
|
||||
|
||||
### With media elements
|
||||
|
||||
Cards can also be rendered with media content such icons or images.
|
||||
|
||||
```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" />}
|
||||
/>
|
||||
```
|
||||
|
||||
<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" />}
|
||||
/>
|
||||
</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.
|
||||
|
||||
```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" />,
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
<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" />,
|
||||
]}
|
||||
/>
|
||||
</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.
|
||||
|
||||
```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" />}
|
||||
disabled
|
||||
/>
|
||||
```
|
||||
|
||||
<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
|
||||
/>
|
||||
</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
|
||||
/>
|
||||
```
|
||||
|
||||
<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
|
||||
/>
|
||||
</Preview>
|
||||
|
||||
|
||||
### Props
|
||||
<Props of={Card}/>
|
||||
|
156
packages/grafana-ui/src/components/Card/Card.story.internal.tsx
Normal file
156
packages/grafana-ui/src/components/Card/Card.story.internal.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { Card } from './Card';
|
||||
import mdx from './Card.mdx';
|
||||
import { Button } from '../Button';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
|
||||
const logo = 'https://grafana.com/static/assets/img/apple-touch-icon.png';
|
||||
|
||||
export default {
|
||||
title: 'General/Card',
|
||||
component: Card,
|
||||
decorators: [withCenteredStory],
|
||||
parameters: {
|
||||
docs: {
|
||||
page: mdx,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const getKnobs = () => {
|
||||
const disabled = boolean('Disabled', false, 'Style props');
|
||||
|
||||
return { disabled };
|
||||
};
|
||||
|
||||
export const Basic = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
heading="Filter by name"
|
||||
description="Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel."
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AsLink = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
href="https://grafana.com"
|
||||
heading="Filter by name"
|
||||
description="Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel."
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithTooltip = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
heading="Reduce"
|
||||
description="Reduce all rows or data points to a single value using a function like max, min, mean or last."
|
||||
tooltip="Click to apply this transformation."
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithTags = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
heading="Elasticsearch – Custom Templated Query"
|
||||
metadata="Elastic Search"
|
||||
tags={['elasticsearch', 'test', 'testdata']}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const WithMedia = () => {
|
||||
const { disabled } = getKnobs();
|
||||
return (
|
||||
<Card
|
||||
href="https://ops-us-east4.grafana.net/api/prom"
|
||||
heading="1-ops-tools1-fallback"
|
||||
metadata={[
|
||||
'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" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
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">
|
||||
https://ops-us-east4.grafana.net/api/prom
|
||||
</a>,
|
||||
]}
|
||||
disabled={disabled}
|
||||
image={<img src={logo} alt="Prometheus 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" />,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Full = () => {
|
||||
const { disabled } = getKnobs();
|
||||
|
||||
return (
|
||||
<Card
|
||||
heading="Card title"
|
||||
metadata={[
|
||||
'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={[
|
||||
<Button key="settings" variant="secondary">
|
||||
Main action
|
||||
</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" />,
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
245
packages/grafana-ui/src/components/Card/Card.tsx
Normal file
245
packages/grafana-ui/src/components/Card/Card.tsx
Normal file
@ -0,0 +1,245 @@
|
||||
import React, { cloneElement, FC, HTMLAttributes, ReactElement, ReactNode, useCallback, useMemo } 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
|
||||
*/
|
||||
export interface ContainerProps extends HTMLAttributes<HTMLOrSVGElement> {
|
||||
/** Content for the card's tooltip */
|
||||
tooltip?: PopoverContent;
|
||||
}
|
||||
|
||||
const CardContainer: FC<ContainerProps> = ({ children, tooltip, ...props }) => {
|
||||
return tooltip ? (
|
||||
<Tooltip placement="top" content={tooltip} theme="info">
|
||||
<div {...props}>{children}</div>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div {...props}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface CardInnerProps {
|
||||
href?: string;
|
||||
}
|
||||
|
||||
const CardInner: FC<CardInnerProps> = ({ children, href }) => {
|
||||
const theme = useTheme();
|
||||
const styles = getCardStyles(theme);
|
||||
return href ? (
|
||||
<a className={styles.innerLink} href={href}>
|
||||
{children}
|
||||
</a>
|
||||
) : (
|
||||
<>{children}</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic card component
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export const Card: FC<Props> = ({
|
||||
heading,
|
||||
description,
|
||||
metadata,
|
||||
tags = [],
|
||||
onTagClick,
|
||||
disabled,
|
||||
image,
|
||||
actions = [],
|
||||
tooltip,
|
||||
secondaryActions = [],
|
||||
href,
|
||||
onClick,
|
||||
className,
|
||||
...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 onCardClick = useCallback(() => (disableHover ? () => {} : onClick), [disableHover, onClick]);
|
||||
|
||||
return (
|
||||
<CardContainer
|
||||
tooltip={tooltip}
|
||||
tabIndex={disableHover ? undefined : 0}
|
||||
className={cx(styles.container, className)}
|
||||
onClick={onCardClick}
|
||||
{...htmlProps}
|
||||
>
|
||||
<CardInner href={href}>
|
||||
{image && <div className={styles.media}>{image}</div>}
|
||||
<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} />}
|
||||
{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>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardInner>
|
||||
</CardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
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};
|
||||
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'};
|
||||
}
|
||||
|
||||
&:focus {
|
||||
${styleMixins.focusCss(theme)};
|
||||
}
|
||||
`,
|
||||
inner: css`
|
||||
width: 100%;
|
||||
`,
|
||||
heading: css`
|
||||
margin-bottom: 0;
|
||||
font-size: ${theme.typography.size.md};
|
||||
line-height: ${theme.typography.lineHeight.xs};
|
||||
`,
|
||||
metadata: css`
|
||||
font-size: ${theme.typography.size.sm};
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
margin: ${theme.spacing.sm} 0 0;
|
||||
line-height: ${theme.typography.lineHeight.xs};
|
||||
`,
|
||||
description: css`
|
||||
margin: ${theme.spacing.sm} 0 0;
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
line-height: ${theme.typography.lineHeight.md};
|
||||
`,
|
||||
media: css`
|
||||
margin-right: ${theme.spacing.md};
|
||||
max-width: 40px;
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
`,
|
||||
actionRow: css`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-top: ${theme.spacing.md};
|
||||
`,
|
||||
actions: css`
|
||||
& > * {
|
||||
margin-right: ${theme.spacing.sm};
|
||||
}
|
||||
`,
|
||||
secondaryActions: css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${theme.colors.textSemiWeak};
|
||||
& > * {
|
||||
margin-right: ${theme.spacing.sm} !important;
|
||||
}
|
||||
`,
|
||||
separator: css`
|
||||
margin: 0 ${theme.spacing.sm};
|
||||
`,
|
||||
innerLink: css`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
`,
|
||||
tagList: css`
|
||||
margin-top: ${theme.spacing.sm};
|
||||
`,
|
||||
};
|
||||
});
|
@ -4,6 +4,9 @@ import { GrafanaTheme } from '@grafana/data';
|
||||
import { useTheme } from '../../themes';
|
||||
import { getTagColor, getTagColorsFromName } from '../../utils';
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type OnTagClick = (name: string, event: React.MouseEvent<HTMLElement>) => any;
|
||||
|
||||
export interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {
|
||||
|
@ -29,7 +29,9 @@ const getStyles = () => {
|
||||
flex-wrap: wrap;
|
||||
`,
|
||||
tag: css`
|
||||
margin-left: 6px;
|
||||
&:not(:first-child) {
|
||||
margin-left: 6px;
|
||||
}
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -29,7 +29,7 @@ export { TimeZonePicker } from './TimePicker/TimeZonePicker';
|
||||
export { List } from './List/List';
|
||||
export { TagsInput } from './TagsInput/TagsInput';
|
||||
export { Pagination } from './Pagination/Pagination';
|
||||
export { Tag } from './Tags/Tag';
|
||||
export { Tag, OnTagClick } from './Tags/Tag';
|
||||
export { TagList } from './Tags/TagList';
|
||||
export { FilterPill } from './FilterPill/FilterPill';
|
||||
|
||||
@ -169,6 +169,7 @@ export { Checkbox } from './Forms/Checkbox';
|
||||
export { TextArea } from './TextArea/TextArea';
|
||||
export { FileUpload } from './FileUpload/FileUpload';
|
||||
export { TimeRangeInput } from './TimePicker/TimeRangeInput';
|
||||
export { Card, Props as CardProps, ContainerProps, CardInnerProps, getCardStyles } from './Card/Card';
|
||||
|
||||
// Legacy forms
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
// @ts-ignore
|
||||
import Highlighter from 'react-highlight-words';
|
||||
import { css } from 'emotion';
|
||||
import { Icon, IconName, Button, LinkButton, Card } from '@grafana/ui';
|
||||
import { AlertRule } from '../../types';
|
||||
import { Icon, IconName, Button, Tooltip, LinkButton, HorizontalGroup } from '@grafana/ui';
|
||||
|
||||
export interface Props {
|
||||
rule: AlertRule;
|
||||
@ -10,56 +11,51 @@ export interface Props {
|
||||
onTogglePause: () => void;
|
||||
}
|
||||
|
||||
class AlertRuleItem extends PureComponent<Props> {
|
||||
renderText(text: string) {
|
||||
return (
|
||||
const AlertRuleItem = ({ rule, search, onTogglePause }: Props) => {
|
||||
const ruleUrl = `${rule.url}?editPanel=${rule.panelId}&tab=alert`;
|
||||
const renderText = useCallback(
|
||||
text => (
|
||||
<Highlighter
|
||||
key={text}
|
||||
highlightClassName="highlight-search-match"
|
||||
textToHighlight={text}
|
||||
searchWords={[this.props.search]}
|
||||
searchWords={[search]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
),
|
||||
[search]
|
||||
);
|
||||
|
||||
render() {
|
||||
const { rule, onTogglePause } = this.props;
|
||||
|
||||
const ruleUrl = `${rule.url}?editPanel=${rule.panelId}&tab=alert`;
|
||||
|
||||
return (
|
||||
<li className="alert-rule-item">
|
||||
<Icon size="xl" name={rule.stateIcon as IconName} className={`alert-rule-item__icon ${rule.stateClass}`} />
|
||||
<div className="alert-rule-item__body">
|
||||
<div className="alert-rule-item__header">
|
||||
<div className="alert-rule-item__name">
|
||||
<a href={ruleUrl}>{this.renderText(rule.name)}</a>
|
||||
</div>
|
||||
<div className="alert-rule-item__text">
|
||||
<span className={`${rule.stateClass}`}>{this.renderText(rule.stateText)}</span>
|
||||
<span className="alert-rule-item__time"> for {rule.stateAge}</span>
|
||||
</div>
|
||||
</div>
|
||||
{rule.info && <div className="small muted alert-rule-item__info">{this.renderText(rule.info)}</div>}
|
||||
</div>
|
||||
|
||||
<div className="alert-rule-item__actions">
|
||||
<HorizontalGroup spacing="sm">
|
||||
<Tooltip placement="bottom" content="Pausing an alert rule prevents it from executing">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
icon={rule.state === 'paused' ? 'play' : 'pause'}
|
||||
onClick={onTogglePause}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="right" content="Edit alert rule">
|
||||
<LinkButton size="sm" variant="secondary" href={ruleUrl} icon="cog" />
|
||||
</Tooltip>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<li
|
||||
className={css`
|
||||
width: 100%;
|
||||
`}
|
||||
>
|
||||
<Card
|
||||
heading={<a href={ruleUrl}>{renderText(rule.name)}</a>}
|
||||
image={
|
||||
<Icon size="xl" name={rule.stateIcon as IconName} className={`alert-rule-item__icon ${rule.stateClass}`} />
|
||||
}
|
||||
metadata={[
|
||||
<span key="state">
|
||||
<span key="text" className={`${rule.stateClass}`}>
|
||||
{renderText(rule.stateText)}{' '}
|
||||
</span>
|
||||
for {rule.stateAge}
|
||||
</span>,
|
||||
rule.info ? renderText(rule.info) : null,
|
||||
]}
|
||||
actions={[
|
||||
<Button variant="secondary" icon={rule.state === 'paused' ? 'play' : 'pause'} onClick={onTogglePause}>
|
||||
{rule.state === 'paused' ? 'Resume' : 'Pause'}
|
||||
</Button>,
|
||||
<LinkButton variant="secondary" href={ruleUrl} icon="cog">
|
||||
Edit alert
|
||||
</LinkButton>,
|
||||
]}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertRuleItem;
|
||||
|
@ -2,90 +2,72 @@
|
||||
|
||||
exports[`Render should render component 1`] = `
|
||||
<li
|
||||
className="alert-rule-item"
|
||||
className="css-1aih96i"
|
||||
>
|
||||
<Icon
|
||||
className="alert-rule-item__icon state class"
|
||||
name="icon"
|
||||
size="xl"
|
||||
/>
|
||||
<div
|
||||
className="alert-rule-item__body"
|
||||
>
|
||||
<div
|
||||
className="alert-rule-item__header"
|
||||
>
|
||||
<div
|
||||
className="alert-rule-item__name"
|
||||
>
|
||||
<a
|
||||
href="https://something.something.darkside?editPanel=1&tab=alert"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
searchWords={
|
||||
Array [
|
||||
"",
|
||||
]
|
||||
}
|
||||
textToHighlight="Some rule"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
className="alert-rule-item__text"
|
||||
>
|
||||
<span
|
||||
className="state class"
|
||||
>
|
||||
<Highlighter
|
||||
highlightClassName="highlight-search-match"
|
||||
searchWords={
|
||||
Array [
|
||||
"",
|
||||
]
|
||||
}
|
||||
textToHighlight="state text"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className="alert-rule-item__time"
|
||||
>
|
||||
for
|
||||
age
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert-rule-item__actions"
|
||||
>
|
||||
<Component
|
||||
spacing="sm"
|
||||
>
|
||||
<Component
|
||||
content="Pausing an alert rule prevents it from executing"
|
||||
placement="bottom"
|
||||
>
|
||||
<Component
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
icon="pause"
|
||||
onClick={[MockFunction]}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
/>
|
||||
</Component>
|
||||
<Component
|
||||
content="Edit alert rule"
|
||||
placement="right"
|
||||
>
|
||||
>
|
||||
Pause
|
||||
</Button>,
|
||||
<LinkButton
|
||||
href="https://something.something.darkside?editPanel=1&tab=alert"
|
||||
icon="cog"
|
||||
size="sm"
|
||||
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"
|
||||
/>
|
||||
</Component>
|
||||
</Component>
|
||||
</div>
|
||||
</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
Block a user