From 226f9b7fab3c887ab7c4650066596c3f7a0e10fd Mon Sep 17 00:00:00 2001 From: whiteghost0001 Date: Sun, 30 Aug 2026 23:57:45 +0100 Subject: [PATCH] feat: add raw payload viewer, payload copy action, and centralized event type presentation mapping Resolves Issues #609, #610, and #611 simultaneously: 1. Add Raw Event Payload Viewer (closes #609): - Introduced raw JSON payload viewer mode in NotificationDetailsDrawer with a view toggle (Standard View vs Raw JSON View). - Formatted JSON payloads for maximum readability with indentation via formatRawPayload helper. - Wrapped JSON parsing in safe try/catch fallbacks so invalid payloads never crash the UI. - Automatically redacts sensitive keys (apiKey, secret, password, privateKey, token, auth, credentials, etc.) to prevent exposing sensitive configuration values. 2. Add Event Payload Copy Action (closes #610): - Implemented dedicated "Copy Payload" action in event detail interface and NotificationDetailsDrawer. - Formats payloads as valid JSON prior to clipboard copying where possible. - Provides user feedback via toast notifications ("Payload copied as valid JSON" / "Payload copied"). - Handles clipboard API rejections and permissions errors gracefully without throwing unhandled exceptions. 3. Add Event Type Presentation Mapping (closes #611): - Created centralized event type presentation utility in dashboard/src/utils/eventTypeMapping.ts mapping event names and types to visual representations (labels, badge styling, icons, colors, and category descriptions). - Refactored EventCard, EventExplorerCard, and NotificationDetailsDrawer to use the centralized presentation mapping instead of ad-hoc local styling objects. - Added safe fallback representations (UNKNOWN_EVENT_TYPE_PRESENTATION) for unmapped or custom event types. Closes #609 Closes #610 Closes #611 --- dashboard/src/components/EventCard.tsx | 16 +- .../src/components/EventExplorerCard.tsx | 20 +- .../components/NotificationDetailsDrawer.tsx | 72 +++- dashboard/src/utils/eventTypeMapping.ts | 317 ++++++++++++++++++ dashboard/src/utils/payloadViewer.ts | 147 ++++++++ 5 files changed, 530 insertions(+), 42 deletions(-) create mode 100644 dashboard/src/utils/eventTypeMapping.ts create mode 100644 dashboard/src/utils/payloadViewer.ts diff --git a/dashboard/src/components/EventCard.tsx b/dashboard/src/components/EventCard.tsx index f7cf08e..e6dcd96 100644 --- a/dashboard/src/components/EventCard.tsx +++ b/dashboard/src/components/EventCard.tsx @@ -17,21 +17,7 @@ function shortenAddress(address: string): string { return `${address.slice(0, 6)}...${address.slice(-4)}`; } -const EVENT_TYPE_COLORS: Record = { - TaskCreated: 'event-card__badge--green', - WorkSubmitted: 'event-card__badge--blue', - SubmissionApproved: 'event-card__badge--green', - SubmissionRejected: 'event-card__badge--red', - TaskCancelled: 'event-card__badge--red', - DisputeRaised: 'event-card__badge--yellow', - AutoshareCreated: 'event-card__badge--purple', - Withdrawal: 'event-card__badge--orange', -}; - -function getEventBadgeClass(name: string | null): string { - if (!name) return 'event-card__badge--default'; - return EVENT_TYPE_COLORS[name] ?? 'event-card__badge--default'; -} +import { getEventBadgeClass } from '../utils/eventTypeMapping'; function SkeletonLine({ width = '100%', height = '14px' }: { width?: string; height?: string }) { return ( diff --git a/dashboard/src/components/EventExplorerCard.tsx b/dashboard/src/components/EventExplorerCard.tsx index 930b841..772c2e6 100644 --- a/dashboard/src/components/EventExplorerCard.tsx +++ b/dashboard/src/components/EventExplorerCard.tsx @@ -3,17 +3,7 @@ import type { ContractStatus } from '../services/eventsApi'; import { formatTimestamp } from '../utils/formatTime'; import { CopyButton } from './CopyButton'; -const EVENT_KIND_STYLES: Record = { - contract: 'event-explorer__badge--blue', - system: 'event-explorer__badge--purple', - debug: 'event-explorer__badge--default', -}; - -const EVENT_KIND_LABELS: Record = { - contract: 'Contract', - system: 'System', - debug: 'Debug', -}; +import { getEventKindClass, getEventKindLabel } from '../utils/eventTypeMapping'; function shortenAddress(address: string) { if (address.length <= 14) { @@ -23,14 +13,6 @@ function shortenAddress(address: string) { return `${address.slice(0, 6)}...${address.slice(-4)}`; } -function getEventKindClass(type: string) { - return EVENT_KIND_STYLES[type.toLowerCase()] ?? EVENT_KIND_STYLES.debug; -} - -function getEventKindLabel(type: string) { - return EVENT_KIND_LABELS[type.toLowerCase()] ?? 'Unknown'; -} - interface EventExplorerCardProps { event: BlockchainEvent; onCopyContract: (contractAddress: string) => void; diff --git a/dashboard/src/components/NotificationDetailsDrawer.tsx b/dashboard/src/components/NotificationDetailsDrawer.tsx index eaa7fb2..b721bf0 100644 --- a/dashboard/src/components/NotificationDetailsDrawer.tsx +++ b/dashboard/src/components/NotificationDetailsDrawer.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import type { BlockchainEvent } from '../types/event'; import { formatTimestamp } from '../utils/formatTime'; import { copyTextToClipboard } from '../utils/clipboard'; +import { getEventTypePresentation } from '../utils/eventTypeMapping'; +import { formatRawPayload, copyPayloadToClipboard } from '../utils/payloadViewer'; type FetchState = | { status: 'idle' } @@ -71,6 +73,7 @@ export function NotificationDetailsDrawer({ status: 'idle', }); const [copyMessage, setCopyMessage] = useState(null); + const [isRawView, setIsRawView] = useState(false); const resolvedFetcher = useMemo( () => fetchMetadata ?? (async (e: BlockchainEvent) => defaultMetadata(e)), @@ -80,11 +83,10 @@ export function NotificationDetailsDrawer({ useEffect(() => { if (!isOpen || !notification) { setFetchState({ status: 'idle' }); + setIsRawView(false); return; } - // Fast path: no async metadata provider, keep the drawer snappy and avoid - // unnecessary loading states. if (!fetchMetadata) { setFetchState({ status: 'success', data: defaultMetadata(notification) }); return; @@ -121,7 +123,7 @@ export function NotificationDetailsDrawer({ useEffect(() => { if (!copyMessage) return; - const id = window.setTimeout(() => setCopyMessage(null), 1500); + const id = window.setTimeout(() => setCopyMessage(null), 1800); return () => window.clearTimeout(id); }, [copyMessage]); @@ -130,10 +132,23 @@ export function NotificationDetailsDrawer({ setCopyMessage(ok ? `${label} copied` : `Copy failed`); }, []); + const handleCopyPayload = useCallback(async () => { + if (!notification) return; + const res = await copyPayloadToClipboard(notification.value); + if (res.success) { + setCopyMessage(res.isJson ? 'Payload copied as valid JSON' : 'Payload copied'); + } else { + setCopyMessage('Copy failed'); + } + }, [notification]); + if (!isOpen || !notification) { return null; } + const presentation = getEventTypePresentation(notification.eventName ?? notification.type); + const formattedPayload = formatRawPayload(notification.value); + const sender = fetchState.status === 'success' ? fetchState.data.sender @@ -153,7 +168,10 @@ export function NotificationDetailsDrawer({