mirror of
https://github.com/grafana/grafana.git
synced 2025-01-06 14:13:11 -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 { BarGauge } from './BarGauge/BarGauge';
|
||||
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 {
|
||||
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;
|
||||
|
@ -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 = {
|
||||
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',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user