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
This commit is contained in:
@@ -13,10 +13,11 @@ import {
|
||||
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 } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClockRotateLeft, faListCheck, faFlaskVial, faRocket } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
function formatDuration(ms?: number) {
|
||||
if (!ms || Number.isNaN(ms)) return "-";
|
||||
@@ -29,28 +30,79 @@ function formatDuration(ms?: number) {
|
||||
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(() => {
|
||||
fetchJobMetrics()
|
||||
.then(setMetrics)
|
||||
.catch(() => setError("Metrikler alınamadı"))
|
||||
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) return [];
|
||||
return metrics.dailyStats.map((d) => ({
|
||||
date: d._id,
|
||||
Başarılı: d.success,
|
||||
Hatalı: d.failed
|
||||
}));
|
||||
}, [metrics]);
|
||||
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 [];
|
||||
@@ -69,6 +121,35 @@ export function HomePage() {
|
||||
});
|
||||
}, [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 (
|
||||
@@ -78,14 +159,14 @@ export function HomePage() {
|
||||
<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>
|
||||
<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-80 min-w-0">
|
||||
<CardContent className="h-48 min-w-0">
|
||||
{loading ? (
|
||||
<div className="text-sm text-muted-foreground">Yükleniyor...</div>
|
||||
) : chartData.length === 0 ? (
|
||||
@@ -96,10 +177,24 @@ export function HomePage() {
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis allowDecimals={false} />
|
||||
<Tooltip />
|
||||
<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="Başarılı" stroke="#10b981" strokeWidth={2} />
|
||||
<Line type="monotone" dataKey="Hatalı" stroke="#ef4444" strokeWidth={2} />
|
||||
<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>
|
||||
)}
|
||||
@@ -111,7 +206,7 @@ export function HomePage() {
|
||||
<CardTitle>Hızlı Metrikler</CardTitle>
|
||||
<CardDescription>Özet görünüm</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<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">
|
||||
@@ -136,33 +231,50 @@ export function HomePage() {
|
||||
<CardHeader className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Etkinlik Akışı</CardTitle>
|
||||
<CardDescription>Son 10 job çalıştırması</CardDescription>
|
||||
<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" />
|
||||
{mergedRuns.length ?? 0} kayıt
|
||||
{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 && mergedRuns.length === 0 && (
|
||||
{!loading && activityItems.length === 0 && (
|
||||
<div className="text-sm text-muted-foreground">Henüz çalıştırma yok.</div>
|
||||
)}
|
||||
{!loading &&
|
||||
mergedRuns.map((run) => (
|
||||
activityItems.map((run) => (
|
||||
<button
|
||||
key={run._id}
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={() => navigate(`/jobs/${run.job._id}`)}
|
||||
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.job.repoUrl} />
|
||||
<RepoIcon repoUrl={run.repoUrl} />
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-foreground">{run.job.name}</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)}
|
||||
{new Date(run.startedAt).toLocaleString()} · Süre:{" "}
|
||||
{formatDuration(run.durationMs)}
|
||||
{run.type === "deploy" && run.message ? ` · ${run.message}` : ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user