jira-ai-fixer/frontend/src/pages/Dashboard.jsx

262 lines
14 KiB
JavaScript

import { useQuery } from '@tanstack/react-query';
import { useAuth } from '../context/AuthContext';
import { issues, reports } from '../services/api';
import { AreaChart, Area, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts';
import { TicketCheck, CheckCircle2, GitPullRequest, Target, TrendingUp, TrendingDown, ArrowUpRight, Clock, AlertCircle, Building2 } from 'lucide-react';
import { cn } from '../lib/utils';
const CHART_COLORS = ['#6366f1', '#22c55e', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4'];
const StatSkeleton = () => (
<div className="stat-card">
<div className="flex items-center justify-between">
<div className="space-y-2">
<div className="skeleton h-3 w-20" />
<div className="skeleton h-8 w-16" />
</div>
<div className="skeleton h-10 w-10 rounded-lg" />
</div>
</div>
);
const ChartSkeleton = () => (
<div className="card">
<div className="card-header">
<div className="skeleton h-4 w-32" />
</div>
<div className="card-body">
<div className="skeleton h-56 w-full rounded-lg" />
</div>
</div>
);
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload) return null;
return (
<div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 shadow-xl text-xs">
<p className="text-gray-400 mb-1">{label}</p>
{payload.map((item, i) => (
<p key={i} className="text-white font-medium">
<span className="inline-block w-2 h-2 rounded-full mr-1.5" style={{ backgroundColor: item.color }} />
{item.name}: {item.value}
</p>
))}
</div>
);
};
export default function Dashboard() {
const { currentOrg } = useAuth();
const { data: stats, isLoading: statsLoading } = useQuery({
queryKey: ['issues-stats', currentOrg?.id],
queryFn: () => issues.stats(currentOrg.id),
enabled: !!currentOrg
});
const { data: report, isLoading: reportLoading } = useQuery({
queryKey: ['report-summary', currentOrg?.id],
queryFn: () => reports.summary(currentOrg.id, 14),
enabled: !!currentOrg
});
if (!currentOrg) {
return (
<div className="flex-1 flex items-center justify-center p-8">
<div className="text-center max-w-md">
<div className="w-16 h-16 rounded-2xl bg-indigo-600/10 flex items-center justify-center mx-auto mb-4">
<Building2 size={28} className="text-indigo-400" />
</div>
<h2 className="text-xl font-semibold text-white mb-2">Select an organization</h2>
<p className="text-gray-400 text-sm">Choose an organization from the sidebar to view your dashboard and manage issues.</p>
</div>
</div>
);
}
const s = stats?.data || {};
const r = report?.data || {};
const loading = statsLoading || reportLoading;
const statCards = [
{ label: 'Total Issues', value: s.total || 0, icon: TicketCheck, color: 'text-blue-400', bg: 'bg-blue-500/10', trend: '+12%', up: true },
{ label: 'Analyzed', value: s.analyzed || 0, icon: CheckCircle2, color: 'text-emerald-400', bg: 'bg-emerald-500/10', trend: '+8%', up: true },
{ label: 'PRs Created', value: s.pr_created || 0, icon: GitPullRequest, color: 'text-purple-400', bg: 'bg-purple-500/10', trend: '+15%', up: true },
{ label: 'Avg Confidence', value: s.avg_confidence ? `${(s.avg_confidence * 100).toFixed(0)}%` : 'N/A', icon: Target, color: 'text-amber-400', bg: 'bg-amber-500/10', trend: '+3%', up: true },
];
const statusData = [
{ name: 'Pending', value: s.pending || 0 },
{ name: 'Analyzing', value: s.analyzing || 0 },
{ name: 'Analyzed', value: s.analyzed || 0 },
{ name: 'PR Created', value: s.pr_created || 0 },
{ name: 'Error', value: s.error || 0 }
].filter(d => d.value > 0);
const sourceData = Object.entries(s.by_source || {}).map(([name, value]) => ({
name: name.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()),
value
}));
return (
<div className="p-6 animate-fade-in">
<div className="page-header">
<div>
<h1 className="page-title">Dashboard</h1>
<p className="page-subtitle">Overview of your issue analysis pipeline</p>
</div>
<div className="flex items-center gap-2">
<span className="badge badge-green">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
System operational
</span>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{loading ? (
Array(4).fill(0).map((_, i) => <StatSkeleton key={i} />)
) : (
statCards.map(stat => {
const Icon = stat.icon;
return (
<div key={stat.label} className="stat-card">
<div className="flex items-center justify-between relative z-10">
<div>
<p className="text-xs font-medium text-gray-400 uppercase tracking-wide">{stat.label}</p>
<p className="text-2xl font-bold text-white mt-1">{stat.value}</p>
<div className={cn("flex items-center gap-1 mt-1.5 text-xs font-medium", stat.up ? "text-emerald-400" : "text-red-400")}>
{stat.up ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
{stat.trend}
<span className="text-gray-500 font-normal ml-0.5">vs last week</span>
</div>
</div>
<div className={cn("w-11 h-11 rounded-xl flex items-center justify-center", stat.bg)}>
<Icon size={20} className={stat.color} />
</div>
</div>
</div>
);
})
)}
</div>
{/* Charts row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
{loading ? (
<>
<ChartSkeleton />
<ChartSkeleton />
</>
) : (
<>
{/* Trend chart */}
<div className="card">
<div className="card-header">
<h3 className="text-sm font-semibold">Issues Trend</h3>
<span className="badge badge-gray text-[10px]">Last 14 days</span>
</div>
<div className="card-body">
<div className="h-56">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={r.daily_breakdown || []}>
<defs>
<linearGradient id="colorTotal" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#6366f1" stopOpacity={0.3} />
<stop offset="95%" stopColor="#6366f1" stopOpacity={0} />
</linearGradient>
<linearGradient id="colorAnalyzed" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.3} />
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e1e2a" />
<XAxis dataKey="date" tick={{ fill: '#5a5a70', fontSize: 11 }} tickLine={false} axisLine={false} />
<YAxis tick={{ fill: '#5a5a70', fontSize: 11 }} tickLine={false} axisLine={false} />
<Tooltip content={<CustomTooltip />} />
<Area type="monotone" dataKey="total" stroke="#6366f1" fill="url(#colorTotal)" strokeWidth={2} name="Total" />
<Area type="monotone" dataKey="analyzed" stroke="#22c55e" fill="url(#colorAnalyzed)" strokeWidth={2} name="Analyzed" />
</AreaChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Status distribution */}
<div className="card">
<div className="card-header">
<h3 className="text-sm font-semibold">Status Distribution</h3>
</div>
<div className="card-body">
<div className="h-56 flex items-center">
{statusData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={statusData}
cx="50%"
cy="50%"
innerRadius={55}
outerRadius={80}
paddingAngle={4}
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{statusData.map((entry, index) => (
<Cell key={entry.name} fill={CHART_COLORS[index % CHART_COLORS.length]} />
))}
</Pie>
<Tooltip content={<CustomTooltip />} />
</PieChart>
</ResponsiveContainer>
) : (
<div className="w-full text-center">
<AlertCircle size={24} className="text-gray-600 mx-auto mb-2" />
<p className="text-gray-500 text-sm">No data yet</p>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
{/* By source */}
{loading ? (
<ChartSkeleton />
) : (
<div className="card">
<div className="card-header">
<h3 className="text-sm font-semibold">Issues by Source</h3>
</div>
<div className="card-body">
<div className="h-56">
{sourceData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={sourceData} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="#1e1e2a" horizontal={false} />
<XAxis type="number" tick={{ fill: '#5a5a70', fontSize: 11 }} tickLine={false} axisLine={false} />
<YAxis type="category" dataKey="name" tick={{ fill: '#8888a0', fontSize: 12 }} width={100} tickLine={false} axisLine={false} />
<Tooltip content={<CustomTooltip />} />
<Bar dataKey="value" fill="#6366f1" radius={[0, 6, 6, 0]} barSize={24} name="Issues" />
</BarChart>
</ResponsiveContainer>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center">
<AlertCircle size={24} className="text-gray-600 mx-auto mb-2" />
<p className="text-gray-500 text-sm">No data yet</p>
<p className="text-gray-600 text-xs mt-1">Connect an integration to start tracking</p>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}