pulse-dashboard
Version:
A Next.js Dashboard application for real-time monitoring and historical analysis of Playwright test executions, based on playwright-pulse-report. This component provides the UI for visualizing Playwright test results and can be run as a standalone CLI too
238 lines (236 loc) • 11.3 kB
JSX
'use client';
import { useState, useEffect } from 'react';
import Image from 'next/image';
import { useTestData } from '@/hooks/useTestData';
import { SummaryMetrics } from './SummaryMetrics';
import { LiveTestResults } from './LiveTestResults';
import { TrendAnalysis } from './TrendAnalysis';
import { FlakyTestsWidget } from './FlakyTestsWidget';
import { SettingsView } from './SettingsView';
import { FailureCategorizationView } from './FailureCategorizationView';
import { PulseCommandView } from "./PulseCommandView";
import { SidebarProvider, Sidebar, SidebarHeader, SidebarTrigger, SidebarContent, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarInset, SidebarFooter, SidebarSeparator, } from "@/components/ui/sidebar";
import { LayoutDashboard, ListChecks, TrendingUp, Settings, Repeat, ListX, ShieldCheck, Terminal, } from "lucide-react";
import Link from "next/link";
export function PulseDashboard() {
const { currentRun, historicalTrends, loadingCurrent, loadingHistorical, errorCurrent, errorHistorical, } = useTestData();
const [activeView, setActiveView] = useState("dashboard");
const [initialLiveResultsFilter, setInitialLiveResultsFilter] = useState(undefined);
const [linkColor, setLinkColor] = useState("#7737BF"); // For footer link
const handleMetricCardClick = (filter) => {
setInitialLiveResultsFilter(filter);
setActiveView("live-results");
};
useEffect(() => {
if (activeView !== "live-results" && initialLiveResultsFilter) {
setInitialLiveResultsFilter(undefined);
}
}, [activeView, initialLiveResultsFilter]);
const allMenuItemsConfig = [
{
id: "dashboard",
label: "Dashboard",
icon: LayoutDashboard,
description: "Real-time Playwright Test Execution Monitoring & Analysis Overview",
labelColorVar: "--sidebar-tab-dashboard-color-hsl",
},
{
id: "live-results",
label: "Test Results",
icon: ListChecks,
description: "Detailed view of the latest test run results with filters.",
labelColorVar: "--sidebar-tab-results-color-hsl",
},
{
id: "trend-analysis",
label: "Trend Analysis",
icon: TrendingUp,
description: "Historical data visualization for test performance.",
labelColorVar: "--sidebar-tab-trends-color-hsl",
},
{
id: "flaky-tests",
label: "Flaky Tests",
icon: Repeat,
description: "Analysis of historically flaky tests.",
labelColorVar: "--sidebar-tab-flaky-color-hsl",
},
{
id: "failure-categorization",
label: "Failure Categorization",
icon: ListX,
description: "Categorize and view common failure types.",
labelColorVar: "--sidebar-tab-failures-color-hsl",
},
{
id: "pulse-command",
label: "Pulse Command",
icon: Terminal,
description: "Generate various report formats using playwright-pulse-report commands.",
labelColorVar: "--sidebar-tab-command-color-hsl",
},
{
id: "settings",
label: "Settings",
icon: Settings,
description: "Configure dashboard appearance and preferences.",
labelColorVar: "--sidebar-tab-settings-color-hsl",
},
];
const settingsMenuItem = allMenuItemsConfig.find((item) => item.id === "settings");
const mainMenuItems = allMenuItemsConfig.filter((item) => item.id !== "settings");
const activeMenuItem = allMenuItemsConfig.find((item) => item.id === activeView);
let componentToRender;
switch (activeView) {
case "dashboard":
componentToRender = (<SummaryMetrics currentRun={currentRun} loading={loadingCurrent} error={errorCurrent} onMetricClick={handleMetricCardClick}/>);
break;
case "live-results":
componentToRender = (<LiveTestResults report={currentRun} loading={loadingCurrent} error={errorCurrent} initialFilter={initialLiveResultsFilter}/>);
break;
case "trend-analysis":
componentToRender = (<TrendAnalysis trends={historicalTrends} loading={loadingHistorical} error={errorHistorical} currentResults={currentRun?.results}/>);
break;
case "flaky-tests":
componentToRender = <FlakyTestsWidget />;
break;
case "failure-categorization":
componentToRender = <FailureCategorizationView />;
break;
case "pulse-command":
componentToRender = <PulseCommandView />;
break;
case "settings":
componentToRender = <SettingsView />;
break;
default:
componentToRender = (<SummaryMetrics currentRun={currentRun} loading={loadingCurrent} error={errorCurrent} onMetricClick={handleMetricCardClick}/>);
}
return (<SidebarProvider defaultOpen>
<Sidebar collapsible="icon" className="border-r border-sidebar-border shadow-lg">
<SidebarHeader className="p-4 group-data-[collapsible=icon]:p-2 flex items-center justify-between group-data-[collapsible=icon]:justify-center border-b border-sidebar-border">
<Link href="/" className="flex items-center" onClick={() => setActiveView("dashboard")}>
<Image src="https://ocpaxmghzmfbuhxzxzae.supabase.co/storage/v1/object/public/images/pulse-report/playwright_pulse_icon.png" alt="Pulse Dashboard Logo" width={120} // Adjusted width for the new logo
height={30} // Adjusted height for the new logo
style={{ objectFit: "fill" }} // Ensures the logo scales nicely
className="transition-all duration-200" data-ai-hint="pulse logo"/>
<span style={{ marginLeft: "10px", color: "#489ef9", fontWeight: 900 }}>
PULSE DASHBOARD
</span>
</Link>
<SidebarTrigger className="md:hidden"/>
</SidebarHeader>
<SidebarContent>
<SidebarMenu>
{mainMenuItems.map((item) => (<SidebarMenuItem key={item.id}>
<SidebarMenuButton onClick={() => {
setActiveView(item.id);
}} isActive={activeView === item.id} tooltip={{
children: item.label,
side: "right",
align: "center",
}}>
<item.icon className="h-5 w-5"/>
<span style={activeView === item.id
? undefined // Active: inherit color from button's active state
: item.labelColorVar
? { color: `hsl(var(${item.labelColorVar}))` } // Inactive: use labelColorVar
: undefined}>
{item.label}
</span>
</SidebarMenuButton>
</SidebarMenuItem>))}
</SidebarMenu>
</SidebarContent>
<SidebarFooter className="p-2 mt-auto border-t border-sidebar-border">
{settingsMenuItem && (<SidebarMenu className="group-data-[collapsible=icon]:py-2">
<SidebarMenuItem>
<SidebarMenuButton onClick={() => {
setActiveView(settingsMenuItem.id);
}} isActive={activeView === settingsMenuItem.id} tooltip={{
children: settingsMenuItem.label,
side: "right",
align: "center",
}}>
<settingsMenuItem.icon className="h-5 w-5"/>
<span className="group-data-[collapsible=icon]:hidden" style={activeView === settingsMenuItem.id
? undefined // Active: inherit color from button's active state
: settingsMenuItem.labelColorVar
? {
color: `hsl(var(${settingsMenuItem.labelColorVar}))`,
} // Inactive: use labelColorVar
: undefined}>
{settingsMenuItem.label}
</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>)}
<div className="pt-2 group-data-[collapsible=icon]:hidden">
<SidebarSeparator className="my-1 group-data-[collapsible=icon]:hidden"/>
<div className="flex items-center justify-center gap-1.5 text-xs p-2">
<ShieldCheck className="h-3.5 w-3.5 text-sidebar-foreground/70"/>
<span className="font-medium text-sidebar-foreground">Pulse</span>
<span className="text-muted-foreground/80">v1.1.0</span>
</div>
</div>
</SidebarFooter>
</Sidebar>
<SidebarInset>
<div className="container mx-auto p-6 md:p-8 space-y-6 min-h-screen flex flex-col rounded-[2rem] shadow-lg bg-background">
<header className="mb-0">
<h1 className="text-3xl font-bold font-headline text-primary tracking-tight">
{activeMenuItem?.label || "Pulse Dashboard"}
</h1>
<p className="text-md text-muted-foreground mt-1">
{activeMenuItem?.description ||
"Real-time Playwright Test Execution Monitoring & Analysis Overview"}
</p>
<p className="text-sm text-muted-foreground mt-2">
Latest Run Date:{" "}
{loadingCurrent ? (<span className="text-muted-foreground">Loading...</span>) : currentRun?.run?.timestamp ? (<span className="text-muted-foreground font-medium">
{new Date(currentRun.run.timestamp).toLocaleString()}
</span>) : errorCurrent ? (<span className="text-destructive font-medium">
Error loading data
</span>) : (<span className="text-muted-foreground font-medium">
Not available
</span>)}
</p>
</header>
<main className="flex-grow mt-6">{componentToRender}</main>
<footer style={{
padding: "0.5rem",
textAlign: "center",
fontFamily: "'Segoe UI', system-ui, sans-serif",
marginTop: "auto", // Ensures footer is at the bottom
}} className="text-foreground">
<div style={{
display: "inline-flex",
alignItems: "center",
gap: "0.5rem",
fontSize: "0.9rem",
fontWeight: 600,
letterSpacing: "0.5px",
}}>
<span>Created by</span>
<a href="https://github.com/Arghajit47" target="_blank" rel="noopener noreferrer" style={{
color: linkColor,
fontWeight: 700,
fontStyle: "italic",
textDecoration: "none",
transition: "all 0.2s ease",
}} onMouseOver={() => setLinkColor("#BF5C37")} onMouseOut={() => setLinkColor("#7737BF")}>
Arghajit Singha
</a>
</div>
<div style={{
marginTop: "0.5rem",
fontSize: "0.75rem",
}} className="text-muted-foreground">
Crafted with precision
</div>
</footer>
</div>
</SidebarInset>
</SidebarProvider>);
}
//# sourceMappingURL=PulseDashboard.jsx.map