Commit f5ee1f93 by Alex Khomenko Committed by GitHub

Grafana-UI: Alert components tweaks (#27128)

* Grafana-UI: Add Alert docs and story

* Grafana-UI: Move Alert styles to emotion

* Grafana-UI: Tweak docs

* Grafana-UI: Update test

* Grafana-UI: Add outline to custom content

* Grafana-UI: Remove class name

* Grafana-UI: Remove _Alert.scss

* Grafana-UI: Add e2e Alert selector

* Grafana-UI: Use @deprecated

* Grafana-UI: Remove circular reference

* Grafana-UI: Final tweaks
parent 88fbdd67
...@@ -95,6 +95,9 @@ export const Components = { ...@@ -95,6 +95,9 @@ export const Components = {
AlertTab: { AlertTab: {
content: 'Alert editor tab content', content: 'Alert editor tab content',
}, },
Alert: {
alert: (severity: string) => `Alert ${severity}`,
},
TransformTab: { TransformTab: {
content: 'Transform editor tab content', content: 'Transform editor tab content',
newTransform: (title: string) => `New transform ${title}`, newTransform: (title: string) => `New transform ${title}`,
......
...@@ -2,6 +2,6 @@ import { e2e } from '../index'; ...@@ -2,6 +2,6 @@ import { e2e } from '../index';
export const assertSuccessNotification = () => { export const assertSuccessNotification = () => {
e2e() e2e()
.get('.alert-success') .get('[aria-label^="Alert success"]')
.should('exist'); .should('exist');
}; };
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { Alert } from './Alert';
<Meta title="MDX|Alert" component={Alert} />
# Alert
An alert displays an important message in a way that attracts the user's attention without interrupting the user's task.
`onRemove` handler can be used to enable manually dismissing the alert.
# Usage
```jsx
<Alert title="Some very important message" severity="info" />
```
<Props of={Alert}/>
import React from 'react';
import { select } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { Alert, AlertVariant } from './Alert';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import mdx from '../Alert/Alert.mdx';
export default {
title: 'Overlays/Alert',
component: Alert,
decorators: [withCenteredStory, withHorizontallyCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const severities: AlertVariant[] = ['error', 'warning', 'info', 'success'];
export const basic = () => {
const severity = select('Severity', severities, 'info');
return <Alert title="Some very important message" severity={severity} />;
};
export const withRemove = () => {
const severity = select('Severity', severities, 'info');
return <Alert title="Some very important message" severity={severity} onRemove={action('Remove button clicked')} />;
};
export const customButtonContent = () => {
const severity = select('Severity', severities, 'info');
return (
<Alert
title="Some very important message"
severity={severity}
buttonContent={<span>Close</span>}
onRemove={action('Remove button clicked')}
/>
);
};
import React, { FC, ReactNode } from 'react'; import React, { FC, ReactNode } from 'react';
import classNames from 'classnames'; import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useTheme } from '../../themes';
import { Icon } from '../Icon/Icon'; import { Icon } from '../Icon/Icon';
import { IconName } from '../../types/icon'; import { IconName } from '../../types/icon';
export type AlertVariant = 'success' | 'warning' | 'error' | 'info'; export type AlertVariant = 'success' | 'warning' | 'error' | 'info';
interface AlertProps { export interface Props {
title: string; title: string;
buttonText?: string; /** On click handler for alert button, mostly used for dismissing the alert */
onButtonClick?: (event: React.MouseEvent) => void;
onRemove?: (event: React.MouseEvent) => void; onRemove?: (event: React.MouseEvent) => void;
severity?: AlertVariant; severity?: AlertVariant;
children?: ReactNode; children?: ReactNode;
/** Custom component or text for alert button */
buttonContent?: ReactNode | string;
/** @deprecated */
/** Deprecated use onRemove instead */
onButtonClick?: (event: React.MouseEvent) => void;
/** @deprecated */
/** Deprecated use buttonContent instead */
buttonText?: string;
} }
function getIconFromSeverity(severity: AlertVariant): string { function getIconFromSeverity(severity: AlertVariant): string {
switch (severity) { switch (severity) {
case 'error': { case 'error':
return 'exclamation-triangle'; case 'warning':
}
case 'warning': {
return 'exclamation-triangle'; return 'exclamation-triangle';
} case 'info':
case 'info': {
return 'info-circle'; return 'info-circle';
} case 'success':
case 'success': {
return 'check'; return 'check';
}
default: default:
return ''; return '';
} }
} }
export const Alert: FC<AlertProps> = ({ title, buttonText, onButtonClick, onRemove, children, severity = 'error' }) => { export const Alert: FC<Props> = ({
const alertClass = classNames('alert', `alert-${severity}`); title,
buttonText,
onButtonClick,
onRemove,
children,
buttonContent,
severity = 'error',
}) => {
const theme = useTheme();
const styles = getStyles(theme, severity, !!buttonContent);
return ( return (
<div className="alert-container"> <div className={styles.container}>
<div className={alertClass}> <div className={styles.alert} aria-label={selectors.components.Alert.alert(severity)}>
<div className="alert-icon"> <div className={styles.icon}>
<Icon size="xl" name={getIconFromSeverity(severity) as IconName} /> <Icon size="xl" name={getIconFromSeverity(severity) as IconName} />
</div> </div>
<div className="alert-body"> <div className={styles.body}>
<div className="alert-title">{title}</div> <div className={styles.title}>{title}</div>
{children && <div className="alert-text">{children}</div>} {children && <div>{children}</div>}
</div> </div>
{/* If onRemove is specified , giving preference to onRemove */} {/* If onRemove is specified, giving preference to onRemove */}
{onRemove && ( {onRemove ? (
<button type="button" className="alert-close" onClick={onRemove}> <button type="button" className={styles.close} onClick={onRemove}>
<Icon name="times" size="lg" /> {buttonContent || <Icon name="times" size="lg" />}
</button> </button>
)} ) : onButtonClick ? (
{onButtonClick && (
<button type="button" className="btn btn-outline-danger" onClick={onButtonClick}> <button type="button" className="btn btn-outline-danger" onClick={onButtonClick}>
{buttonText} {buttonText}
</button> </button>
)} ) : null}
</div> </div>
</div> </div>
); );
}; };
const getStyles = (theme: GrafanaTheme, severity: AlertVariant, outline: boolean) => {
const { redBase, redShade, greenBase, greenShade, blue80, blue77, white } = theme.palette;
const backgrounds = {
error: css`
background: linear-gradient(90deg, ${redBase}, ${redShade});
`,
warning: css`
background: linear-gradient(90deg, ${redBase}, ${redShade});
`,
info: css`
background: linear-gradient(100deg, ${blue80}, ${blue77});
`,
success: css`
background: linear-gradient(100deg, ${greenBase}, ${greenShade});
`,
};
return {
container: css`
z-index: ${theme.zIndex.tooltip};
`,
alert: css`
padding: 15px 20px;
margin-bottom: ${theme.spacing.xs};
position: relative;
color: ${white};
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
border-radius: ${theme.border.radius.md};
display: flex;
flex-direction: row;
align-items: center;
${backgrounds[severity]}
`,
icon: css`
padding: 0 ${theme.spacing.md} 0 0;
display: flex;
align-items: center;
justify-content: center;
width: 35px;
`,
title: css`
font-weight: ${theme.typography.weight.semibold};
`,
body: css`
flex-grow: 1;
margin: 0 ${theme.spacing.md} 0 0;
a {
color: ${white};
text-decoration: underline;
}
`,
close: css`
background: none;
display: flex;
align-items: center;
border: ${outline ? `1px solid ${white}` : 'none'};
border-radius: ${theme.border.radius.sm};
`,
};
};
//
// Alerts
// --------------------------------------------------
// Base styles
// -------------------------
.alert {
padding: 15px 20px;
margin-bottom: $space-xs;
text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5);
background: $alert-error-bg;
position: relative;
color: $white;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2);
border-radius: $border-radius;
display: flex;
flex-direction: row;
align-items: center;
}
// Alternate styles
// -------------------------
.alert-success {
background: $alert-success-bg;
}
.alert-danger,
.alert-error {
background: $alert-error-bg;
}
.alert-info {
background: $alert-info-bg;
}
.alert-warning {
background: $alert-warning-bg;
}
.alert-container {
z-index: $zindex-tooltip;
}
.page-alert-list {
z-index: 8000;
min-width: 400px;
max-width: 600px;
position: fixed;
right: 10px;
top: 60px;
}
.alert-close {
padding: 0 0 0 $space-md;
border: none;
background: none;
display: flex;
align-items: center;
.fa {
align-self: flex-end;
font-size: 21px;
color: rgba(255, 255, 255, 0.75);
}
}
.alert-title {
font-weight: $font-weight-semi-bold;
}
.alert-icon {
padding: 0 $space-md 0 0;
display: flex;
align-items: center;
justify-content: center;
width: 35px;
.fa {
font-size: 21px;
}
}
.alert-body {
flex-grow: 1;
a {
color: $white;
text-decoration: underline;
}
}
.alert-icon-on-top {
align-items: flex-start;
}
...@@ -12,5 +12,4 @@ ...@@ -12,5 +12,4 @@
@import 'TableInputCSV/TableInputCSV'; @import 'TableInputCSV/TableInputCSV';
@import 'TimePicker/TimeOfDayPicker'; @import 'TimePicker/TimeOfDayPicker';
@import 'Tooltip/Tooltip'; @import 'Tooltip/Tooltip';
@import 'Alert/Alert';
@import 'Slider/Slider'; @import 'Slider/Slider';
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