UI update
This commit is contained in:
@@ -4,6 +4,7 @@ import cors from "cors";
|
||||
import mongoose from "mongoose";
|
||||
import { Server } from "socket.io";
|
||||
import authRoutes from "./routes/auth.js";
|
||||
import jobsRoutes from "./routes/jobs.js";
|
||||
import { config } from "./config/env.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
@@ -22,6 +23,7 @@ app.get("/health", (_req, res) => {
|
||||
});
|
||||
|
||||
app.use("/auth", authRoutes);
|
||||
app.use("/jobs", jobsRoutes);
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
|
||||
26
backend/src/models/job.ts
Normal file
26
backend/src/models/job.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export type TimeUnit = "dakika" | "saat" | "gün";
|
||||
|
||||
export interface JobDocument extends Document {
|
||||
name: string;
|
||||
repoUrl: string;
|
||||
testCommand: string;
|
||||
checkValue: number;
|
||||
checkUnit: TimeUnit;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const JobSchema = new Schema<JobDocument>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true },
|
||||
repoUrl: { type: String, required: true, trim: true },
|
||||
testCommand: { type: String, required: true, trim: true },
|
||||
checkValue: { type: Number, required: true, min: 1 },
|
||||
checkUnit: { type: String, required: true, enum: ["dakika", "saat", "gün"] }
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export const Job = mongoose.model<JobDocument>("Job", JobSchema);
|
||||
54
backend/src/routes/jobs.ts
Normal file
54
backend/src/routes/jobs.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Router } from "express";
|
||||
import { authMiddleware } from "../middleware/authMiddleware.js";
|
||||
import { Job } from "../models/job.js";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get("/", async (_req, res) => {
|
||||
const jobs = await Job.find().sort({ createdAt: -1 }).lean();
|
||||
res.json(jobs);
|
||||
});
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit } = req.body;
|
||||
if (!name || !repoUrl || !testCommand || !checkValue || !checkUnit) {
|
||||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||||
}
|
||||
try {
|
||||
const job = await Job.create({ name, repoUrl, testCommand, checkValue, checkUnit });
|
||||
return res.status(201).json(job);
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Job oluşturulamadı", error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.put("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, repoUrl, testCommand, checkValue, checkUnit } = req.body;
|
||||
try {
|
||||
const job = await Job.findByIdAndUpdate(
|
||||
id,
|
||||
{ name, repoUrl, testCommand, checkValue, checkUnit },
|
||||
{ new: true, runValidators: true }
|
||||
);
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
return res.json(job);
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Job güncellenemedi", error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete("/:id", async (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const job = await Job.findByIdAndDelete(id);
|
||||
if (!job) return res.status(404).json({ message: "Job bulunamadı" });
|
||||
return res.json({ success: true });
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Job silinemedi", error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -10,8 +10,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-brands-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",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"axios": "^1.5.1",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
|
||||
41
frontend/src/api/jobs.ts
Normal file
41
frontend/src/api/jobs.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { apiClient } from "./client";
|
||||
|
||||
export type TimeUnit = "dakika" | "saat" | "gün";
|
||||
|
||||
export interface Job {
|
||||
_id: string;
|
||||
name: string;
|
||||
repoUrl: string;
|
||||
testCommand: string;
|
||||
checkValue: number;
|
||||
checkUnit: TimeUnit;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface JobInput {
|
||||
name: string;
|
||||
repoUrl: string;
|
||||
testCommand: string;
|
||||
checkValue: number;
|
||||
checkUnit: TimeUnit;
|
||||
}
|
||||
|
||||
export async function fetchJobs(): Promise<Job[]> {
|
||||
const { data } = await apiClient.get("/jobs");
|
||||
return data as Job[];
|
||||
}
|
||||
|
||||
export async function createJob(payload: JobInput): Promise<Job> {
|
||||
const { data } = await apiClient.post("/jobs", payload);
|
||||
return data as Job;
|
||||
}
|
||||
|
||||
export async function updateJob(id: string, payload: JobInput): Promise<Job> {
|
||||
const { data } = await apiClient.put(`/jobs/${id}`, payload);
|
||||
return data as Job;
|
||||
}
|
||||
|
||||
export async function deleteJob(id: string): Promise<void> {
|
||||
await apiClient.delete(`/jobs/${id}`);
|
||||
}
|
||||
115
frontend/src/components/ui/select.tsx
Normal file
115
frontend/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ScrollUpButton className="flex cursor-default items-center justify-center py-1">
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectPrimitive.ScrollDownButton className="flex cursor-default items-center justify-center py-1">
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label ref={ref} className={cn("px-2 py-1.5 text-sm font-semibold", className)} {...props} />
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator ref={ref} className={cn("m-1 h-px bg-border", className)} {...props} />
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator
|
||||
};
|
||||
@@ -1,30 +1,315 @@
|
||||
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 (
|
||||
<Card className="border-border card-shadow">
|
||||
<CardHeader>
|
||||
<CardTitle>Jobs Durumu</CardTitle>
|
||||
<CardDescription>Diğer sayfalarda başlatılan canlı sayaç burada izlenebilir.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||
<>
|
||||
<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">
|
||||
<span>Canlı Durum</span>
|
||||
<span className="font-semibold text-foreground">
|
||||
{running ? "Çalışıyor" : "Beklemede"}
|
||||
<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 className="flex items-center justify-between">
|
||||
<span>Sayaç Değeri</span>
|
||||
<span className="font-mono text-lg text-foreground">{value}</span>
|
||||
</div>
|
||||
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
Sayaç Home sayfasından başlatılır. Bu panel aynı anda açık olan tüm kullanıcılarda anlık güncellenir.
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user