Files
Wisecolt-CI/frontend/src/pages/HomePage.tsx
wisecolt e5fd3bd9d5 feat(deployments): docker tabanlı proje yönetim ve otomatik deploy sistemi ekle
Docker Compose projeleri için tam kapsamlı yönetim paneli ve otomatik deployment altyapısı eklendi.

Sistem özellikleri:
- Belirtilen root dizin altındaki docker-compose dosyası içeren projeleri tarama
- Git repo bağlantısı ile branch yönetimi ve klonlama/pull işlemleri
- Docker compose up/down komutları ile otomatik deploy
- Gitea webhook entegrasyonu ile commit bazlı tetikleme
- Deploy geçmişi, log kayıtları ve durum takibi (running/success/failed)
- Deploy metrikleri ve dashboard görselleştirmesi
- Webhook token ve secret yönetimi ile güvenlik
- Proje favicon servisi

Teknik değişiklikler:
- Backend: deploymentProject, deploymentRun ve settings modelleri eklendi
- Backend: deploymentService ile git ve docker işlemleri otomatize edildi
- Backend: webhook doğrulaması için signature kontrolü eklendi
- Docker: docker-cli ve docker-compose bağımlılıkları eklendi
- Frontend: deployments ve settings sayfaları eklendi
- Frontend: dashboard'a deploy metrikleri ve aktivite akışı eklendi
- API: /api/deployments ve /api/settings yolları eklendi
2026-01-18 16:24:11 +03:00

289 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useLiveData } from "../providers/live-provider";
import { fetchJobMetrics, JobMetrics } from "../api/jobs";
import { fetchDeploymentMetrics, DeploymentMetrics, DeploymentRunWithProject } from "../api/deployments";
import { JobStatusBadge } from "../components/JobStatusBadge";
import { RepoIcon } from "../components/RepoIcon";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faClockRotateLeft, faListCheck, faFlaskVial, faRocket } 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`;
}
function toYmd(date: Date) {
return date.toISOString().slice(0, 10);
}
export function HomePage() {
const [metrics, setMetrics] = useState<JobMetrics | null>(null);
const [deploymentMetrics, setDeploymentMetrics] = useState<DeploymentMetrics | null>(null);
const [deployRuns, setDeployRuns] = useState<DeploymentRunWithProject[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { jobStreams } = useLiveData();
const navigate = useNavigate();
useEffect(() => {
Promise.allSettled([fetchJobMetrics(), fetchDeploymentMetrics()])
.then(([jobResult, deployResult]) => {
if (jobResult.status === "fulfilled") {
setMetrics(jobResult.value);
} else {
setMetrics({
dailyStats: [],
recentRuns: [],
totals: { successRate: 0, totalRuns: 0 }
});
setError("Job metrikleri alınamadı");
}
if (deployResult.status === "fulfilled") {
setDeploymentMetrics(deployResult.value);
setDeployRuns(deployResult.value.recentRuns || []);
} else {
setDeploymentMetrics({ dailyStats: [], recentRuns: [] });
}
})
.finally(() => setLoading(false));
}, []);
const chartData = useMemo(() => {
if (!metrics) {
const days = Array.from({ length: 7 }).map((_, idx) => {
const date = new Date();
date.setDate(date.getDate() - (6 - idx));
return toYmd(date);
});
return days.map((date) => ({
date,
"Test Başarılı": 0,
"Test Hatalı": 0,
"Deploy Başarılı": 0,
"Deploy Hatalı": 0
}));
}
const deployMap = new Map((deploymentMetrics?.dailyStats || []).map((d) => [d._id, d]));
const jobMap = new Map(metrics.dailyStats.map((d) => [d._id, d]));
const days = Array.from({ length: 7 }).map((_, idx) => {
const date = new Date();
date.setDate(date.getDate() - (6 - idx));
return toYmd(date);
});
return days.map((date) => {
const job = jobMap.get(date);
const deploy = deployMap.get(date);
return {
date,
"Test Başarılı": job?.success || 0,
"Test Hatalı": job?.failed || 0,
"Deploy Başarılı": deploy?.success || 0,
"Deploy Hatalı": deploy?.failed || 0
};
});
}, [metrics, deploymentMetrics]);
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 activityItems = useMemo(() => {
const jobItems = mergedRuns.map((run) => ({
id: run._id,
type: "test" as const,
title: run.job.name,
repoUrl: run.job.repoUrl,
status: run.status,
startedAt: run.startedAt,
durationMs: run.durationMs,
link: `/jobs/${run.job._id}`
}));
const deployItems = deployRuns.map((run) => ({
id: run._id,
type: "deploy" as const,
title: run.project.name,
repoUrl: run.project.repoUrl,
status: run.status,
startedAt: run.startedAt,
durationMs: run.durationMs,
message: run.message,
link: `/deployments/${run.project._id}`
}));
return [...jobItems, ...deployItems]
.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime())
.slice(0, 10);
}, [mergedRuns, deployRuns]);
const lastRunDuration = useMemo(() => formatDuration(mergedRuns[0]?.durationMs), [mergedRuns]);
return (
<div className="grid gap-6">
<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>Test ve Deploy sonuç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-48 min-w-0">
{loading ? (
<div className="text-sm text-muted-foreground">Yükleniyor...</div>
) : 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
wrapperStyle={{ zIndex: 50 }}
contentStyle={{
backgroundColor: "white",
border: "1px solid #e5e7eb",
borderRadius: "8px",
boxShadow: "0 10px 20px rgba(0,0,0,0.12)",
color: "#111827",
opacity: 1
}}
labelStyle={{ color: "#111827", fontWeight: 600 }}
itemStyle={{ color: "#111827" }}
/>
<Legend />
<Line type="monotone" dataKey="Test Başarılı" stroke="#10b981" strokeWidth={2} />
<Line type="monotone" dataKey="Test Hatalı" stroke="#ef4444" strokeWidth={2} />
<Line type="monotone" dataKey="Deploy Başarılı" stroke="#f59e0b" strokeWidth={2} />
<Line type="monotone" dataKey="Deploy Hatalı" stroke="#f97316" 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="flex h-48 flex-col justify-center 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 aktivite</CardDescription>
</div>
<div className="text-xs text-muted-foreground flex items-center gap-1">
<FontAwesomeIcon icon={faListCheck} className="h-3.5 w-3.5" />
{activityItems.length ?? 0} kayıt
</div>
</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 && activityItems.length === 0 && (
<div className="text-sm text-muted-foreground">Henüz çalıştırma yok.</div>
)}
{!loading &&
activityItems.map((run) => (
<button
key={run.id}
type="button"
onClick={() => navigate(run.link)}
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.repoUrl} />
<div>
<div className="flex flex-wrap items-center gap-2 text-sm font-semibold text-foreground">
<span
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold ${
run.type === "test"
? "border-sky-200 bg-sky-100 text-sky-700"
: "border-amber-200 bg-amber-100 text-amber-800"
}`}
>
<FontAwesomeIcon
icon={run.type === "test" ? faFlaskVial : faRocket}
className="h-3 w-3"
/>
{run.type === "test" ? "Test" : "Deploy"}
</span>
<span>{run.title}</span>
</div>
<div className="text-xs text-muted-foreground">
{new Date(run.startedAt).toLocaleString()} · Süre:{" "}
{formatDuration(run.durationMs)}
{run.type === "deploy" && run.message ? ` · ${run.message}` : ""}
</div>
</div>
</div>
<JobStatusBadge status={run.status} />
</button>
))}
</CardContent>
</Card>
</div>
);
}