UI Update
This commit is contained in:
@@ -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<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const child = spawn(command, {
|
const child = spawn(command, {
|
||||||
cwd,
|
cwd,
|
||||||
@@ -34,23 +39,37 @@ function runCommand(command: string, cwd: string, onData: (chunk: string) => voi
|
|||||||
env: { ...process.env, CI: process.env.CI || "1" }
|
env: { ...process.env, CI: process.env.CI || "1" }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout | null = null;
|
||||||
|
let timedOut = false;
|
||||||
const emitLines = (chunk: Buffer) => {
|
const emitLines = (chunk: Buffer) => {
|
||||||
const cleaned = cleanOutput(chunk.toString()).replace(/\r\n|\r/g, "\n");
|
const cleaned = cleanOutput(chunk.toString()).replace(/\r\n|\r/g, "\n");
|
||||||
cleaned.split("\n").forEach((line) => {
|
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.stdout.on("data", emitLines);
|
||||||
child.stderr.on("data", emitLines);
|
child.stderr.on("data", emitLines);
|
||||||
|
|
||||||
child.on("error", (err) => {
|
child.on("error", (err) => {
|
||||||
|
if (timeout) clearTimeout(timeout);
|
||||||
onData(`Hata: ${err.message}`);
|
onData(`Hata: ${err.message}`);
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on("close", (code) => {
|
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();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`Komut kod ${code} ile kapandı`));
|
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 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);
|
await this.emitStatus(jobId, { status: "running", lastMessage: "Çalıştırılıyor..." } as JobDocument);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const repoDir = await cloneOrPull(job, (line) => pushLog(line));
|
const repoDir = await cloneOrPull(job, (line) => pushLog(line));
|
||||||
await ensureDependencies(repoDir, (line) => pushLog(line));
|
await ensureDependencies(repoDir, (line) => pushLog(line));
|
||||||
pushLog(`Test komutu çalıştırılıyor: ${job.testCommand}`);
|
pushLog(`Test komutu çalıştırılıyor: ${job.testCommand}`);
|
||||||
await runCommand(job.testCommand, repoDir, (line) => pushLog(line));
|
await runCommand(job.testCommand, repoDir, (line) => pushLog(line), 180_000);
|
||||||
pushLog("Test tamamlandı: Başarılı");
|
pushLog("Test tamamlandı: Başarılı");
|
||||||
const duration = Date.now() - startedAt;
|
const duration = Date.now() - startedAt;
|
||||||
await Job.findByIdAndUpdate(jobId, {
|
await Job.findByIdAndUpdate(jobId, {
|
||||||
status: "success",
|
status: "success",
|
||||||
lastRunAt: new Date(),
|
lastRunAt: new Date(),
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||||
"@fortawesome/free-brands-svg-icons": "^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/free-solid-svg-icons": "^7.1.0",
|
||||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
import { NavLink, Outlet, useNavigate } from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 { Button } from "./ui/button";
|
||||||
import { ThemeToggle } from "./ThemeToggle";
|
import { ThemeToggle } from "./ThemeToggle";
|
||||||
import { useAuth } from "../providers/auth-provider";
|
import { useAuth } from "../providers/auth-provider";
|
||||||
@@ -15,7 +15,7 @@ export function DashboardLayout() {
|
|||||||
const navigation = useMemo(
|
const navigation = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: "Home", to: "/home", icon: faHouse },
|
{ 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() {
|
|||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="flex-1 overflow-y-auto md:ml-64">
|
<main className="flex-1 overflow-y-auto md:ml-64 min-w-0">
|
||||||
<div className="mx-auto flex max-w-5xl flex-col gap-6 px-4 py-8 sm:px-6 lg:px-8">
|
<div className="mx-auto flex max-w-5xl flex-col gap-6 px-4 py-8 sm:px-6 lg:px-8">
|
||||||
<div className="grid gap-6">
|
<div className="grid gap-6">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
|
|||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<SocketProvider>
|
<SocketProvider>
|
||||||
<LiveProvider>
|
<LiveProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<App />
|
<App />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -69,48 +69,10 @@ export function HomePage() {
|
|||||||
});
|
});
|
||||||
}, [metrics, jobStreams]);
|
}, [metrics, jobStreams]);
|
||||||
|
|
||||||
const lastRun = mergedRuns[0];
|
const lastRunDuration = useMemo(() => formatDuration(mergedRuns[0]?.durationMs), [mergedRuns]);
|
||||||
const lastRunDuration = formatDuration(lastRun?.durationMs);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6">
|
<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">
|
<div className="grid gap-4 lg:grid-cols-3">
|
||||||
<Card className="border-border card-shadow lg:col-span-2">
|
<Card className="border-border card-shadow lg:col-span-2">
|
||||||
<CardHeader className="flex items-center justify-between">
|
<CardHeader className="flex items-center justify-between">
|
||||||
@@ -123,8 +85,10 @@ export function HomePage() {
|
|||||||
{metrics?.totals.totalRuns ?? 0} toplam koşu
|
{metrics?.totals.totalRuns ?? 0} toplam koşu
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-80">
|
<CardContent className="h-80 min-w-0">
|
||||||
{chartData.length === 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>
|
<div className="text-sm text-muted-foreground">Grafik verisi yok.</div>
|
||||||
) : (
|
) : (
|
||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import { useParams, useNavigate } from "react-router-dom";
|
|||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
faArrowLeft,
|
faArrowLeft,
|
||||||
faCircleCheck,
|
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
faClock,
|
faClock,
|
||||||
faPen,
|
faPen,
|
||||||
faTrash
|
faTrash,
|
||||||
|
faVialCircleCheck,
|
||||||
|
faFilterCircleXmark,
|
||||||
|
faRepeat,
|
||||||
|
faClockRotateLeft,
|
||||||
|
faAlarmClock
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||||
import { Button } from "../components/ui/button";
|
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 { useJobStream } from "../providers/live-provider";
|
||||||
import { useSocket } from "../providers/socket-provider";
|
import { useSocket } from "../providers/socket-provider";
|
||||||
import { JobStatusBadge } from "../components/JobStatusBadge";
|
import { JobStatusBadge } from "../components/JobStatusBadge";
|
||||||
import { faListCheck } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Input } from "../components/ui/input";
|
import { Input } from "../components/ui/input";
|
||||||
import { Label } from "../components/ui/label";
|
import { Label } from "../components/ui/label";
|
||||||
@@ -51,7 +54,7 @@ export function JobDetailPage() {
|
|||||||
});
|
});
|
||||||
const stream = useJobStream(id || "");
|
const stream = useJobStream(id || "");
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
const logContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const prevStatusRef = useRef<string | undefined>(undefined);
|
const prevStatusRef = useRef<string | undefined>(undefined);
|
||||||
const currentLogs = stream.logs.length > 0 ? stream.logs : lastRun?.logs || [];
|
const currentLogs = stream.logs.length > 0 ? stream.logs : lastRun?.logs || [];
|
||||||
|
|
||||||
@@ -220,7 +223,9 @@ export function JobDetailPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
if (logContainerRef.current) {
|
||||||
|
logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight;
|
||||||
|
}
|
||||||
}, [currentLogs]);
|
}, [currentLogs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -314,7 +319,7 @@ export function JobDetailPage() {
|
|||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<CardTitle>{job?.name || "Job Detayı"}</CardTitle>
|
<CardTitle>{job?.name || "Job Detayı"}</CardTitle>
|
||||||
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-muted/50 px-3 py-1 text-xs font-semibold text-foreground">
|
<span className="inline-flex items-center gap-2 rounded-full border border-border bg-muted/50 px-3 py-1 text-xs font-semibold text-foreground">
|
||||||
<FontAwesomeIcon icon={faListCheck} className="h-3.5 w-3.5 text-foreground/80" />
|
<FontAwesomeIcon icon={faRepeat} className="h-3.5 w-3.5 text-foreground/80" />
|
||||||
{runCount} test
|
{runCount} test
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -338,7 +343,8 @@ export function JobDetailPage() {
|
|||||||
{job.checkValue} {job.checkUnit}
|
{job.checkValue} {job.checkUnit}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg leading-none text-muted-foreground">·</span>
|
<span className="text-lg leading-none text-muted-foreground">·</span>
|
||||||
<span className="rounded-md bg-muted px-2 py-1 font-mono text-xs text-foreground/80">
|
<span className="inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 font-mono text-xs text-foreground/80">
|
||||||
|
<FontAwesomeIcon icon={faClockRotateLeft} className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
{countdown}
|
{countdown}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -450,35 +456,35 @@ export function JobDetailPage() {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{testSummary.testFiles ? (
|
{testSummary.testFiles ? (
|
||||||
<div className="flex items-start gap-3 text-sm text-foreground">
|
<div className="flex items-start gap-3 text-sm text-foreground">
|
||||||
<FontAwesomeIcon icon={faCircleCheck} className="mt-0.5 h-4 w-4 text-emerald-600" />
|
<FontAwesomeIcon icon={faVialCircleCheck} className="mt-0.5 h-4 w-4 text-emerald-600" />
|
||||||
<span className="font-medium">{testSummary.testFiles}</span>
|
<span className="font-medium">{testSummary.testFiles}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{testSummary.tests ? (
|
{testSummary.tests ? (
|
||||||
<div className="flex items-start gap-3 text-sm text-foreground">
|
<div className="flex items-start gap-3 text-sm text-foreground">
|
||||||
<FontAwesomeIcon icon={faCircleCheck} className="mt-0.5 h-4 w-4 text-emerald-600" />
|
<FontAwesomeIcon icon={faVialCircleCheck} className="mt-0.5 h-4 w-4 text-emerald-600" />
|
||||||
<span className="font-medium">{testSummary.tests}</span>
|
<span className="font-medium">{testSummary.tests}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{testSummary.passing ? (
|
{testSummary.passing ? (
|
||||||
<div className="flex items-start gap-3 text-sm text-foreground">
|
<div className="flex items-start gap-3 text-sm text-foreground">
|
||||||
<FontAwesomeIcon icon={faCircleCheck} className="mt-0.5 h-4 w-4 text-emerald-600" />
|
<FontAwesomeIcon icon={faVialCircleCheck} className="mt-0.5 h-4 w-4 text-emerald-600" />
|
||||||
<span className="font-medium">{testSummary.passing}</span>
|
<span className="font-medium">{testSummary.passing}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{testSummary.failing ? (
|
{testSummary.failing ? (
|
||||||
<div className="flex items-start gap-3 text-sm text-foreground">
|
<div className="flex items-start gap-3 text-sm text-foreground">
|
||||||
<FontAwesomeIcon icon={faCircleExclamation} className="mt-0.5 h-4 w-4 text-red-500" />
|
<FontAwesomeIcon icon={faFilterCircleXmark} className="mt-0.5 h-4 w-4 text-red-400" />
|
||||||
<span className="font-medium">{testSummary.failing}</span>
|
<span className="font-medium">{testSummary.failing}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{testSummary.startAt ? (
|
{testSummary.startAt ? (
|
||||||
<div className="flex items-start gap-3 text-sm text-foreground">
|
<div className="flex items-start gap-3 text-sm text-foreground">
|
||||||
<FontAwesomeIcon icon={faClock} className="mt-0.5 h-4 w-4" style={{ color: "#F6C344" }} />
|
<FontAwesomeIcon icon={faAlarmClock} className="mt-0.5 h-4 w-4 text-orange-500" />
|
||||||
<span className="font-medium">{testSummary.startAt}</span>
|
<span className="font-medium">{testSummary.startAt}</span>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -496,7 +502,10 @@ export function JobDetailPage() {
|
|||||||
<CardTitle>Logs</CardTitle>
|
<CardTitle>Logs</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="h-64 overflow-auto rounded-md border border-border bg-black p-3 font-mono text-xs text-green-100">
|
<div
|
||||||
|
ref={logContainerRef}
|
||||||
|
className="h-64 overflow-auto rounded-md border border-border bg-black p-3 font-mono text-xs text-green-100"
|
||||||
|
>
|
||||||
{currentLogs.length === 0 && (
|
{currentLogs.length === 0 && (
|
||||||
<div className="text-muted-foreground">Henüz çıktı yok. Test çalıştırmaları bekleniyor.</div>
|
<div className="text-muted-foreground">Henüz çıktı yok. Test çalıştırmaları bekleniyor.</div>
|
||||||
)}
|
)}
|
||||||
@@ -505,7 +514,6 @@ export function JobDetailPage() {
|
|||||||
{line}
|
{line}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={logEndRef} />
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 { Card, CardContent } from "../components/ui/card";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Input } from "../components/ui/input";
|
import { Input } from "../components/ui/input";
|
||||||
@@ -226,7 +226,7 @@ export function JobsPage() {
|
|||||||
<div className="text-lg font-semibold text-foreground">{job.name}</div>
|
<div className="text-lg font-semibold text-foreground">{job.name}</div>
|
||||||
<JobStatusBadge status={status} />
|
<JobStatusBadge status={status} />
|
||||||
<span className="ml-1 inline-flex items-center gap-1 rounded-full border border-border bg-muted/60 px-2 py-1 text-[11px] font-semibold text-foreground">
|
<span className="ml-1 inline-flex items-center gap-1 rounded-full border border-border bg-muted/60 px-2 py-1 text-[11px] font-semibold text-foreground">
|
||||||
<FontAwesomeIcon icon={faListCheck} className="h-3.5 w-3.5 text-foreground/80" />
|
<FontAwesomeIcon icon={faRepeat} className="h-3.5 w-3.5 text-foreground/80" />
|
||||||
{runCount}x
|
{runCount}x
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -262,7 +262,8 @@ export function JobsPage() {
|
|||||||
{job.checkValue} {job.checkUnit}
|
{job.checkValue} {job.checkUnit}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg leading-none text-muted-foreground">·</span>
|
<span className="text-lg leading-none text-muted-foreground">·</span>
|
||||||
<span className="rounded-md bg-muted px-2 py-1 font-mono text-xs text-foreground/80">
|
<span className="inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 font-mono text-xs text-foreground/80">
|
||||||
|
<FontAwesomeIcon icon={faClockRotateLeft} className="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
{countdown}
|
{countdown}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user