Commit 206921d2 by Dominik Prokop Committed by GitHub

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.
parent 5324bb4f
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'),
});
});
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();
});
});
});
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>
);
};
// 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>
`;
......@@ -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';
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();
});
});
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;
// 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>
`;
......@@ -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',
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment