diff --git a/packages/grafana-ui/src/components/EmptyState/EmptyState.mdx b/packages/grafana-ui/src/components/EmptyState/EmptyState.mdx index 846652136a9..ed1e0863f70 100644 --- a/packages/grafana-ui/src/components/EmptyState/EmptyState.mdx +++ b/packages/grafana-ui/src/components/EmptyState/EmptyState.mdx @@ -3,13 +3,60 @@ import { EmptyState } from './EmptyState'; # EmptyState -Use an empty state to communicate to the user that there is no data to display, or that a search query returned no results. +The EmptyState component consists of a message and optionally an image, button, and additional information. -## variant="not-found" +## When to use -### When to use +Use an empty state to communicate to the user that there is no data to display and provide instructions for what to do next. +Example use cases: -Use in place of a results table or list when a search query or filter returns no results. +- When a user has not created a particular resource yet +- When a filter or search query returns no results +- When a user completes all actions, such as clearing their inbox or notifications + +There are different variants to handle the most common empty state use cases. These variants provide a specific default message and image. + +## Usage + +### `variant="call-to-action"` + +Use when there is no data to display and you want to encourage the user to take an action. Usually either to complete some initial configuration, or to create an item. + +```jsx +import { EmptyState, LinkButton, TextLink } from '@grafana/ui'; + + + Create playlist + + } +> + You can use playlists to cycle dashboards on TVs without user control.{' '} + + Learn more. + +; +``` + +For scenarios where there is no single button that can be clicked to create the item, you can omit the `button` prop. Instead, provide additional information to help the user understand how to create the specific resource. + +```jsx +import { EmptyState, TextLink } from '@grafana/ui'; + + + Create a library panel from any existing dashboard panel through the panel context menu.{' '} + + Learn more. + +; +``` + +### `variant="not-found"` + +Use in place of content when a search query or filter returns no results. There are sensible defaults for the image, so in most cases all you need to provide is a message. @@ -19,14 +66,19 @@ import { EmptyState } from '@grafana/ui'; ; ``` -### Providing custom overrides +## Customization -You can optionally override or hide the image, add additional information or a button (e.g. to clear the search query) +For all variants you can: + +- provide a custom image or hide the image entirely +- provide a button (e.g. to provide a call to action or clear the search query) +- provide additional information via React children ```jsx -import { Button, EmptyState } from '@grafana/ui'; +import { Button, EmptyState, TextLink } from '@grafana/ui'; } image={} message="No playlists found" diff --git a/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx b/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx index d4da9da7cff..9e81d2e234e 100644 --- a/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx +++ b/packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx @@ -1,6 +1,8 @@ import { Meta, StoryFn } from '@storybook/react'; import React from 'react'; +import { Button } from '../Button'; + import { EmptyState } from './EmptyState'; import mdx from './EmptyState.mdx'; @@ -12,10 +14,14 @@ const meta: Meta = { page: mdx, }, controls: { - exclude: ['button', 'image', 'variant'], + exclude: ['image'], }, }, argTypes: { + button: { + control: 'select', + options: ['None', 'Create', 'Clear filters'], + }, children: { type: 'string', }, @@ -23,13 +29,24 @@ const meta: Meta = { }; export const Basic: StoryFn = (args) => { - return ; + let button; + if (args.button === 'Create') { + button = ( + + ); + } else if (args.button === 'Clear filters') { + button = ; + } + return ; }; Basic.args = { + button: 'Create', children: 'Use this space to add any additional information', - message: 'No results found', - variant: 'not-found', + message: "You haven't created any dashboards yet", + variant: 'call-to-action', }; export default meta; diff --git a/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx b/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx index 4fa685e2d19..97340c52c41 100644 --- a/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx +++ b/packages/grafana-ui/src/components/EmptyState/EmptyState.tsx @@ -4,6 +4,7 @@ import { Box } from '../Layout/Box/Box'; import { Stack } from '../Layout/Stack/Stack'; import { Text } from '../Text/Text'; +import { GrotCTA } from './GrotCTA/GrotCTA'; import { GrotNotFound } from './GrotNotFound/GrotNotFound'; interface Props { @@ -21,21 +22,24 @@ interface Props { */ message: string; /** - * Empty state variant. Possible values are 'search'. + * Which variant to use. Affects the default image shown. */ - variant: 'not-found'; + variant: 'call-to-action' | 'not-found'; } export const EmptyState = ({ button, children, - image = , + image, message, hideImage = false, + variant, }: React.PropsWithChildren) => { + const imageToShow = image ?? getDefaultImageForVariant(variant); + return ( - {!hideImage && image} + {!hideImage && imageToShow} {message} {children && {children}} @@ -44,3 +48,17 @@ export const EmptyState = ({ ); }; + +function getDefaultImageForVariant(variant: Props['variant']) { + switch (variant) { + case 'call-to-action': { + return ; + } + case 'not-found': { + return ; + } + default: { + throw new Error(`Unknown variant: ${variant}`); + } + } +} diff --git a/packages/grafana-ui/src/components/EmptyState/GrotCTA/GrotCTA.tsx b/packages/grafana-ui/src/components/EmptyState/GrotCTA/GrotCTA.tsx new file mode 100644 index 00000000000..ce7d363fb6f --- /dev/null +++ b/packages/grafana-ui/src/components/EmptyState/GrotCTA/GrotCTA.tsx @@ -0,0 +1,32 @@ +import { css } from '@emotion/css'; +import React, { SVGProps } from 'react'; +import SVG from 'react-inlinesvg'; + +import { GrafanaTheme2 } from '@grafana/data'; + +import { useStyles2 } from '../../../themes'; + +import grotCTASvg from './grot-cta.svg'; + +export interface Props { + width?: SVGProps['width']; + height?: SVGProps['height']; +} + +export const GrotCTA = ({ width = 'auto', height }: Props) => { + const styles = useStyles2(getStyles); + + return ; +}; + +GrotCTA.displayName = 'GrotCTA'; + +const getStyles = (theme: GrafanaTheme2) => { + return { + svg: css({ + '#grot-cta-cactus-1, #grot-cta-cactus-2': { + fill: theme.isDark ? '#58558c' : '#c9c5f4', + }, + }), + }; +}; diff --git a/packages/grafana-ui/src/components/EmptyState/GrotCTA/grot-cta.svg b/packages/grafana-ui/src/components/EmptyState/GrotCTA/grot-cta.svg new file mode 100644 index 00000000000..7e44c92839c --- /dev/null +++ b/packages/grafana-ui/src/components/EmptyState/GrotCTA/grot-cta.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +