diff --git a/backend/src/services/jobService.ts b/backend/src/services/jobService.ts index 271cfea..a58691b 100644 --- a/backend/src/services/jobService.ts +++ b/backend/src/services/jobService.ts @@ -26,7 +26,12 @@ function cleanOutput(input: string) { ); } -function runCommand(command: string, cwd: string, onData: (chunk: string) => void) { +function runCommand( + command: string, + cwd: string, + onData: (chunk: string) => void, + timeoutMs?: number +) { return new Promise((resolve, reject) => { const child = spawn(command, { cwd, @@ -34,23 +39,37 @@ function runCommand(command: string, cwd: string, onData: (chunk: string) => voi env: { ...process.env, CI: process.env.CI || "1" } }); + let timeout: NodeJS.Timeout | null = null; + let timedOut = false; const emitLines = (chunk: Buffer) => { const cleaned = cleanOutput(chunk.toString()).replace(/\r\n|\r/g, "\n"); cleaned.split("\n").forEach((line) => { - onData(line); + if (line.trim().length > 0) onData(line); }); }; + if (timeoutMs) { + timeout = setTimeout(() => { + timedOut = true; + onData(`Test zaman aşımına uğradı (${timeoutMs / 1000}s)`); + child.kill("SIGKILL"); + }, timeoutMs); + } + child.stdout.on("data", emitLines); child.stderr.on("data", emitLines); child.on("error", (err) => { + if (timeout) clearTimeout(timeout); onData(`Hata: ${err.message}`); reject(err); }); child.on("close", (code) => { - if (code === 0) { + if (timeout) clearTimeout(timeout); + if (timedOut) { + reject(new Error("Test zaman aşımına uğradı")); + } else if (code === 0) { resolve(); } else { reject(new Error(`Komut kod ${code} ile kapandı`)); @@ -141,13 +160,13 @@ class JobService { await Job.findByIdAndUpdate(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." }); await this.emitStatus(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." } as JobDocument); - try { - const repoDir = await cloneOrPull(job, (line) => pushLog(line)); - await ensureDependencies(repoDir, (line) => pushLog(line)); - pushLog(`Test komutu çalıştırılıyor: ${job.testCommand}`); - await runCommand(job.testCommand, repoDir, (line) => pushLog(line)); - pushLog("Test tamamlandı: Başarılı"); - const duration = Date.now() - startedAt; + try { + const repoDir = await cloneOrPull(job, (line) => pushLog(line)); + await ensureDependencies(repoDir, (line) => pushLog(line)); + pushLog(`Test komutu çalıştırılıyor: ${job.testCommand}`); + await runCommand(job.testCommand, repoDir, (line) => pushLog(line), 180_000); + pushLog("Test tamamlandı: Başarılı"); + const duration = Date.now() - startedAt; await Job.findByIdAndUpdate(jobId, { status: "success", lastRunAt: new Date(), diff --git a/frontend/package.json b/frontend/package.json index 0f15841..ec89314 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", + "@fortawesome/free-regular-svg-icons": "^7.1.0", "@fortawesome/free-solid-svg-icons": "^7.1.0", "@fortawesome/react-fontawesome": "^3.1.0", "@radix-ui/react-select": "^2.2.6", diff --git a/frontend/src/components/DashboardLayout.tsx b/frontend/src/components/DashboardLayout.tsx index 49409dc..069b7e3 100644 --- a/frontend/src/components/DashboardLayout.tsx +++ b/frontend/src/components/DashboardLayout.tsx @@ -1,7 +1,7 @@ import React, { useMemo, useState } from "react"; import { NavLink, Outlet, useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faHouse, faBriefcase, faArrowRightFromBracket, faUser } from "@fortawesome/free-solid-svg-icons"; +import { faHouse, faBriefcase, faArrowRightFromBracket, faUser, faFlaskVial } from "@fortawesome/free-solid-svg-icons"; import { Button } from "./ui/button"; import { ThemeToggle } from "./ThemeToggle"; import { useAuth } from "../providers/auth-provider"; @@ -15,7 +15,7 @@ export function DashboardLayout() { const navigation = useMemo( () => [ { label: "Home", to: "/home", icon: faHouse }, - { label: "Jobs", to: "/jobs", icon: faBriefcase } + { label: "Jobs", to: "/jobs", icon: faFlaskVial } ], [] ); @@ -73,7 +73,7 @@ export function DashboardLayout() { -
+
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index e70d1e9..e83053b 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -15,7 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - + diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index a3fb836..60839f4 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -69,48 +69,10 @@ export function HomePage() { }); }, [metrics, jobStreams]); - const lastRun = mergedRuns[0]; - const lastRunDuration = formatDuration(lastRun?.durationMs); + const lastRunDuration = useMemo(() => formatDuration(mergedRuns[0]?.durationMs), [mergedRuns]); return (
-
- - - Son Çalıştırma - Son 10 çalıştırmanın en günceli -
- Başarı oranı:{" "} - {metrics?.totals.successRate ?? 0}% -
-
- - {loading &&
Yükleniyor...
} - {error &&
{error}
} - {!loading && lastRun && ( -
-
- -
-
{lastRun.job.name}
-
- {new Date(lastRun.startedAt).toLocaleString()} -
-
-
-
- -
- Süre: {lastRunDuration} -
-
-
- )} - {!loading && !lastRun &&
Henüz kayıt yok.
} -
-
-
-
@@ -123,8 +85,10 @@ export function HomePage() { {metrics?.totals.totalRuns ?? 0} toplam koşu
- - {chartData.length === 0 ? ( + + {loading ? ( +
Yükleniyor...
+ ) : chartData.length === 0 ? (
Grafik verisi yok.
) : ( diff --git a/frontend/src/pages/JobDetailPage.tsx b/frontend/src/pages/JobDetailPage.tsx index c98dcbf..d3164c4 100644 --- a/frontend/src/pages/JobDetailPage.tsx +++ b/frontend/src/pages/JobDetailPage.tsx @@ -3,11 +3,15 @@ import { useParams, useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faArrowLeft, - faCircleCheck, faCircleExclamation, faClock, faPen, - faTrash + faTrash, + faVialCircleCheck, + faFilterCircleXmark, + faRepeat, + faClockRotateLeft, + faAlarmClock } from "@fortawesome/free-solid-svg-icons"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; import { Button } from "../components/ui/button"; @@ -16,7 +20,6 @@ import { deleteJob, fetchJob, Job, JobInput, JobRun, runJob, updateJob } from ". import { useJobStream } from "../providers/live-provider"; import { useSocket } from "../providers/socket-provider"; import { JobStatusBadge } from "../components/JobStatusBadge"; -import { faListCheck } from "@fortawesome/free-solid-svg-icons"; import { toast } from "sonner"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; @@ -51,7 +54,7 @@ export function JobDetailPage() { }); const stream = useJobStream(id || ""); const socket = useSocket(); - const logEndRef = useRef(null); + const logContainerRef = useRef(null); const prevStatusRef = useRef(undefined); const currentLogs = stream.logs.length > 0 ? stream.logs : lastRun?.logs || []; @@ -220,7 +223,9 @@ export function JobDetailPage() { }; useEffect(() => { - logEndRef.current?.scrollIntoView({ behavior: "smooth" }); + if (logContainerRef.current) { + logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; + } }, [currentLogs]); useEffect(() => { @@ -314,7 +319,7 @@ export function JobDetailPage() {
{job?.name || "Job Detayı"} - + {runCount} test
@@ -338,7 +343,8 @@ export function JobDetailPage() { {job.checkValue} {job.checkUnit} · - + + {countdown}
@@ -450,35 +456,35 @@ export function JobDetailPage() {
{testSummary.testFiles ? (
- + {testSummary.testFiles}
) : null} {testSummary.tests ? (
- + {testSummary.tests}
) : null} {testSummary.passing ? (
- + {testSummary.passing}
) : null} {testSummary.failing ? (
- + {testSummary.failing}
) : null} {testSummary.startAt ? (
- + {testSummary.startAt}
) : null} @@ -496,7 +502,10 @@ export function JobDetailPage() { Logs -
+
{currentLogs.length === 0 && (
Henüz çıktı yok. Test çalıştırmaları bekleniyor.
)} @@ -505,7 +514,6 @@ export function JobDetailPage() { {line}
))} -
diff --git a/frontend/src/pages/JobsPage.tsx b/frontend/src/pages/JobsPage.tsx index f9ff775..de04e4d 100644 --- a/frontend/src/pages/JobsPage.tsx +++ b/frontend/src/pages/JobsPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faListCheck, faPen, faPlay, faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faListCheck, faPlay, faPlus, faClockRotateLeft, faRepeat } from "@fortawesome/free-solid-svg-icons"; import { Card, CardContent } from "../components/ui/card"; import { Button } from "../components/ui/button"; import { Input } from "../components/ui/input"; @@ -226,7 +226,7 @@ export function JobsPage() {
{job.name}
- + {runCount}x
@@ -262,7 +262,8 @@ export function JobsPage() { {job.checkValue} {job.checkUnit} · - + + {countdown}