mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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.
This commit is contained in:
parent
5324bb4f11
commit
206921d21b
@ -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'),
|
||||||
|
});
|
||||||
|
});
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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>
|
||||||
|
`;
|
@ -37,3 +37,5 @@ export { Gauge } from './Gauge/Gauge';
|
|||||||
export { Graph } from './Graph/Graph';
|
export { Graph } from './Graph/Graph';
|
||||||
export { BarGauge } from './BarGauge/BarGauge';
|
export { BarGauge } from './BarGauge/BarGauge';
|
||||||
export { VizRepeater } from './VizRepeater/VizRepeater';
|
export { VizRepeater } from './VizRepeater/VizRepeater';
|
||||||
|
|
||||||
|
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
|
||||||
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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 {
|
export interface Props {
|
||||||
model: any;
|
model: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmptyListCTA extends Component<Props, any> {
|
const EmptyListCTA: React.FunctionComponent<Props> = props => {
|
||||||
render() {
|
const theme = useContext(ThemeContext);
|
||||||
const {
|
|
||||||
title,
|
const {
|
||||||
buttonIcon,
|
title,
|
||||||
buttonLink,
|
buttonIcon,
|
||||||
buttonTitle,
|
buttonLink,
|
||||||
onClick,
|
buttonTitle,
|
||||||
proTip,
|
onClick,
|
||||||
proTipLink,
|
proTip,
|
||||||
proTipLinkTitle,
|
proTipLink,
|
||||||
proTipTarget,
|
proTipLinkTitle,
|
||||||
} = this.props.model;
|
proTipTarget,
|
||||||
return (
|
} = props.model;
|
||||||
<div className="empty-list-cta">
|
|
||||||
<div className="empty-list-cta__title">{title}</div>
|
const footer = proTip ? (
|
||||||
<a onClick={onClick} href={buttonLink} className="empty-list-cta__button btn btn-xlarge btn-primary">
|
<span>
|
||||||
<i className={buttonIcon} />
|
<i className="fa fa-rocket" />
|
||||||
{buttonTitle}
|
<> ProTip: {proTip} </>
|
||||||
</a>
|
<a href={proTipLink} target={proTipTarget} className="text-link">
|
||||||
{proTip && (
|
{proTipLinkTitle}
|
||||||
<div className="empty-list-cta__pro-tip">
|
</a>
|
||||||
<i className="fa fa-rocket" /> ProTip: {proTip}
|
</span>
|
||||||
<a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
|
) : null;
|
||||||
{proTipLinkTitle}
|
|
||||||
</a>
|
const ctaElementClassName = !footer
|
||||||
</div>
|
? css`
|
||||||
)}
|
margin-bottom: 20px;
|
||||||
</div>
|
`
|
||||||
);
|
: '';
|
||||||
}
|
|
||||||
}
|
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;
|
export default EmptyListCTA;
|
||||||
|
@ -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>
|
|
||||||
`;
|
|
@ -133,7 +133,7 @@ export class AlertTab extends PureComponent<Props> {
|
|||||||
|
|
||||||
const model = {
|
const model = {
|
||||||
title: 'Panel has no alert rule defined',
|
title: 'Panel has no alert rule defined',
|
||||||
icon: 'icon-gf icon-gf-alert',
|
buttonIcon: 'icon-gf icon-gf-alert',
|
||||||
onClick: this.onAddAlert,
|
onClick: this.onAddAlert,
|
||||||
buttonTitle: 'Create Alert',
|
buttonTitle: 'Create Alert',
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user