Skip to content

Commit

Permalink
[v11.0.x] EmptyState: Add call-to-action variant (#86439)
Browse files Browse the repository at this point in the history
* EmptyState: Add `call-to-action` variant (#85017)

* add default variant

* enhance story

* rename to "initial" + update docs

* default to showing a CTA

* use switch

* translations

* update and use LinkButton

* update default message

* rename "initial" to "nothing-here"

* update variant name to `call-to-action`

* i18n

* add CTA svg

* extract into its own component

(cherry picked from commit f99d5a1)

* remove console.log

---------

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
  • Loading branch information
grafana-delivery-bot[bot] and ashharrison90 committed Apr 17, 2024
1 parent 9b33f96 commit b5d8a75
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 15 deletions.
66 changes: 59 additions & 7 deletions packages/grafana-ui/src/components/EmptyState/EmptyState.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

<EmptyState
variant="call-to-action"
message="You haven't created any playlists yet"
button={
<LinkButton icon="plus" href="playlists/new" size="lg">
Create playlist
</LinkButton>
}
>
You can use playlists to cycle dashboards on TVs without user control.{' '}
<TextLink external href="<externalDocsLink>">
Learn more.
</TextLink>
</EmptyState>;
```

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';

<EmptyState variant="call-to-action" message="You haven't created any library panels yet">
Create a library panel from any existing dashboard panel through the panel context menu.{' '}
<TextLink external href="<externalDocsLink>">
Learn more.
</TextLink>
</EmptyState>;
```

### `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.

Expand All @@ -19,14 +66,19 @@ import { EmptyState } from '@grafana/ui';
<EmptyState variant="not-found" message="No playlists found" />;
```

### 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';

<EmptyState
variant="not-found"
button={<Button variant="secondary" onClick={clearSearchQuery} />}
image={<AnyReactNode />}
message="No playlists found"
Expand Down
25 changes: 21 additions & 4 deletions packages/grafana-ui/src/components/EmptyState/EmptyState.story.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -12,24 +14,39 @@ const meta: Meta<typeof EmptyState> = {
page: mdx,
},
controls: {
exclude: ['button', 'image', 'variant'],
exclude: ['image'],
},
},
argTypes: {
button: {
control: 'select',
options: ['None', 'Create', 'Clear filters'],
},
children: {
type: 'string',
},
},
};

export const Basic: StoryFn<typeof EmptyState> = (args) => {
return <EmptyState {...args} />;
let button;
if (args.button === 'Create') {
button = (
<Button icon="plus" size="lg">
Create dashboard
</Button>
);
} else if (args.button === 'Clear filters') {
button = <Button variant="secondary">Clear filters</Button>;
}
return <EmptyState {...args} button={button} />;
};

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;
26 changes: 22 additions & 4 deletions packages/grafana-ui/src/components/EmptyState/EmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 = <GrotNotFound width={300} />,
image,
message,
hideImage = false,
variant,
}: React.PropsWithChildren<Props>) => {
const imageToShow = image ?? getDefaultImageForVariant(variant);

return (
<Box paddingY={4} gap={4} display="flex" direction="column" alignItems="center">
{!hideImage && image}
{!hideImage && imageToShow}
<Stack direction="column" alignItems="center">
<Text variant="h4">{message}</Text>
{children && <Text color="secondary">{children}</Text>}
Expand All @@ -44,3 +48,17 @@ export const EmptyState = ({
</Box>
);
};

function getDefaultImageForVariant(variant: Props['variant']) {
switch (variant) {
case 'call-to-action': {
return <GrotCTA width={300} />;
}
case 'not-found': {
return <GrotNotFound width={300} />;
}
default: {
throw new Error(`Unknown variant: ${variant}`);
}
}
}
32 changes: 32 additions & 0 deletions packages/grafana-ui/src/components/EmptyState/GrotCTA/GrotCTA.tsx
Original file line number Diff line number Diff line change
@@ -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<SVGElement>['width'];
height?: SVGProps<SVGElement>['height'];
}

export const GrotCTA = ({ width = 'auto', height }: Props) => {
const styles = useStyles2(getStyles);

return <SVG src={grotCTASvg} className={styles.svg} height={height} width={width} />;
};

GrotCTA.displayName = 'GrotCTA';

const getStyles = (theme: GrafanaTheme2) => {
return {
svg: css({
'#grot-cta-cactus-1, #grot-cta-cactus-2': {
fill: theme.isDark ? '#58558c' : '#c9c5f4',
},
}),
};
};
46 changes: 46 additions & 0 deletions packages/grafana-ui/src/components/EmptyState/GrotCTA/grot-cta.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit b5d8a75

Please sign in to comment.