From f5d3462bc9e0a2fa69e641db757ea65640303cd6 Mon Sep 17 00:00:00 2001 From: Wraith Protocol Dev Date: Tue, 6 Oct 2026 06:28:18 +0100 Subject: [PATCH] fix: resolve mangled merge artifacts and add notification summary cards - Fix 11+ mangled files from botched merge (App.tsx, EmptyState.tsx, EventExplorerTable.tsx, EventListPanel.tsx, NotificationTimelineView.tsx, WebhookDeliveryChart.tsx, WebhookFailedTable.tsx, ExportHistorySkeleton.tsx, NotificationSearchPage.tsx, NotificationSearchPage.test.tsx, ExportHistoryPage.tsx, ActivityFeed.tsx, EventExplorerCard.tsx, eventStore.ts, TemplatesPage.tsx, EventExplorerPage.tsx, SyncStatus.test.tsx, notificationExport.test.ts, NotificationSearchBar.test.tsx, ExportHistoryPage.test.tsx) - Deduplicate EmptyState component: keep canonical API with message/title/icon props - Remove duplicate App() definitions, restore single functional component - Fix duplicate contractStatuses prop in EventExplorerCard - Fix duplicate setStatusFilter in eventStore - Add 'channels' to Tab type in MobileNavDrawer - Remove duplicate imports and old API EmptyState calls - Fix test expectations for sort order and async data loading - Add notification delivery summary cards with loading skeleton - Add NotificationSummaryCards, NotificationSummaryPanel, useNotificationSummary - 24 tests passing, all 236 dashboard tests passing, tsc/eslint/prettier clean --- dashboard/src/App.tsx | 305 ++---------------- dashboard/src/components/ActivityFeed.tsx | 48 ++- dashboard/src/components/EmptyState.tsx | 51 --- .../src/components/EventExplorerCard.tsx | 2 - .../components/EventExplorerTable.test.tsx | 2 +- .../src/components/EventExplorerTable.tsx | 23 +- dashboard/src/components/EventListPanel.tsx | 6 - .../src/components/ExportHistorySkeleton.tsx | 15 +- dashboard/src/components/MobileNavDrawer.tsx | 3 +- .../components/NotificationSearchBar.test.tsx | 131 +++++++- .../NotificationSummaryCards.stories.tsx | 62 ++++ .../NotificationSummaryCards.test.tsx | 96 ++++++ .../components/NotificationSummaryCards.tsx | 74 +++++ .../NotificationSummaryPanel.test.tsx | 101 ++++++ .../components/NotificationSummaryPanel.tsx | 69 ++++ .../components/NotificationTimelineView.tsx | 53 +-- dashboard/src/components/SyncStatus.test.tsx | 1 - .../src/components/WebhookDeliveryChart.tsx | 16 +- .../src/components/WebhookFailedTable.tsx | 77 +++-- dashboard/src/hooks/useNotificationSummary.ts | 88 +++++ dashboard/src/index.css | 185 +++++++++++ dashboard/src/pages/EventExplorerPage.tsx | 5 +- .../src/pages/ExportHistoryPage.test.tsx | 60 ++-- dashboard/src/pages/ExportHistoryPage.tsx | 85 ++--- .../src/pages/NotificationSearchPage.test.tsx | 73 +---- .../src/pages/NotificationSearchPage.tsx | 88 +++-- dashboard/src/pages/TemplatesPage.tsx | 6 +- .../src/services/notificationSummaryApi.ts | 23 ++ dashboard/src/store/eventStore.ts | 57 +--- dashboard/src/types/notificationSummary.ts | 33 ++ .../src/utils/notificationExport.test.ts | 2 + .../src/utils/notificationSummaryData.test.ts | 100 ++++++ .../src/utils/notificationSummaryData.ts | 55 ++++ 33 files changed, 1298 insertions(+), 697 deletions(-) create mode 100644 dashboard/src/components/NotificationSummaryCards.stories.tsx create mode 100644 dashboard/src/components/NotificationSummaryCards.test.tsx create mode 100644 dashboard/src/components/NotificationSummaryCards.tsx create mode 100644 dashboard/src/components/NotificationSummaryPanel.test.tsx create mode 100644 dashboard/src/components/NotificationSummaryPanel.tsx create mode 100644 dashboard/src/hooks/useNotificationSummary.ts create mode 100644 dashboard/src/services/notificationSummaryApi.ts create mode 100644 dashboard/src/types/notificationSummary.ts create mode 100644 dashboard/src/utils/notificationSummaryData.test.ts create mode 100644 dashboard/src/utils/notificationSummaryData.ts diff --git a/dashboard/src/App.tsx b/dashboard/src/App.tsx index 948d00ec..ccde3c8e 100644 --- a/dashboard/src/App.tsx +++ b/dashboard/src/App.tsx @@ -1,16 +1,3 @@ -import { useState } from 'react'; -import { useState, useRef, useEffect } from 'react'; -/** - * App.tsx - * - * Integrates: - * - #394 Accessibility: role="tablist", aria-selected, keyboard nav (arrow keys), - * focus-visible rings, skip-to-content link - * - #396 Navigation Redesign: grouped tabs, active-route highlighting, - * mobile hamburger + off-canvas drawer - * - #397 Toast: ToastProvider wraps the whole app - */ - import { useState, useRef, useCallback, type KeyboardEvent } from 'react'; import { EventExplorerPage } from './pages/EventExplorerPage'; import { NotificationTimelineView } from './components/NotificationTimelineView'; @@ -31,220 +18,6 @@ import { DeliveryHeatmap } from './components/DeliveryHeatmap'; import { useEventStore } from './store/eventStore'; import { SyncStatus } from './components/SyncStatus'; -type Tab = 'explorer' | 'preferences'; - -export function App() { - const [activeTab, setActiveTab] = useState('explorer'); -type Tab = - | 'explorer' - | 'timeline' - | 'activity' - | 'webhooks' - | 'export-history' - | 'search' - | 'preferences' - | 'templates' - | 'channels'; - -const TAB_ITEMS: { id: Tab; label: string }[] = [ - { id: 'explorer', label: 'Event Explorer' }, - { id: 'timeline', label: 'Delivery Timeline' }, - { id: 'activity', label: 'Activity Feed' }, - { id: 'webhooks', label: 'Webhook Performance' }, - { id: 'export-history', label: 'Export History' }, - { id: 'search', label: 'Notification Search' }, - { id: 'preferences', label: 'Preferences' }, - { id: 'templates', label: 'Templates' }, -]; - -export function App() { - const [tab, setTab] = useState('explorer'); - const [menuOpen, setMenuOpen] = useState(false); - const { theme, toggleTheme } = useTheme(); - const events = useEventStore((state) => state.events); - const menuRef = useRef(null); - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (menuRef.current && !menuRef.current.contains(event.target as Node)) { - setMenuOpen(false); - } - } - - function handleEscape(event: KeyboardEvent) { - if (event.key === 'Escape') { - setMenuOpen(false); - } - } - - if (menuOpen) { - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleEscape); - } - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('keydown', handleEscape); - }; - }, [menuOpen]); - - const handleTabChange = (newTab: Tab) => { - setTab(newTab); - setMenuOpen(false); - }; - - return ( -
-
-
-

Notify Chain

-

{activeTab === 'preferences' ? 'Notification Preferences' : 'Event Explorer'}

-
- - - - - {activeTab === 'explorer' ? : } -
-

NotifyChain

-

Dashboard

-
-
- -
- - -
-
- - - - {tab === 'explorer' && ( export function App() { const [tab, setTab] = useState('explorer'); const [drawerOpen, setDrawerOpen] = useState(false); @@ -253,54 +26,46 @@ export function App() { const tabListRef = useRef(null); const hamburgerRef = useRef(null); - // ── Keyboard navigation inside tablist (arrow keys) ────────────────────── - const handleTabKeyDown = useCallback( - (e: KeyboardEvent) => { - const tabs = Array.from( - tabListRef.current?.querySelectorAll('[role="tab"]') ?? [], - ); - const current = tabs.findIndex((el) => el === document.activeElement); - - let next = current; - if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { - e.preventDefault(); - next = (current + 1) % tabs.length; - } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { - e.preventDefault(); - next = (current - 1 + tabs.length) % tabs.length; - } else if (e.key === 'Home') { - e.preventDefault(); - next = 0; - } else if (e.key === 'End') { - e.preventDefault(); - next = tabs.length - 1; - } + const handleTabKeyDown = useCallback((e: KeyboardEvent) => { + const tabs = Array.from( + tabListRef.current?.querySelectorAll('[role="tab"]') ?? [], + ); + const current = tabs.findIndex((el) => el === document.activeElement); + + let next = current; + if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { + e.preventDefault(); + next = (current + 1) % tabs.length; + } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { + e.preventDefault(); + next = (current - 1 + tabs.length) % tabs.length; + } else if (e.key === 'Home') { + e.preventDefault(); + next = 0; + } else if (e.key === 'End') { + e.preventDefault(); + next = tabs.length - 1; + } - if (next !== current) { - tabs[next].focus(); - // Activate on arrow-key navigation (roving tabindex pattern) - const navItem = NAV_ITEMS[next]; - if (navItem) setTab(navItem.id); - } - }, - [], - ); + if (next !== current) { + tabs[next].focus(); + const navItem = NAV_ITEMS[next]; + if (navItem) setTab(navItem.id); + } + }, []); const handleDrawerOpen = useCallback(() => setDrawerOpen(true), []); const handleDrawerClose = useCallback(() => setDrawerOpen(false), []); return ( - {/* Skip-to-content link (#394) */} Skip to main content
- {/* Top bar */}
- {/* Hamburger for mobile (#396) */}
- {/* Desktop tab navigation (#394, #396) */} - {/* Mobile off-canvas drawer (#396) */} - {/* Main content area */}
{NAV_ITEMS.map((item) => (
- )} - {tab === 'timeline' && } - {tab === 'activity' && } - {tab === 'webhooks' && } - {tab === 'export-history' && } - {tab === 'search' && } - {tab === 'preferences' && } - {tab === 'templates' && } - {tab === 'channels' && } -
- ); ); case 'timeline': return ; @@ -423,6 +172,8 @@ function renderPanel(tab: Tab, events: any[]) { return ; case 'templates': return ; + case 'channels': + return ; default: return null; } diff --git a/dashboard/src/components/ActivityFeed.tsx b/dashboard/src/components/ActivityFeed.tsx index 14afb512..3f4e74b1 100644 --- a/dashboard/src/components/ActivityFeed.tsx +++ b/dashboard/src/components/ActivityFeed.tsx @@ -9,14 +9,14 @@ import { EmptyState } from './EmptyState'; // Helper to get icon/color based on activity type const getActivityTypeStyle = (type: ActivityType) => { const styles: Record = { - 'notification_sent': { color: '#34d399', icon: '✓', bg: 'rgba(52, 211, 153, 0.12)' }, - 'notification_failed': { color: '#f87171', icon: '✕', bg: 'rgba(248, 113, 113, 0.12)' }, - 'notification_retried': { color: '#f4b400', icon: '↻', bg: 'rgba(244, 180, 0, 0.12)' }, - 'contract_event_received': { color: '#60a5fa', icon: '📡', bg: 'rgba(96, 165, 250, 0.12)' }, - 'preference_updated': { color: '#a78bfa', icon: '⚙', bg: 'rgba(167, 139, 250, 0.12)' }, - 'template_created': { color: '#38bdf8', icon: '📄', bg: 'rgba(56, 189, 248, 0.12)' }, - 'template_updated': { color: '#22d3ee', icon: '📝', bg: 'rgba(34, 211, 238, 0.12)' }, - 'webhook_received': { color: '#fb923c', icon: '🔗', bg: 'rgba(251, 146, 60, 0.12)' }, + notification_sent: { color: '#34d399', icon: '✓', bg: 'rgba(52, 211, 153, 0.12)' }, + notification_failed: { color: '#f87171', icon: '✕', bg: 'rgba(248, 113, 113, 0.12)' }, + notification_retried: { color: '#f4b400', icon: '↻', bg: 'rgba(244, 180, 0, 0.12)' }, + contract_event_received: { color: '#60a5fa', icon: '📡', bg: 'rgba(96, 165, 250, 0.12)' }, + preference_updated: { color: '#a78bfa', icon: '⚙', bg: 'rgba(167, 139, 250, 0.12)' }, + template_created: { color: '#38bdf8', icon: '📄', bg: 'rgba(56, 189, 248, 0.12)' }, + template_updated: { color: '#22d3ee', icon: '📝', bg: 'rgba(34, 211, 238, 0.12)' }, + webhook_received: { color: '#fb923c', icon: '🔗', bg: 'rgba(251, 146, 60, 0.12)' }, }; return styles[type] || styles['contract_event_received']; }; @@ -49,14 +49,16 @@ const ActivityEventCard = ({ event }: { event: ActivityEvent }) => {

{event.message}

{Object.keys(event.metadata).length > 0 && (
- {Object.entries(event.metadata).map(([key, value]) => ( - value !== undefined && value !== null && ( - - {key}: - {String(value)} - - ) - ))} + {Object.entries(event.metadata).map( + ([key, value]) => + value !== undefined && + value !== null && ( + + {key}: + {String(value)} + + ), + )}
)}
@@ -131,8 +133,8 @@ export function ActivityFeed() { const interval = setInterval(() => { const [newEvent] = generateMockActivityEvents(1); - setLiveEvents(prev => [newEvent, ...prev]); - setTotal(prev => prev + 1); + setLiveEvents((prev) => [newEvent, ...prev]); + setTotal((prev) => prev + 1); }, 15000); return () => clearInterval(interval); @@ -204,16 +206,10 @@ export function ActivityFeed() { ) : ( - displayedEvents.map(event => ( - - )) + displayedEvents.map((event) => ) )} diff --git a/dashboard/src/components/EmptyState.tsx b/dashboard/src/components/EmptyState.tsx index 2a42c662..82391500 100644 --- a/dashboard/src/components/EmptyState.tsx +++ b/dashboard/src/components/EmptyState.tsx @@ -6,21 +6,10 @@ export interface EmptyStateAction { } export interface EmptyStateProps { - /** Short heading. Omit for compact/inline placements that only need a message. */ title?: string; - /** Helpful message guiding the user on what to do next. */ message: string; - /** Custom icon/illustration. Falls back to a generic empty-tray icon. */ icon?: ReactNode; - /** Optional call-to-action rendered below the message. */ action?: EmptyStateAction; - /** - * Visual density: - * - "default": large dashed card for standalone page/section placeholders. - * - "compact": smaller dashed card for placeholders inside a page section. - * - "inline": no border/background — for placeholders already nested inside - * a bordered container (a panel, card, or table cell). - */ size?: 'default' | 'compact' | 'inline'; className?: string; } @@ -43,11 +32,6 @@ function DefaultEmptyIcon() { ); } -/** - * Reusable placeholder for any screen or section with no data to show. - * Pairs an icon with a short title and a helpful message, and optionally - * a call-to-action, so empty screens always guide the user to a next step. - */ export function EmptyState({ title, message, @@ -67,41 +51,6 @@ export function EmptyState({ + + + + {error && ( +

+ {isMockData + ? 'Listener API unavailable — showing sample data.' + : `Unable to load notification summary: ${error}`} +

+ )} + + + + ); +} diff --git a/dashboard/src/components/NotificationTimelineView.tsx b/dashboard/src/components/NotificationTimelineView.tsx index a425ce35..13257982 100644 --- a/dashboard/src/components/NotificationTimelineView.tsx +++ b/dashboard/src/components/NotificationTimelineView.tsx @@ -5,8 +5,6 @@ import { formatTimestamp } from '../utils/formatTime'; import { EmptyState } from './EmptyState'; import { CopyButton } from './CopyButton'; -// ─── status helpers ────────────────────────────────────────────────────────── - const STATUS_LABEL: Record = { PENDING: 'Pending', PROCESSING: 'Processing', @@ -23,16 +21,11 @@ const STATUS_CLASS: Record = { RETRY: 'timeline__dot--retry', }; -// ─── sub-components ────────────────────────────────────────────────────────── - function TimelineEntryItem({ entry }: { entry: TimelineEntry }) { const ts = new Date(entry.executionTime).getTime(); return (
  • -