feat(loop): setup profil yönetimini LoopSetupCard'a taşı

Dashboard'daki ProfilesCard bileşenini kaldırıp profil yönetimi
işlevselliğini LoopSetupCard bileşeni içine entegre etti. Artık
kullanıcılar loop setuplarını doğrudan LoopSetupCard üzerinden
oluşturabilir, düzenleyebilir, silebilir ve uygulayabilir.
This commit is contained in:
2026-01-04 00:22:40 +03:00
parent 915fb48828
commit 2bf2e2495d
2 changed files with 246 additions and 81 deletions

View File

@@ -1,49 +1,176 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useAppStore } from "../../store/useAppStore";
import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card";
import { Input } from "../ui/Input";
import { Button } from "../ui/Button";
import { api } from "../../api/client";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleInfo, faClock, faList, faLock } from "@fortawesome/free-solid-svg-icons";
import { faCircleInfo, faPen, faPlay, faTrash } from "@fortawesome/free-solid-svg-icons";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "../ui/AlertDialog";
import { useUiStore } from "../../store/useUiStore";
interface Profile {
id: string;
name: string;
allowIp: string;
delayMs: number;
targetLoops: number;
}
export const LoopSetupCard = () => {
const selectedHash = useAppStore((s) => s.selectedHash);
const jobs = useAppStore((s) => s.jobs);
const loopForm = useAppStore((s) => s.loopForm);
const setLoopForm = useAppStore((s) => s.setLoopForm);
const [dryRun, setDryRun] = useState<string | null>(null);
const pushAlert = useUiStore((s) => s.pushAlert);
const job = jobs.find((j) => j.torrentHash === selectedHash);
const [profiles, setProfiles] = useState<Profile[]>([]);
const [name, setName] = useState("");
const [allowIp, setAllowIp] = useState(loopForm.allowIp || "");
const [delayMs, setDelayMs] = useState(loopForm.delayMs ?? 3000);
const [targetLoops, setTargetLoops] = useState(loopForm.targetLoops ?? 3);
const [editingId, setEditingId] = useState<string | null>(null);
const startLoop = async () => {
if (!selectedHash) {
const loadProfiles = async () => {
const response = await api.get("/api/profiles");
setProfiles(response.data);
};
useEffect(() => {
loadProfiles();
}, []);
useEffect(() => {
setAllowIp(loopForm.allowIp || "");
setDelayMs(loopForm.delayMs ?? 3000);
setTargetLoops(loopForm.targetLoops ?? 3);
}, [loopForm]);
const saveProfile = async () => {
const payload = {
name: name.trim(),
allowIp: allowIp.trim(),
delayMs,
targetLoops,
};
if (!payload.name || !payload.allowIp) {
pushAlert({
title: "Eksik bilgi",
description: "Preset name ve Allow IP zorunlu.",
variant: "warn",
});
return;
}
await api.post("/api/loop/start", {
hash: selectedHash,
allowIp: loopForm.allowIp,
targetLoops: loopForm.targetLoops,
delayMs: loopForm.delayMs,
try {
if (editingId) {
const response = await api.put(`/api/profiles/${editingId}`, payload);
setProfiles((prev) =>
prev.map((profile) => (profile.id === editingId ? response.data : profile))
);
pushAlert({
title: "Setup güncellendi",
description: "Kaydedilen setup güncellendi.",
variant: "success",
});
setEditingId(null);
} else {
const response = await api.post("/api/profiles", payload);
setProfiles((prev) => [...prev, response.data]);
pushAlert({
title: "Setup kaydedildi",
description: "Yeni setup eklendi.",
variant: "success",
});
}
setLoopForm({
allowIp: payload.allowIp,
delayMs: payload.delayMs,
targetLoops: payload.targetLoops,
});
setName("");
} catch (error) {
pushAlert({
title: "Kaydedilemedi",
description: "Lütfen bilgileri kontrol edip tekrar deneyin.",
variant: "error",
});
}
};
const startEdit = (profile: Profile) => {
setEditingId(profile.id);
setName(profile.name);
setAllowIp(profile.allowIp);
setDelayMs(profile.delayMs);
setTargetLoops(profile.targetLoops);
setLoopForm({
allowIp: profile.allowIp,
delayMs: profile.delayMs,
targetLoops: profile.targetLoops,
});
};
const stopLoop = async () => {
const applyProfile = async (profile: Profile) => {
if (!selectedHash) {
pushAlert({
title: "Torrent seçilmedi",
description: "Önce bir torrent seçin.",
variant: "warn",
});
return;
}
await api.post("/api/loop/stop-by-hash", { hash: selectedHash });
try {
await api.post("/api/loop/start", {
hash: selectedHash,
allowIp: profile.allowIp,
targetLoops: profile.targetLoops,
delayMs: profile.delayMs,
});
setLoopForm({
allowIp: profile.allowIp,
delayMs: profile.delayMs,
targetLoops: profile.targetLoops,
});
pushAlert({
title: "Loop başlatıldı",
description: "Seçilen setup ile loop başlatıldı.",
variant: "success",
});
} catch (error: any) {
const apiError = error?.response?.data?.error;
pushAlert({
title: "Loop başlatılamadı",
description: apiError || "Sunucu loglarını kontrol edin.",
variant: "error",
});
}
};
const runDry = async () => {
if (!selectedHash) {
return;
const removeProfile = async (profileId: string) => {
try {
await api.delete(`/api/profiles/${profileId}`);
setProfiles((prev) => prev.filter((profile) => profile.id !== profileId));
pushAlert({
title: "Setup silindi",
description: "Setup listeden kaldırıldı.",
variant: "success",
});
} catch (error) {
pushAlert({
title: "Silme başarısız",
description: "Setup silinemedi.",
variant: "error",
});
}
const response = await api.post("/api/loop/dry-run", {
hash: selectedHash,
allowIp: loopForm.allowIp || "127.0.0.1",
});
setDryRun(JSON.stringify(response.data, null, 2));
};
return (
@@ -56,54 +183,108 @@ export const LoopSetupCard = () => {
</CardHeader>
<CardContent>
<div className="grid gap-3 text-sm">
<label className="space-y-2">
<span className="flex items-center gap-2">
<FontAwesomeIcon icon={faLock} className="text-slate-400" />
Allow IP
</span>
<div className="grid gap-2">
<Input
value={loopForm.allowIp}
onChange={(e) => setLoopForm({ allowIp: e.target.value })}
placeholder="Preset name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</label>
<label className="space-y-2">
<span className="flex items-center gap-2">
<FontAwesomeIcon icon={faList} className="text-slate-400" />
Loops
</span>
<Input
type="number"
value={loopForm.targetLoops}
onChange={(e) => setLoopForm({ targetLoops: Number(e.target.value) })}
placeholder="Allow IP"
value={allowIp}
onChange={(e) => {
setAllowIp(e.target.value);
setLoopForm({ allowIp: e.target.value });
}}
/>
</label>
<label className="space-y-2">
<span className="flex items-center gap-2">
<FontAwesomeIcon icon={faClock} className="text-slate-400" />
Delay (ms)
</span>
<Input
type="number"
value={loopForm.delayMs}
onChange={(e) => setLoopForm({ delayMs: Number(e.target.value) })}
/>
</label>
<div className="flex gap-2">
<Button onClick={startLoop} disabled={!selectedHash || !loopForm.allowIp}>
Start
</Button>
<Button variant="outline" onClick={stopLoop} disabled={!selectedHash}>
Stop
</Button>
<Button variant="ghost" onClick={runDry} disabled={!selectedHash}>
Dry Run
<div className="grid grid-cols-2 gap-2">
<Input
type="number"
placeholder="Delay (ms)"
value={delayMs}
onChange={(e) => {
const value = Number(e.target.value);
setDelayMs(value);
setLoopForm({ delayMs: value });
}}
/>
<Input
type="number"
placeholder="Loops"
value={targetLoops}
onChange={(e) => {
const value = Number(e.target.value);
setTargetLoops(value);
setLoopForm({ targetLoops: value });
}}
/>
</div>
<Button onClick={saveProfile} className="w-full">
Save Setup
</Button>
</div>
{dryRun && (
<pre className="max-h-40 overflow-auto rounded-md bg-slate-900 p-3 text-xs text-slate-100">
{dryRun}
</pre>
)}
<div className="border-t border-slate-200 pt-3">
<div className="space-y-2">
{profiles.map((profile) => (
<div
key={profile.id}
className="flex items-center justify-between rounded-md border border-slate-200 bg-white px-3 py-2"
>
<div>
<div className="font-semibold">{profile.name}</div>
<div className="text-xs text-slate-500">
{profile.allowIp} {profile.targetLoops} loops {profile.delayMs} ms
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
className="h-8 w-8 px-0"
onClick={() => applyProfile(profile)}
title="Apply"
>
<FontAwesomeIcon icon={faPlay} />
</Button>
<Button
variant="outline"
className="h-8 w-8 px-0"
onClick={() => startEdit(profile)}
title="Edit"
>
<FontAwesomeIcon icon={faPen} />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" className="h-8 w-8 px-0" title="Delete">
<FontAwesomeIcon icon={faTrash} />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Setup silinsin mi?</AlertDialogTitle>
<AlertDialogDescription>
Bu işlem geri alınamaz.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>İptal</AlertDialogCancel>
<AlertDialogAction onClick={() => removeProfile(profile.id)}>
Sil
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
))}
{profiles.length === 0 && (
<div className="rounded-md border border-dashed border-slate-200 bg-white px-3 py-2 text-xs text-slate-500">
Henüz kayıtlı setup yok.
</div>
)}
</div>
</div>
</div>
</CardContent>
</Card>

View File

@@ -4,12 +4,7 @@ import { TorrentDetailsCard } from "../components/torrents/TorrentDetailsCard";
import { LoopSetupCard } from "../components/loop/LoopSetupCard";
import { LoopStatsCard } from "../components/loop/LoopStatsCard";
import { LogsPanel } from "../components/loop/LogsPanel";
import { ProfilesCard } from "../components/loop/ProfilesCard";
import { useAppStore } from "../store/useAppStore";
export const DashboardPage = () => {
const setLoopForm = useAppStore((s) => s.setLoopForm);
return (
<>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1.2fr_1fr]">
@@ -21,19 +16,8 @@ export const DashboardPage = () => {
</div>
</div>
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1.2fr_1fr]">
<div className="grid grid-cols-1 gap-6">
<LogsPanel />
<div className="space-y-4">
<ProfilesCard
onApply={(profile) => {
setLoopForm({
allowIp: profile.allowIp,
delayMs: profile.delayMs,
targetLoops: profile.targetLoops,
});
}}
/>
</div>
</div>
</>
);