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
152 lines (149 loc) • 8.81 kB
JSX
'use client';
import * as React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Skeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, Legend, ResponsiveContainer, BarChart, Bar } from 'recharts';
import { TrendingUp, Terminal, Info, Users } from 'lucide-react';
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (<div className="custom-recharts-tooltip">
<p className="label">{`Date: ${label}`}</p>
{payload.map((entry, index) => (<p key={`item-${index}`} style={{ color: entry.color }} className="text-xs">
{`${entry.name}: ${entry.value?.toLocaleString()}${entry.unit || ''}`}
</p>))}
</div>);
}
return null;
};
const TrendAnalysisComponent = ({ trends, loading, error }) => {
const outcomesChartRef = React.useRef(null);
const durationChartRef = React.useRef(null);
const workerCountChartRef = React.useRef(null);
const formattedTrends = React.useMemo(() => {
if (!trends || trends.length === 0) {
return [];
}
return trends.map(t => ({
...t,
date: new Date(t.date).toLocaleDateString('en-CA', { month: 'short', day: 'numeric' }),
durationSeconds: parseFloat((t.duration / 1000).toFixed(2)),
workerCount: t.workerCount // workerCount is already a number or undefined
}));
}, [trends]);
if (loading) {
return (<Card className="shadow-xl rounded-xl backdrop-blur-md bg-card/80 border-border/50">
<CardHeader>
<CardTitle className="flex items-center text-2xl font-headline text-primary">
<TrendingUp className="h-7 w-7 mr-2"/>
Historical Trend Analysis
</CardTitle>
</CardHeader>
<CardContent className="space-y-8 p-6">
{[...Array(3)].map((_, i) => (<div key={i} className="bg-muted/30 p-4 rounded-lg shadow-inner">
<Skeleton className="h-6 w-1/3 mb-4 rounded-md bg-muted/50"/>
<Skeleton className="h-64 w-full rounded-md bg-muted/50"/>
</div>))}
</CardContent>
</Card>);
}
if (error) {
return (<Alert variant="destructive" className="mt-4 shadow-md rounded-lg">
<Terminal className="h-4 w-4"/>
<AlertTitle>Error Fetching Historical Trends</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>);
}
if (!trends || trends.length === 0) {
return (<Card className="shadow-xl rounded-xl backdrop-blur-md bg-card/80 border-border/50">
<CardHeader>
<CardTitle className="flex items-center text-2xl font-headline text-primary">
<TrendingUp className="h-7 w-7 mr-2"/>
Historical Trend Analysis
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<Alert className="rounded-lg border-primary/30 bg-primary/5 text-primary">
<Info className="h-5 w-5 text-primary/80"/>
<AlertTitle className="font-semibold">No Historical Data</AlertTitle>
<AlertDescription>
No historical trend data available. Ensure 'trend-*.json' files exist in 'pulse-report/history/' and are correctly formatted.
</AlertDescription>
</Alert>
</CardContent>
</Card>);
}
const workerCountDataAvailable = formattedTrends.some(t => typeof t.workerCount === 'number' && t.workerCount > 0);
return (<Card className="shadow-xl rounded-xl backdrop-blur-md bg-card/80 border-border/50">
<CardHeader>
<CardTitle className="text-2xl font-headline text-primary flex items-center">
<TrendingUp className="h-7 w-7 mr-2"/>
Historical Trend Analysis
</CardTitle>
</CardHeader>
<CardContent className="space-y-10 p-6">
<div>
<div className="flex justify-between items-center mb-4">
<h4 className="text-xl font-semibold text-foreground">Test Outcomes Over Time</h4>
</div>
<div ref={outcomesChartRef} className="w-full h-[350px] bg-muted/30 p-4 rounded-lg shadow-inner">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={formattedTrends} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))"/>
<XAxis dataKey="date" stroke="hsl(var(--muted-foreground))" tick={{ fontSize: 12 }}/>
<YAxis stroke="hsl(var(--muted-foreground))" tick={{ fontSize: 12 }}/>
<RechartsTooltip content={<CustomTooltip />} cursor={{ fill: 'hsl(var(--muted))', fillOpacity: 0.2 }}/>
<Legend wrapperStyle={{ fontSize: "12px", paddingTop: "10px" }}/>
<Line type="monotone" dataKey="totalTests" name="Total Tests" stroke="hsl(var(--chart-2))" strokeWidth={2} dot={{ r: 3 }} activeDot={{ r: 6 }}/>
<Line type="monotone" dataKey="passed" name="Passed" stroke="hsl(var(--chart-3))" strokeWidth={2} dot={{ r: 3 }} activeDot={{ r: 6 }}/>
<Line type="monotone" dataKey="failed" name="Failed" stroke="hsl(var(--chart-4))" strokeWidth={2} dot={{ r: 3 }} activeDot={{ r: 6 }}/>
<Line type="monotone" dataKey="skipped" name="Skipped" stroke="hsl(var(--chart-5))" strokeWidth={2} dot={{ r: 3 }} activeDot={{ r: 6 }}/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-4">
<h4 className="text-xl font-semibold text-foreground">Test Duration Over Time</h4>
</div>
<div ref={durationChartRef} className="w-full h-[350px] bg-muted/30 p-4 rounded-lg shadow-inner">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={formattedTrends} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))"/>
<XAxis dataKey="date" stroke="hsl(var(--muted-foreground))" tick={{ fontSize: 12 }}/>
<YAxis stroke="hsl(var(--muted-foreground))" tick={{ fontSize: 12 }} unit="s" name="Seconds"/>
<RechartsTooltip content={<CustomTooltip />} cursor={{ fill: 'hsl(var(--muted))', fillOpacity: 0.2 }}/>
<Legend wrapperStyle={{ fontSize: "12px", paddingTop: "10px" }}/>
<Bar dataKey="durationSeconds" name="Duration (s)" fill="hsl(var(--chart-1))" radius={[6, 6, 0, 0]} barSize={20}/>
</BarChart>
</ResponsiveContainer>
</div>
</div>
{workerCountDataAvailable && (<div>
<div className="flex justify-between items-center mb-4">
<h4 className="text-xl font-semibold text-foreground flex items-center">
<Users className="h-5 w-5 mr-2 text-primary"/> Active Worker Count Over Time
</h4>
</div>
<div ref={workerCountChartRef} className="w-full h-[350px] bg-muted/30 p-4 rounded-lg shadow-inner">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={formattedTrends.filter(t => typeof t.workerCount === 'number')} margin={{ top: 5, right: 20, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))"/>
<XAxis dataKey="date" stroke="hsl(var(--muted-foreground))" tick={{ fontSize: 12 }}/>
<YAxis stroke="hsl(var(--muted-foreground))" tick={{ fontSize: 12 }} allowDecimals={false} domain={['dataMin', 'dataMax']}/>
<RechartsTooltip content={<CustomTooltip />} cursor={{ fill: 'hsl(var(--muted))', fillOpacity: 0.2 }}/>
<Legend wrapperStyle={{ fontSize: "12px", paddingTop: "10px" }}/>
<Line type="monotone" dataKey="workerCount" name="Active Workers" stroke="hsl(var(--chart-info))" strokeWidth={2} dot={{ r: 3 }} activeDot={{ r: 6 }} connectNulls={false}/>
</LineChart>
</ResponsiveContainer>
</div>
<p className="text-xs text-muted-foreground mt-2">
Note: This chart shows the number of unique worker IDs detected in each historical run. Runs without worker information will not be plotted.
</p>
</div>)}
</CardContent>
</Card>);
};
export const TrendAnalysis = React.memo(TrendAnalysisComponent);
TrendAnalysis.displayName = 'TrendAnalysis';
//# sourceMappingURL=TrendAnalysis.jsx.map