Files
Wisecolt-CI/frontend/src/pages/JobsPage.tsx
2025-11-26 20:17:53 +03:00

316 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 { toast } from "sonner";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faGithub, faGitlab } from "@fortawesome/free-brands-svg-icons";
import { faCodeBranch, faPen, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
import { Button } from "../components/ui/button";
import { Input } from "../components/ui/input";
import { Label } from "../components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from "../components/ui/select";
import { useLiveCounter } from "../providers/live-provider";
import { createJob, deleteJob, fetchJobs, Job, JobInput, updateJob } from "../api/jobs";
type FormState = {
_id?: string;
name: string;
repoUrl: string;
testCommand: string;
checkValue: string;
checkUnit: JobInput["checkUnit"];
};
const defaultForm: FormState = {
name: "",
repoUrl: "",
testCommand: "",
checkValue: "",
checkUnit: "dakika"
};
function RepoIcon({ repoUrl }: { repoUrl: string }) {
const lower = repoUrl.toLowerCase();
if (lower.includes("github.com")) {
return <FontAwesomeIcon icon={faGithub} className="h-5 w-5 text-foreground" />;
}
if (lower.includes("gitlab.com")) {
return <FontAwesomeIcon icon={faGitlab} className="h-5 w-5 text-foreground" />;
}
if (lower.includes("gitea")) {
return (
<div className="flex h-5 w-5 items-center justify-center rounded-sm bg-emerald-600 text-[10px] font-semibold text-white">
Ge
</div>
);
}
return <FontAwesomeIcon icon={faCodeBranch} className="h-5 w-5 text-foreground" />;
}
export function JobsPage() {
const { value, running } = useLiveCounter();
const [jobs, setJobs] = useState<Job[]>([]);
const [loading, setLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [form, setForm] = useState<FormState>(defaultForm);
const [saving, setSaving] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const isEdit = useMemo(() => !!form._id, [form._id]);
const loadJobs = async () => {
setLoading(true);
try {
const data = await fetchJobs();
setJobs(data);
} catch (err) {
toast.error("Jobs alınamadı");
} finally {
setLoading(false);
}
};
useEffect(() => {
loadJobs();
}, []);
const handleOpenNew = () => {
setForm(defaultForm);
setModalOpen(true);
};
const handleSave = async () => {
setSaving(true);
try {
const payload: JobInput = {
name: form.name,
repoUrl: form.repoUrl,
testCommand: form.testCommand,
checkValue: Number(form.checkValue),
checkUnit: form.checkUnit
};
if (!payload.name || !payload.repoUrl || !payload.testCommand || !payload.checkValue) {
toast.error("Tüm alanları doldurun");
setSaving(false);
return;
}
if (Number.isNaN(payload.checkValue) || payload.checkValue <= 0) {
toast.error("Check Time değeri 1 veya daha büyük olmalı");
setSaving(false);
return;
}
if (isEdit && form._id) {
const updated = await updateJob(form._id, payload);
setJobs((prev) => prev.map((j) => (j._id === updated._id ? updated : j)));
toast.success("Job güncellendi");
} else {
const created = await createJob(payload);
setJobs((prev) => [created, ...prev]);
toast.success("Job oluşturuldu");
}
setModalOpen(false);
} catch (err) {
toast.error("İşlem sırasında hata oluştu");
} finally {
setSaving(false);
}
};
const handleDelete = async (id: string) => {
setDeletingId(id);
try {
await deleteJob(id);
setJobs((prev) => prev.filter((j) => j._id !== id));
toast.success("Job silindi");
} catch (err) {
toast.error("Silme sırasında hata oluştu");
} finally {
setDeletingId(null);
}
};
const handleEdit = (job: Job) => {
const { _id, name, repoUrl, testCommand, checkValue, checkUnit } = job;
setForm({ _id, name, repoUrl, testCommand, checkValue: String(checkValue), checkUnit });
setModalOpen(true);
};
const handleClose = () => {
setModalOpen(false);
};
return (
<>
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-xl font-semibold text-foreground">Jobs</h2>
<p className="text-sm text-muted-foreground">
Home sayfasında başlatılan sayaç:{" "}
<span className="font-mono text-foreground">{value}</span> ({running ? "çalışıyor" : "beklemede"})
</p>
</div>
<Button onClick={handleOpenNew} className="gap-2">
<FontAwesomeIcon icon={faPlus} className="h-4 w-4" />
New Job
</Button>
</div>
<div className="grid gap-4">
{loading && (
<div className="rounded-md border border-border bg-muted/30 px-4 py-3 text-sm text-muted-foreground">
Jobs yükleniyor...
</div>
)}
{!loading && jobs.length === 0 && (
<div className="rounded-md border border-dashed border-border px-4 py-6 text-center text-sm text-muted-foreground">
Henüz job yok. Sağ üstten ekleyebilirsiniz.
</div>
)}
{!loading &&
jobs.map((job) => (
<Card key={job._id} className="border-border card-shadow">
<CardContent className="flex items-start gap-4 px-4 py-4">
<div className="pt-2.5">
<RepoIcon repoUrl={job.repoUrl} />
</div>
<div className="flex-1 space-y-2">
<div className="flex items-center justify-between">
<div className="text-lg font-semibold text-foreground">{job.name}</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-10 w-10"
onClick={() => handleEdit(job)}
>
<FontAwesomeIcon icon={faPen} className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className="h-10 w-10"
disabled={deletingId === job._id}
onClick={() => handleDelete(job._id)}
>
<FontAwesomeIcon icon={faTrash} className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
<div className="grid gap-1 text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">Repo:</span>
<span className="truncate text-foreground/80">{job.repoUrl}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">Test:</span>
<span className="text-foreground/80">{job.testCommand}</span>
</div>
<div className="flex items-center gap-2">
<span className="font-medium text-foreground">Kontrol:</span>
<span className="text-foreground/80">
{job.checkValue} {job.checkUnit}
</span>
</div>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{modalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
<div className="w-full max-w-lg rounded-lg border border-border bg-card card-shadow">
<div className="flex items-center justify-between border-b border-border px-5 py-4">
<div className="space-y-1">
<div className="text-lg font-semibold text-foreground">{isEdit ? "Job Güncelle" : "Yeni Job"}</div>
<div className="text-sm text-muted-foreground">
Detayları girin, Jobs listesi canlı olarak güncellenecek.
</div>
</div>
<Button variant="ghost" size="icon" onClick={handleClose}>
</Button>
</div>
<div className="space-y-4 px-5 py-4">
<div className="space-y-2">
<Label htmlFor="name">Job Name</Label>
<Input
id="name"
value={form.name}
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
placeholder="Nightly E2E"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="repo">Repo URL</Label>
<Input
id="repo"
value={form.repoUrl}
onChange={(e) => setForm((prev) => ({ ...prev, repoUrl: e.target.value }))}
placeholder="https://github.com/org/repo"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="test">Test Command</Label>
<Input
id="test"
value={form.testCommand}
onChange={(e) => setForm((prev) => ({ ...prev, testCommand: e.target.value }))}
placeholder="npm test"
required
/>
</div>
<div className="space-y-2">
<Label>Check Time</Label>
<div className="flex gap-3">
<Input
type="number"
min={1}
className="w-32"
value={form.checkValue}
placeholder="15"
onChange={(e) => setForm((prev) => ({ ...prev, checkValue: e.target.value }))}
/>
<Select
value={form.checkUnit}
onValueChange={(value) =>
setForm((prev) => ({ ...prev, checkUnit: value as JobInput["checkUnit"] }))
}
>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Birim seçin" />
</SelectTrigger>
<SelectContent>
<SelectItem value="dakika">dakika</SelectItem>
<SelectItem value="saat">saat</SelectItem>
<SelectItem value="gün">gün</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
<Button variant="ghost" onClick={handleClose} disabled={saving}>
İptal
</Button>
<Button onClick={handleSave} disabled={saving}>
{saving ? "Kaydediliyor..." : "Kaydet"}
</Button>
</div>
</div>
</div>
)}
</>
);
}