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:
@@ -1,49 +1,176 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useAppStore } from "../../store/useAppStore";
|
import { useAppStore } from "../../store/useAppStore";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card";
|
import { Card, CardContent, CardHeader, CardTitle } from "../ui/Card";
|
||||||
import { Input } from "../ui/Input";
|
import { Input } from "../ui/Input";
|
||||||
import { Button } from "../ui/Button";
|
import { Button } from "../ui/Button";
|
||||||
import { api } from "../../api/client";
|
import { api } from "../../api/client";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 = () => {
|
export const LoopSetupCard = () => {
|
||||||
const selectedHash = useAppStore((s) => s.selectedHash);
|
const selectedHash = useAppStore((s) => s.selectedHash);
|
||||||
const jobs = useAppStore((s) => s.jobs);
|
|
||||||
const loopForm = useAppStore((s) => s.loopForm);
|
const loopForm = useAppStore((s) => s.loopForm);
|
||||||
const setLoopForm = useAppStore((s) => s.setLoopForm);
|
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 () => {
|
const loadProfiles = async () => {
|
||||||
if (!selectedHash) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
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 applyProfile = async (profile: Profile) => {
|
||||||
|
if (!selectedHash) {
|
||||||
|
pushAlert({
|
||||||
|
title: "Torrent seçilmedi",
|
||||||
|
description: "Önce bir torrent seçin.",
|
||||||
|
variant: "warn",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
await api.post("/api/loop/start", {
|
await api.post("/api/loop/start", {
|
||||||
hash: selectedHash,
|
hash: selectedHash,
|
||||||
allowIp: loopForm.allowIp,
|
allowIp: profile.allowIp,
|
||||||
targetLoops: loopForm.targetLoops,
|
targetLoops: profile.targetLoops,
|
||||||
delayMs: loopForm.delayMs,
|
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 stopLoop = async () => {
|
const removeProfile = async (profileId: string) => {
|
||||||
if (!selectedHash) {
|
try {
|
||||||
return;
|
await api.delete(`/api/profiles/${profileId}`);
|
||||||
}
|
setProfiles((prev) => prev.filter((profile) => profile.id !== profileId));
|
||||||
await api.post("/api/loop/stop-by-hash", { hash: selectedHash });
|
pushAlert({
|
||||||
};
|
title: "Setup silindi",
|
||||||
|
description: "Setup listeden kaldırıldı.",
|
||||||
const runDry = async () => {
|
variant: "success",
|
||||||
if (!selectedHash) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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));
|
} catch (error) {
|
||||||
|
pushAlert({
|
||||||
|
title: "Silme başarısız",
|
||||||
|
description: "Setup silinemedi.",
|
||||||
|
variant: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,55 +183,109 @@ export const LoopSetupCard = () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid gap-3 text-sm">
|
<div className="grid gap-3 text-sm">
|
||||||
<label className="space-y-2">
|
<div className="grid gap-2">
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon={faLock} className="text-slate-400" />
|
|
||||||
Allow IP
|
|
||||||
</span>
|
|
||||||
<Input
|
<Input
|
||||||
value={loopForm.allowIp}
|
placeholder="Preset name"
|
||||||
onChange={(e) => setLoopForm({ allowIp: e.target.value })}
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</label>
|
<Input
|
||||||
<label className="space-y-2">
|
placeholder="Allow IP"
|
||||||
<span className="flex items-center gap-2">
|
value={allowIp}
|
||||||
<FontAwesomeIcon icon={faList} className="text-slate-400" />
|
onChange={(e) => {
|
||||||
Loops
|
setAllowIp(e.target.value);
|
||||||
</span>
|
setLoopForm({ allowIp: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={loopForm.targetLoops}
|
placeholder="Delay (ms)"
|
||||||
onChange={(e) => setLoopForm({ targetLoops: Number(e.target.value) })}
|
value={delayMs}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
setDelayMs(value);
|
||||||
|
setLoopForm({ delayMs: 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
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={loopForm.delayMs}
|
placeholder="Loops"
|
||||||
onChange={(e) => setLoopForm({ delayMs: Number(e.target.value) })}
|
value={targetLoops}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = Number(e.target.value);
|
||||||
|
setTargetLoops(value);
|
||||||
|
setLoopForm({ targetLoops: value });
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</div>
|
||||||
<div className="flex gap-2">
|
<Button onClick={saveProfile} className="w-full">
|
||||||
<Button onClick={startLoop} disabled={!selectedHash || !loopForm.allowIp}>
|
Save Setup
|
||||||
Start
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" onClick={stopLoop} disabled={!selectedHash}>
|
|
||||||
Stop
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" onClick={runDry} disabled={!selectedHash}>
|
|
||||||
Dry Run
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{dryRun && (
|
|
||||||
<pre className="max-h-40 overflow-auto rounded-md bg-slate-900 p-3 text-xs text-slate-100">
|
<div className="border-t border-slate-200 pt-3">
|
||||||
{dryRun}
|
<div className="space-y-2">
|
||||||
</pre>
|
{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>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ import { TorrentDetailsCard } from "../components/torrents/TorrentDetailsCard";
|
|||||||
import { LoopSetupCard } from "../components/loop/LoopSetupCard";
|
import { LoopSetupCard } from "../components/loop/LoopSetupCard";
|
||||||
import { LoopStatsCard } from "../components/loop/LoopStatsCard";
|
import { LoopStatsCard } from "../components/loop/LoopStatsCard";
|
||||||
import { LogsPanel } from "../components/loop/LogsPanel";
|
import { LogsPanel } from "../components/loop/LogsPanel";
|
||||||
import { ProfilesCard } from "../components/loop/ProfilesCard";
|
|
||||||
import { useAppStore } from "../store/useAppStore";
|
|
||||||
|
|
||||||
export const DashboardPage = () => {
|
export const DashboardPage = () => {
|
||||||
const setLoopForm = useAppStore((s) => s.setLoopForm);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1.2fr_1fr]">
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1.2fr_1fr]">
|
||||||
@@ -21,19 +16,8 @@ export const DashboardPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
</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 />
|
<LogsPanel />
|
||||||
<div className="space-y-4">
|
|
||||||
<ProfilesCard
|
|
||||||
onApply={(profile) => {
|
|
||||||
setLoopForm({
|
|
||||||
allowIp: profile.allowIp,
|
|
||||||
delayMs: profile.delayMs,
|
|
||||||
targetLoops: profile.targetLoops,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user