Update
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user