From 206921d21b8c3acf71410c938259c06de3167cd9 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Wed, 27 Mar 2019 22:33:20 +0100 Subject: [PATCH] Feature: Introduced CallToActionCard to @grafana/ui (#16237) CallToActionCard is an abstraction to display a card with message, call to action element and a footer. It is used i.e. on datasource add page. --- .../CallToActionCard.story.tsx | 34 +++++++++ .../CallToActionCard.test.tsx | 38 ++++++++++ .../CallToActionCard/CallToActionCard.tsx | 49 ++++++++++++ .../CallToActionCard.test.tsx.snap | 52 +++++++++++++ packages/grafana-ui/src/components/index.ts | 2 + .../EmptyListCTA/EmptyListCTA.test.tsx | 22 ------ .../components/EmptyListCTA/EmptyListCTA.tsx | 76 ++++++++++--------- .../__snapshots__/EmptyListCTA.test.tsx.snap | 39 ---------- public/app/features/alerting/AlertTab.tsx | 2 +- 9 files changed, 218 insertions(+), 96 deletions(-) create mode 100644 packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx create mode 100644 packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.test.tsx create mode 100644 packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx create mode 100644 packages/grafana-ui/src/components/CallToActionCard/__snapshots__/CallToActionCard.test.tsx.snap delete mode 100644 public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx delete mode 100644 public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx new file mode 100644 index 00000000000..6d7c9ab97b5 --- /dev/null +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.story.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { renderComponentWithTheme } from '../../utils/storybook/withTheme'; +import { CallToActionCard } from './CallToActionCard'; +import { select, text } from '@storybook/addon-knobs'; +import { LargeButton } from '../Button/Button'; +import { action } from '@storybook/addon-actions'; + +const CallToActionCardStories = storiesOf('UI/CallToActionCard', module); + +CallToActionCardStories.add('default', () => { + const ctaElements: { [key: string]: JSX.Element } = { + custom:

This is just H1 tag, you can any component as CTA element

, + button: ( + + Add datasource + + ), + }; + const ctaElement = select( + 'Call to action element', + { + Custom: 'custom', + Button: 'button', + }, + 'custom' + ); + + return renderComponentWithTheme(CallToActionCard, { + message: text('Call to action message', 'Renders message prop content'), + callToActionElement: ctaElements[ctaElement], + footer: text('Call to action footer', 'Renders footer prop content'), + }); +}); diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.test.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.test.tsx new file mode 100644 index 00000000000..afd3d6c87e5 --- /dev/null +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.test.tsx @@ -0,0 +1,38 @@ +import React, { useContext } from 'react'; +import { render } from 'enzyme'; +import { CallToActionCard, CallToActionCardProps } from './CallToActionCard'; +import { ThemeContext } from '../../themes'; + +type Omit = Pick>; + +const TestRenderer = (props: Omit) => { + const theme = useContext(ThemeContext); + return ; +}; + +describe('CallToActionCard', () => { + describe('rendering', () => { + it('when no message and footer provided', () => { + const tree = render(Click me} />); + expect(tree).toMatchSnapshot(); + }); + + it('when message and no footer provided', () => { + const tree = render( + Click me} /> + ); + expect(tree).toMatchSnapshot(); + }); + + it('when message and footer provided', () => { + const tree = render( + Click me} + /> + ); + expect(tree).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx new file mode 100644 index 00000000000..002382762cc --- /dev/null +++ b/packages/grafana-ui/src/components/CallToActionCard/CallToActionCard.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Themeable, GrafanaTheme } from '../../types/theme'; +import { selectThemeVariant } from '../../themes/selectThemeVariant'; +import { css, cx } from 'emotion'; + +export interface CallToActionCardProps extends Themeable { + message?: string | JSX.Element; + callToActionElement: JSX.Element; + footer?: string | JSX.Element; + className?: string; +} + +const getCallToActionCardStyles = (theme: GrafanaTheme) => ({ + wrapper: css` + label: call-to-action-card; + padding: ${theme.spacing.lg}; + background: ${selectThemeVariant({ light: theme.colors.gray6, dark: theme.colors.grayBlue }, theme.type)}; + border-radius: ${theme.border.radius.md}; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + `, + message: css` + margin-bottom: ${theme.spacing.lg}; + font-style: italic; + `, + footer: css` + margin-top: ${theme.spacing.lg}; + `, +}); + +export const CallToActionCard: React.FunctionComponent = ({ + message, + callToActionElement, + footer, + theme, + className, +}) => { + const css = getCallToActionCardStyles(theme); + + return ( +
+ {message &&
{message}
} + {callToActionElement} + {footer &&
{footer}
} +
+ ); +}; diff --git a/packages/grafana-ui/src/components/CallToActionCard/__snapshots__/CallToActionCard.test.tsx.snap b/packages/grafana-ui/src/components/CallToActionCard/__snapshots__/CallToActionCard.test.tsx.snap new file mode 100644 index 00000000000..4c677f7bffd --- /dev/null +++ b/packages/grafana-ui/src/components/CallToActionCard/__snapshots__/CallToActionCard.test.tsx.snap @@ -0,0 +1,52 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CallToActionCard rendering when message and footer provided 1`] = ` +
+
+ Click button bellow +
+ + Click me + +
+ footer content +
+
+`; + +exports[`CallToActionCard rendering when message and no footer provided 1`] = ` +
+
+ Click button bellow +
+ + Click me + +
+`; + +exports[`CallToActionCard rendering when no message and footer provided 1`] = ` + +`; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index a79e0ede6b8..2cc86c83db2 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -37,3 +37,5 @@ export { Gauge } from './Gauge/Gauge'; export { Graph } from './Graph/Graph'; export { BarGauge } from './BarGauge/BarGauge'; export { VizRepeater } from './VizRepeater/VizRepeater'; + +export { CallToActionCard } from './CallToActionCard/CallToActionCard'; diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx deleted file mode 100644 index ac07b671c31..00000000000 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import EmptyListCTA from './EmptyListCTA'; - -const model = { - title: 'Title', - buttonIcon: 'ga css class', - buttonLink: 'http://url/to/destination', - buttonTitle: 'Click me', - onClick: jest.fn(), - proTip: 'This is a tip', - proTipLink: 'http://url/to/tip/destination', - proTipLinkTitle: 'Learn more', - proTipTarget: '_blank', -}; - -describe('EmptyListCTA', () => { - it('renders correctly', () => { - const tree = shallow(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index 6b5c6ebb7ca..d36eeaaf6ff 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -1,40 +1,48 @@ -import React, { Component } from 'react'; - +import React, { useContext } from 'react'; +import { CallToActionCard, ExtraLargeLinkButton, ThemeContext } from '@grafana/ui'; +import { css } from 'emotion'; export interface Props { model: any; } -class EmptyListCTA extends Component { - render() { - const { - title, - buttonIcon, - buttonLink, - buttonTitle, - onClick, - proTip, - proTipLink, - proTipLinkTitle, - proTipTarget, - } = this.props.model; - return ( -
-
{title}
- - - {buttonTitle} - - {proTip && ( -
- ProTip: {proTip} - - {proTipLinkTitle} - -
- )} -
- ); - } -} +const EmptyListCTA: React.FunctionComponent = props => { + const theme = useContext(ThemeContext); + + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + onClick, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget, + } = props.model; + + const footer = proTip ? ( + + + <> ProTip: {proTip} + + {proTipLinkTitle} + + + ) : null; + + const ctaElementClassName = !footer + ? css` + margin-bottom: 20px; + ` + : ''; + + const ctaElement = ( + + {buttonTitle} + + ); + + return ; +}; export default EmptyListCTA; diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap deleted file mode 100644 index 21c2ed294b4..00000000000 --- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap +++ /dev/null @@ -1,39 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`EmptyListCTA renders correctly 1`] = ` -
-
- Title -
- - - Click me - -
- - ProTip: - This is a tip - - Learn more - -
-
-`; diff --git a/public/app/features/alerting/AlertTab.tsx b/public/app/features/alerting/AlertTab.tsx index 6343b4ca2c9..9e114e559f5 100644 --- a/public/app/features/alerting/AlertTab.tsx +++ b/public/app/features/alerting/AlertTab.tsx @@ -133,7 +133,7 @@ export class AlertTab extends PureComponent { const model = { title: 'Panel has no alert rule defined', - icon: 'icon-gf icon-gf-alert', + buttonIcon: 'icon-gf icon-gf-alert', onClick: this.onAddAlert, buttonTitle: 'Create Alert', };