This commit is contained in:
2025-11-27 17:26:53 +03:00
parent 5222cceb81
commit eef82577ab
11 changed files with 333 additions and 160 deletions

View File

@@ -1,40 +1,212 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Line,
LineChart,
CartesianGrid,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Legend
} from "recharts";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { Button } from "../components/ui/button";
import { useLiveCounter } from "../providers/live-provider";
import { useLiveData } from "../providers/live-provider";
import { fetchJobMetrics, JobMetrics } from "../api/jobs";
import { JobStatusBadge } from "../components/JobStatusBadge";
import { RepoIcon } from "../components/RepoIcon";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClockRotateLeft, faListCheck } from "@fortawesome/free-solid-svg-icons";
function formatDuration(ms?: number) {
if (!ms || Number.isNaN(ms)) return "-";
const seconds = Math.round(ms / 1000);
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const rem = seconds % 60;
if (minutes < 60) return `${minutes}dk ${rem}s`;
const hours = Math.floor(minutes / 60);
return `${hours}sa ${minutes % 60}dk`;
}
export function HomePage() {
const { value, running, startCounter, stopCounter } = useLiveCounter();
const [metrics, setMetrics] = useState<JobMetrics | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { jobStreams } = useLiveData();
const navigate = useNavigate();
useEffect(() => {
fetchJobMetrics()
.then(setMetrics)
.catch(() => setError("Metrikler alınamadı"))
.finally(() => setLoading(false));
}, []);
const chartData = useMemo(() => {
if (!metrics) return [];
return metrics.dailyStats.map((d) => ({
date: d._id,
Başarılı: d.success,
Hatalı: d.failed
}));
}, [metrics]);
const mergedRuns = useMemo(() => {
if (!metrics) return [];
return metrics.recentRuns.map((run, idx) => {
const live = jobStreams[run.job._id];
// Sadece en güncel run (liste başı) canlı güncellemeleri alır; geçmiş kayıtlar sabit kalır.
if (idx === 0 && live?.status) {
return {
...run,
status: live.status,
finishedAt: live.lastRunAt || run.finishedAt,
durationMs: live.lastDurationMs ?? run.durationMs
};
}
return run;
});
}, [metrics, jobStreams]);
const lastRun = mergedRuns[0];
const lastRunDuration = formatDuration(lastRun?.durationMs);
return (
<Card className="border-border card-shadow">
<CardHeader>
<CardTitle>Canlı Sayaç</CardTitle>
<CardDescription>
Sayaç sunucu tarafından çalışır; başlattıktan sonra diğer sayfalardan anlık izleyebilirsiniz.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-md border border-border bg-muted/40 px-4 py-3">
<div>
<div className="text-sm text-muted-foreground">Durum</div>
<div className="text-lg font-semibold text-foreground">
{running ? "Çalışıyor" : "Beklemede"}
<div className="grid gap-6">
<div className="grid gap-4">
<Card className="border-border card-shadow">
<CardHeader className="space-y-1">
<CardTitle>Son Çalıştırma</CardTitle>
<CardDescription>Son 10 çalıştırmanın en günceli</CardDescription>
<div className="text-xs text-muted-foreground">
Başarı oranı:{" "}
<span className="font-semibold text-foreground">{metrics?.totals.successRate ?? 0}%</span>
</div>
</CardHeader>
<CardContent>
{loading && <div className="text-sm text-muted-foreground">Yükleniyor...</div>}
{error && <div className="text-sm text-destructive">{error}</div>}
{!loading && lastRun && (
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-3">
<RepoIcon repoUrl={lastRun.job.repoUrl} />
<div>
<div className="text-base font-semibold text-foreground">{lastRun.job.name}</div>
<div className="text-xs text-muted-foreground">
{new Date(lastRun.startedAt).toLocaleString()}
</div>
</div>
</div>
<div className="flex items-center gap-3">
<JobStatusBadge status={lastRun.status} />
<div className="text-xs text-muted-foreground">
Süre: <span className="font-semibold text-foreground">{lastRunDuration}</span>
</div>
</div>
</div>
)}
{!loading && !lastRun && <div className="text-sm text-muted-foreground">Henüz kayıt yok.</div>}
</CardContent>
</Card>
</div>
<div className="grid gap-4 lg:grid-cols-3">
<Card className="border-border card-shadow lg:col-span-2">
<CardHeader className="flex items-center justify-between">
<div>
<CardTitle>Son 7 Gün Çalıştırma Trendleri</CardTitle>
<CardDescription>Başarılı / Hatalı job sayıları</CardDescription>
</div>
<div className="text-xs text-muted-foreground flex items-center gap-2">
<FontAwesomeIcon icon={faClockRotateLeft} className="h-3.5 w-3.5" />
{metrics?.totals.totalRuns ?? 0} toplam koşu
</div>
</CardHeader>
<CardContent className="h-80">
{chartData.length === 0 ? (
<div className="text-sm text-muted-foreground">Grafik verisi yok.</div>
) : (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
<XAxis dataKey="date" />
<YAxis allowDecimals={false} />
<Tooltip />
<Legend />
<Line type="monotone" dataKey="Başarılı" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="Hatalı" stroke="#ef4444" strokeWidth={2} />
</LineChart>
</ResponsiveContainer>
)}
</CardContent>
</Card>
<Card className="border-border card-shadow">
<CardHeader>
<CardTitle>Hızlı Metrikler</CardTitle>
<CardDescription>Özet görünüm</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm text-muted-foreground">
<div className="flex items-center justify-between">
<span>Başarı Oranı</span>
<span className="text-lg font-semibold text-foreground">
{metrics?.totals.successRate ?? 0}%
</span>
</div>
<div className="flex items-center justify-between">
<span>Toplam Çalıştırma</span>
<span className="text-lg font-semibold text-foreground">
{metrics?.totals.totalRuns ?? 0}
</span>
</div>
<div className="flex items-center justify-between">
<span>Son Süre</span>
<span className="text-lg font-semibold text-foreground">{lastRunDuration}</span>
</div>
</CardContent>
</Card>
</div>
<Card className="border-border card-shadow">
<CardHeader className="flex items-center justify-between">
<div>
<CardTitle>Etkinlik Akışı</CardTitle>
<CardDescription>Son 10 job çalıştırması</CardDescription>
</div>
<div className="text-right">
<div className="text-sm text-muted-foreground">Değer</div>
<div className="text-3xl font-bold text-foreground">{value}</div>
<div className="text-xs text-muted-foreground flex items-center gap-1">
<FontAwesomeIcon icon={faListCheck} className="h-3.5 w-3.5" />
{mergedRuns.length ?? 0} kayıt
</div>
</div>
<div className="flex gap-3">
<Button className="flex-1" onClick={startCounter} disabled={running}>
Sayaçı Başlat
</Button>
<Button className="flex-1" variant="outline" onClick={stopCounter} disabled={!running}>
Durdur
</Button>
</div>
</CardContent>
</Card>
</CardHeader>
<CardContent className="space-y-3">
{loading && <div className="text-sm text-muted-foreground">Yükleniyor...</div>}
{error && <div className="text-sm text-destructive">{error}</div>}
{!loading && mergedRuns.length === 0 && (
<div className="text-sm text-muted-foreground">Henüz çalıştırma yok.</div>
)}
{!loading &&
mergedRuns.map((run) => (
<button
key={run._id}
type="button"
onClick={() => navigate(`/jobs/${run.job._id}`)}
className="flex w-full items-center justify-between rounded-md border border-border bg-muted/40 px-3 py-2 text-left transition hover:bg-muted"
>
<div className="flex items-center gap-3">
<RepoIcon repoUrl={run.job.repoUrl} />
<div>
<div className="text-sm font-semibold text-foreground">{run.job.name}</div>
<div className="text-xs text-muted-foreground">
{new Date(run.startedAt).toLocaleString()} · Süre: {formatDuration(run.durationMs)}
</div>
</div>
</div>
<JobStatusBadge status={run.status} />
</button>
))}
</CardContent>
</Card>
</div>
);
}