Browse Source

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.
Dominik Prokop 6 years ago
parent
commit
206921d21b

+ 34 - 0
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: <h1>This is just H1 tag, you can any component as CTA element</h1>,
+    button: (
+      <LargeButton icon="fa fa-plus" onClick={action('cta button clicked')}>
+        Add datasource
+      </LargeButton>
+    ),
+  };
+  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'),
+  });
+});

+ 38 - 0
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<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
+
+const TestRenderer = (props: Omit<CallToActionCardProps, 'theme'>) => {
+  const theme = useContext(ThemeContext);
+  return <CallToActionCard theme={theme} {...props} />;
+};
+
+describe('CallToActionCard', () => {
+  describe('rendering', () => {
+    it('when no message and footer provided', () => {
+      const tree = render(<TestRenderer callToActionElement={<a href="http://dummy.link">Click me</a>} />);
+      expect(tree).toMatchSnapshot();
+    });
+
+    it('when message and no footer provided', () => {
+      const tree = render(
+        <TestRenderer message="Click button bellow" callToActionElement={<a href="http://dummy.link">Click me</a>} />
+      );
+      expect(tree).toMatchSnapshot();
+    });
+
+    it('when message and footer provided', () => {
+      const tree = render(
+        <TestRenderer
+          message="Click button bellow"
+          footer="footer content"
+          callToActionElement={<a href="http://dummy.link">Click me</a>}
+        />
+      );
+      expect(tree).toMatchSnapshot();
+    });
+  });
+});

+ 49 - 0
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<CallToActionCardProps> = ({
+  message,
+  callToActionElement,
+  footer,
+  theme,
+  className,
+}) => {
+  const css = getCallToActionCardStyles(theme);
+
+  return (
+    <div className={cx([css.wrapper, className])}>
+      {message && <div className={css.message}>{message}</div>}
+      {callToActionElement}
+      {footer && <div className={css.footer}>{footer}</div>}
+    </div>
+  );
+};

+ 52 - 0
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`] = `
+<div
+  class="css-1ph0cdx-call-to-action-card"
+>
+  <div
+    class="css-m2iibx"
+  >
+    Click button bellow
+  </div>
+  <a
+    href="http://dummy.link"
+  >
+    Click me
+  </a>
+  <div
+    class="css-1sg2huk"
+  >
+    footer content
+  </div>
+</div>
+`;
+
+exports[`CallToActionCard rendering when message and no footer provided 1`] = `
+<div
+  class="css-1ph0cdx-call-to-action-card"
+>
+  <div
+    class="css-m2iibx"
+  >
+    Click button bellow
+  </div>
+  <a
+    href="http://dummy.link"
+  >
+    Click me
+  </a>
+</div>
+`;
+
+exports[`CallToActionCard rendering when no message and footer provided 1`] = `
+<div
+  class="css-1ph0cdx-call-to-action-card"
+>
+  <a
+    href="http://dummy.link"
+  >
+    Click me
+  </a>
+</div>
+`;

+ 2 - 0
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';

+ 0 - 22
public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx

@@ -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(<EmptyListCTA model={model} />);
-    expect(tree).toMatchSnapshot();
-  });
-});

+ 42 - 34
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<Props, any> {
-  render() {
-    const {
-      title,
-      buttonIcon,
-      buttonLink,
-      buttonTitle,
-      onClick,
-      proTip,
-      proTipLink,
-      proTipLinkTitle,
-      proTipTarget,
-    } = this.props.model;
-    return (
-      <div className="empty-list-cta">
-        <div className="empty-list-cta__title">{title}</div>
-        <a onClick={onClick} href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-primary">
-          <i className={buttonIcon} />
-          {buttonTitle}
-        </a>
-        {proTip && (
-          <div className="empty-list-cta__pro-tip">
-            <i className="fa fa-rocket" /> ProTip: {proTip}
-            <a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
-              {proTipLinkTitle}
-            </a>
-          </div>
-        )}
-      </div>
-    );
-  }
-}
+const EmptyListCTA: React.FunctionComponent<Props> = props => {
+  const theme = useContext(ThemeContext);
+
+  const {
+    title,
+    buttonIcon,
+    buttonLink,
+    buttonTitle,
+    onClick,
+    proTip,
+    proTipLink,
+    proTipLinkTitle,
+    proTipTarget,
+  } = props.model;
+
+  const footer = proTip ? (
+    <span>
+      <i className="fa fa-rocket" />
+      <> ProTip: {proTip} </>
+      <a href={proTipLink} target={proTipTarget} className="text-link">
+        {proTipLinkTitle}
+      </a>
+    </span>
+  ) : null;
+
+  const ctaElementClassName = !footer
+    ? css`
+        margin-bottom: 20px;
+      `
+    : '';
+
+  const ctaElement = (
+    <ExtraLargeLinkButton onClick={onClick} href={buttonLink} icon={buttonIcon} className={ctaElementClassName}>
+      {buttonTitle}
+    </ExtraLargeLinkButton>
+  );
+
+  return <CallToActionCard message={title} footer={footer} callToActionElement={ctaElement} theme={theme} />;
+};
 
 export default EmptyListCTA;

+ 0 - 39
public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap

@@ -1,39 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EmptyListCTA renders correctly 1`] = `
-<div
-  className="empty-list-cta"
->
-  <div
-    className="empty-list-cta__title"
-  >
-    Title
-  </div>
-  <a
-    className="empty-list-cta__button btn btn-xlarge btn-primary"
-    href="http://url/to/destination"
-    onClick={[MockFunction]}
-  >
-    <i
-      className="ga css class"
-    />
-    Click me
-  </a>
-  <div
-    className="empty-list-cta__pro-tip"
-  >
-    <i
-      className="fa fa-rocket"
-    />
-     ProTip: 
-    This is a tip
-    <a
-      className="text-link empty-list-cta__pro-tip-link"
-      href="http://url/to/tip/destination"
-      target="_blank"
-    >
-      Learn more
-    </a>
-  </div>
-</div>
-`;

+ 1 - 1
public/app/features/alerting/AlertTab.tsx

@@ -133,7 +133,7 @@ export class AlertTab extends PureComponent<Props> {
 
     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',
     };