diff --git a/public/app/core/components/AppNotifications/StoredNotificationItem.tsx b/public/app/core/components/AppNotifications/StoredNotificationItem.tsx index 57b612db7ae..034388c850a 100644 --- a/public/app/core/components/AppNotifications/StoredNotificationItem.tsx +++ b/public/app/core/components/AppNotifications/StoredNotificationItem.tsx @@ -1,46 +1,43 @@ -import { css } from '@emotion/css'; +import { css, cx } 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, IconButton, IconName, useTheme2 } from '@grafana/ui'; +import { Icon, IconName, useTheme2 } from '@grafana/ui'; import { getIconFromSeverity } from '@grafana/ui/src/components/Alert/Alert'; export type AlertVariant = 'success' | 'warning' | 'error' | 'info'; export interface Props { + className?: string; title: string; severity?: AlertVariant; timestamp?: number; traceId?: string; children?: ReactNode; - onRemove?: (event: React.MouseEvent) => void; } export const StoredNotificationItem = ({ + className, title, severity = 'error', traceId, timestamp, children, - onRemove, }: Props) => { const theme = useTheme2(); const styles = getStyles(theme, severity); const showTraceId = config.featureToggles.tracing && traceId; return ( -
+
{title}
{children}
{showTraceId && `Trace ID: ${traceId}`} -
- -
{timestamp && {formatDistanceToNow(timestamp, { addSuffix: true })}}
); @@ -78,6 +75,7 @@ const getStyles = (theme: GrafanaTheme2, severity: AlertVariant) => { alignSelf: 'center', fontWeight: theme.typography.fontWeightMedium, color: theme.colors.text.primary, + paddingTop: theme.spacing(1), }), body: css({ gridArea: 'body', @@ -96,13 +94,6 @@ const getStyles = (theme: GrafanaTheme2, severity: AlertVariant) => { fontSize: theme.typography.pxToRem(10), color: theme.colors.text.secondary, }), - close: css({ - gridArea: 'close', - display: 'flex', - justifySelf: 'end', - padding: theme.spacing(1, 0.5), - background: 'none', - }), timestamp: css({ gridArea: 'timestamp', alignSelf: 'end', diff --git a/public/app/features/notifications/StoredNotifications.tsx b/public/app/features/notifications/StoredNotifications.tsx index 1a6e3ef8008..b434e47a70d 100644 --- a/public/app/features/notifications/StoredNotifications.tsx +++ b/public/app/features/notifications/StoredNotifications.tsx @@ -1,9 +1,9 @@ import { css, cx } from '@emotion/css'; -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import { useEffectOnce } from 'react-use'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Icon, useStyles2 } from '@grafana/ui'; +import { Button, Checkbox, Icon, useStyles2 } from '@grafana/ui'; import { StoredNotificationItem } from 'app/core/components/AppNotifications/StoredNotificationItem'; import { clearAllNotifications, @@ -17,6 +17,7 @@ import { useDispatch, useSelector } from 'app/types'; export function StoredNotifications() { const dispatch = useDispatch(); const notifications = useSelector((state) => selectWarningsAndErrors(state.appNotifications)); + const [selectedNotificationIds, setSelectedNotificationIds] = useState([]); const lastReadTimestamp = useRef(useSelector((state) => selectLastReadTimestamp(state.appNotifications))); const styles = useStyles2(getStyles); @@ -24,14 +25,27 @@ export function StoredNotifications() { dispatch(readAllNotifications(Date.now())); }); - const onClearNotification = (id: string) => { - dispatch(clearNotification(id)); + const clearSelectedNotifications = () => { + selectedNotificationIds.forEach((id) => { + dispatch(clearNotification(id)); + }); + setSelectedNotificationIds([]); }; const clearAllNotifs = () => { dispatch(clearAllNotifications()); }; + const handleCheckboxToggle = (id: string, isChecked: boolean) => { + setSelectedNotificationIds((prevState) => { + if (isChecked && !prevState.includes(id)) { + return [...prevState, id]; + } else { + return prevState.filter((notificationId) => notificationId !== id); + } + }); + }; + if (notifications.length === 0) { return (
@@ -44,19 +58,28 @@ export function StoredNotifications() { return (
This page displays all past errors and warnings. Once dismissed, they cannot be retrieved. - +
+ +
    {notifications.map((notif) => ( -
  • lastReadTimestamp.current })} - > +
  • + ) => + handleCheckboxToggle(notif.id, event.target.checked) + } + /> lastReadTimestamp.current })} severity={notif.severity} title={notif.title} - onRemove={() => onClearNotification(notif.id)} timestamp={notif.timestamp} traceId={notif.traceId} > @@ -71,6 +94,11 @@ export function StoredNotifications() { function getStyles(theme: GrafanaTheme2) { return { + topRow: css({ + alignItems: 'center', + display: 'flex', + justifyContent: 'flex-end', + }), smallText: css({ fontSize: theme.typography.pxToRem(10), color: theme.colors.text.secondary, @@ -90,9 +118,10 @@ function getStyles(theme: GrafanaTheme2) { gap: theme.spacing(1), }), listItem: css({ - listStyle: 'none', - gap: theme.spacing(1), alignItems: 'center', + display: 'flex', + gap: theme.spacing(2), + listStyle: 'none', position: 'relative', }), newItem: css({ @@ -113,6 +142,10 @@ function getStyles(theme: GrafanaTheme2) { alignItems: 'center', gap: theme.spacing(1), }), + notification: css({ + flex: 1, + position: 'relative', + }), wrapper: css({ display: 'flex', flexDirection: 'column',