From 99639cf229a631009247c1bf6dc9ef5bda8dad82 Mon Sep 17 00:00:00 2001 From: Tim Levett Date: Mon, 12 Jan 2026 15:42:40 -0600 Subject: [PATCH] Stored Notifications: Add copy button --- .../StoredNotificationItem.tsx | 61 ++++++++++++++++++- .../notifications/StoredNotifications.tsx | 1 + 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/public/app/core/components/AppNotifications/StoredNotificationItem.tsx b/public/app/core/components/AppNotifications/StoredNotificationItem.tsx index c3df7d40596..a4a8fe8e994 100644 --- a/public/app/core/components/AppNotifications/StoredNotificationItem.tsx +++ b/public/app/core/components/AppNotifications/StoredNotificationItem.tsx @@ -1,9 +1,10 @@ import { css } from '@emotion/css'; import { formatDistanceToNow } from 'date-fns'; -import { ReactNode } from 'react'; +import { ReactNode, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Card, Checkbox, useTheme2 } from '@grafana/ui'; +import { t } from '@grafana/i18n'; +import { Card, Checkbox, IconButton, Tooltip, useTheme2 } from '@grafana/ui'; export type AlertVariant = 'success' | 'warning' | 'error' | 'info'; @@ -14,6 +15,7 @@ export interface Props { onClick: () => void; severity?: AlertVariant; title: string; + text?: string; timestamp?: number; traceId?: string; } @@ -25,11 +27,51 @@ export const StoredNotificationItem = ({ onClick, severity = 'error', title, + text, traceId, timestamp, }: Props) => { const theme = useTheme2(); const styles = getStyles(theme); + const [hasCopied, setHasCopied] = useState(false); + + const handleCopy = async (e: React.MouseEvent) => { + e.stopPropagation(); + + const parts: string[] = []; + parts.push(`Title: ${title}`); + if (text) { + parts.push(`Message: ${text}`); + } + if (traceId) { + parts.push(`Trace ID: ${traceId}`); + } + if (timestamp) { + const date = new Date(timestamp); + parts.push(`Timestamp: ${date.toISOString()}`); + } + const textToCopy = parts.join('\n'); + + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(textToCopy); + } else { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = textToCopy; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + } + setHasCopied(true); + setTimeout(() => setHasCopied(false), 2000); + } catch (err) { + console.error('Failed to copy text:', err); + } + }; return ( @@ -42,6 +84,21 @@ export const StoredNotificationItem = ({ {traceId && {`Trace ID: ${traceId}`}} {timestamp && formatDistanceToNow(timestamp, { addSuffix: true })} + + + + + ); }; diff --git a/public/app/features/notifications/StoredNotifications.tsx b/public/app/features/notifications/StoredNotifications.tsx index b5db2d1327a..eb50d281920 100644 --- a/public/app/features/notifications/StoredNotifications.tsx +++ b/public/app/features/notifications/StoredNotifications.tsx @@ -90,6 +90,7 @@ export function StoredNotifications() { onClick={() => handleCheckboxToggle(notif.id)} severity={notif.severity} title={notif.title} + text={notif.text} timestamp={notif.timestamp} traceId={notif.traceId} >