Notification history: Use Card instead of reusing alert (#49418)

* Use Card instead of reusing alert

* only need clearSelectedNotifications now
This commit is contained in:
Ashley Harrison 2022-05-23 16:48:17 +01:00 committed by GitHub
parent ce86b4ebe7
commit fe16680c6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 57 additions and 114 deletions

View File

@ -1,105 +1,63 @@
import { css, cx } from '@emotion/css';
import { css } from '@emotion/css';
import { formatDistanceToNow } from 'date-fns';
import React, { ReactNode } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Icon, IconName, useTheme2 } from '@grafana/ui';
import { getIconFromSeverity } from '@grafana/ui/src/components/Alert/Alert';
import { Card, Checkbox, useTheme2 } from '@grafana/ui';
export type AlertVariant = 'success' | 'warning' | 'error' | 'info';
export interface Props {
children?: ReactNode;
className?: string;
title: string;
isSelected: boolean;
onClick: () => void;
severity?: AlertVariant;
title: string;
timestamp?: number;
traceId?: string;
children?: ReactNode;
}
export const StoredNotificationItem = ({
children,
className,
title,
isSelected,
onClick,
severity = 'error',
title,
traceId,
timestamp,
children,
}: Props) => {
const theme = useTheme2();
const styles = getStyles(theme, severity);
const styles = getStyles(theme);
const showTraceId = config.featureToggles.tracing && traceId;
return (
<div className={cx(styles.wrapper, className)}>
<div className={styles.icon}>
<Icon size="xl" name={getIconFromSeverity(severity) as IconName} />
</div>
<div className={styles.title}>{title}</div>
<div className={styles.body}>{children}</div>
<span className={styles.trace}>{showTraceId && `Trace ID: ${traceId}`}</span>
{timestamp && <span className={styles.timestamp}>{formatDistanceToNow(timestamp, { addSuffix: true })}</span>}
</div>
<Card className={className} onClick={onClick}>
<Card.Heading>{title}</Card.Heading>
<Card.Description>{children}</Card.Description>
<Card.Figure>
<Checkbox onChange={onClick} tabIndex={-1} value={isSelected} />
</Card.Figure>
<Card.Tags className={styles.trace}>
{showTraceId && <span>{`Trace ID: ${traceId}`}</span>}
{timestamp && formatDistanceToNow(timestamp, { addSuffix: true })}
</Card.Tags>
</Card>
);
};
const getStyles = (theme: GrafanaTheme2, severity: AlertVariant) => {
const color = theme.colors[severity];
const borderRadius = theme.shape.borderRadius();
const getStyles = (theme: GrafanaTheme2) => {
return {
wrapper: css({
display: 'grid',
gridTemplateColumns: 'auto 1fr auto',
gridTemplateRows: 'auto 1fr auto',
gridTemplateAreas: `
'icon title close'
'icon body body'
'icon trace timestamp'`,
gap: `0 ${theme.spacing(2)}`,
background: theme.colors.background.secondary,
borderRadius: borderRadius,
}),
icon: css({
gridArea: 'icon',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: theme.spacing(2, 3),
background: color.main,
color: color.contrastText,
borderRadius: `${borderRadius} 0 0 ${borderRadius}`,
}),
title: css({
gridArea: 'title',
alignSelf: 'center',
fontWeight: theme.typography.fontWeightMedium,
color: theme.colors.text.primary,
paddingTop: theme.spacing(1),
}),
body: css({
gridArea: 'body',
maxHeight: '50vh',
marginRight: theme.spacing(1),
overflowY: 'auto',
overflowWrap: 'break-word',
wordBreak: 'break-word',
color: theme.colors.text.secondary,
}),
trace: css({
gridArea: 'trace',
justifySelf: 'start',
alignSelf: 'end',
paddingBottom: theme.spacing(1),
fontSize: theme.typography.pxToRem(10),
alignItems: 'flex-end',
alignSelf: 'flex-end',
color: theme.colors.text.secondary,
}),
timestamp: css({
gridArea: 'timestamp',
alignSelf: 'end',
padding: theme.spacing(1),
display: 'flex',
flexDirection: 'column',
fontSize: theme.typography.pxToRem(10),
color: theme.colors.text.secondary,
justifySelf: 'flex-end',
}),
};
};

View File

@ -3,7 +3,7 @@ import React, { useRef, useState } from 'react';
import { useEffectOnce } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Button, Checkbox, Icon, useStyles2 } from '@grafana/ui';
import { Alert, Button, Checkbox, Icon, useStyles2 } from '@grafana/ui';
import { StoredNotificationItem } from 'app/core/components/AppNotifications/StoredNotificationItem';
import {
clearAllNotifications,
@ -18,6 +18,9 @@ export function StoredNotifications() {
const dispatch = useDispatch();
const notifications = useSelector((state) => selectWarningsAndErrors(state.appNotifications));
const [selectedNotificationIds, setSelectedNotificationIds] = useState<string[]>([]);
const allNotificationsSelected = notifications.every((notification) =>
selectedNotificationIds.includes(notification.id)
);
const lastReadTimestamp = useRef(useSelector((state) => selectLastReadTimestamp(state.appNotifications)));
const styles = useStyles2(getStyles);
@ -26,19 +29,23 @@ export function StoredNotifications() {
});
const clearSelectedNotifications = () => {
selectedNotificationIds.forEach((id) => {
dispatch(clearNotification(id));
});
if (allNotificationsSelected) {
dispatch(clearAllNotifications());
} else {
selectedNotificationIds.forEach((id) => {
dispatch(clearNotification(id));
});
}
setSelectedNotificationIds([]);
};
const clearAllNotifs = () => {
dispatch(clearAllNotifications());
const handleAllCheckboxToggle = (isChecked: boolean) => {
setSelectedNotificationIds(isChecked ? notifications.map((n) => n.id) : []);
};
const handleCheckboxToggle = (id: string, isChecked: boolean) => {
const handleCheckboxToggle = (id: string) => {
setSelectedNotificationIds((prevState) => {
if (isChecked && !prevState.includes(id)) {
if (!prevState.includes(id)) {
return [...prevState, id];
} else {
return prevState.filter((notificationId) => notificationId !== id);
@ -57,27 +64,26 @@ export function StoredNotifications() {
return (
<div className={styles.wrapper}>
This page displays all past errors and warnings. Once dismissed, they cannot be retrieved.
<Alert
severity="info"
title="This page displays past errors and warnings. Once dismissed, they cannot be retrieved."
/>
<div className={styles.topRow}>
<Button
variant="destructive"
onClick={selectedNotificationIds.length === 0 ? clearAllNotifs : clearSelectedNotifications}
className={styles.clearAll}
>
{selectedNotificationIds.length === 0 ? 'Clear all notifications' : 'Clear selected notifications'}
<Checkbox
value={allNotificationsSelected}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => handleAllCheckboxToggle(event.target.checked)}
/>
<Button disabled={selectedNotificationIds.length === 0} onClick={clearSelectedNotifications}>
Dismiss notifications
</Button>
</div>
<ul className={styles.list}>
{notifications.map((notif) => (
<li key={notif.id} className={styles.listItem}>
<Checkbox
value={selectedNotificationIds.includes(notif.id)}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
handleCheckboxToggle(notif.id, event.target.checked)
}
/>
<StoredNotificationItem
className={cx(styles.notification, { [styles.newItem]: notif.timestamp > lastReadTimestamp.current })}
className={cx({ [styles.newItem]: notif.timestamp > lastReadTimestamp.current })}
isSelected={selectedNotificationIds.includes(notif.id)}
onClick={() => handleCheckboxToggle(notif.id)}
severity={notif.severity}
title={notif.title}
timestamp={notif.timestamp}
@ -97,25 +103,11 @@ function getStyles(theme: GrafanaTheme2) {
topRow: css({
alignItems: 'center',
display: 'flex',
justifyContent: 'flex-end',
}),
smallText: css({
fontSize: theme.typography.pxToRem(10),
color: theme.colors.text.secondary,
}),
side: css({
display: 'flex',
flexDirection: 'column',
padding: '3px 6px',
paddingTop: theme.spacing(1),
alignItems: 'flex-end',
justifyContent: 'space-between',
flexShrink: 0,
gap: theme.spacing(2),
}),
list: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
}),
listItem: css({
alignItems: 'center',
@ -142,17 +134,10 @@ function getStyles(theme: GrafanaTheme2) {
alignItems: 'center',
gap: theme.spacing(1),
}),
notification: css({
flex: 1,
position: 'relative',
}),
wrapper: css({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
}),
clearAll: css({
alignSelf: 'flex-end',
}),
};
}