import { useEffect, useMemo, useState, type CSSProperties } from "react"; import { toast } from "sonner"; import { useLocation, useNavigate } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCloudArrowUp, faEye, faEyeSlash, faPenToSquare, faPlus, faRotate, faRocket } from "@fortawesome/free-solid-svg-icons"; import { Card, CardContent } 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"; import { createDeployment, deleteDeployment, DeploymentInput, DeploymentProject, fetchDeploymentComposeFiles, fetchDeploymentBranches, fetchDeploymentEnvExamples, fetchDeployments, restartDeployment, runDeployment, updateDeployment } from "../api/deployments"; import { JobStatusBadge } from "../components/JobStatusBadge"; import { useLiveData } from "../providers/live-provider"; type FormState = { _id?: string; name: string; repoUrl: string; branch: string; composeFile: DeploymentInput["composeFile"]; port: string; }; type EnvExample = { name: string; content: string }; const defaultForm: FormState = { name: "", repoUrl: "", branch: "main", composeFile: "docker-compose.yml", port: "" }; export function DeploymentsPage() { const navigate = useNavigate(); const location = useLocation(); const { deploymentStreams } = useLiveData(); const apiBase = (import.meta.env.VITE_API_URL || "").replace(/\/$/, ""); const [deployments, setDeployments] = useState([]); const [loading, setLoading] = useState(false); const [modalOpen, setModalOpen] = useState(false); const [saving, setSaving] = useState(false); const [form, setForm] = useState(defaultForm); const [pendingEditId, setPendingEditId] = useState(null); const [branchOptions, setBranchOptions] = useState([]); const [branchLoading, setBranchLoading] = useState(false); const [composeOptions, setComposeOptions] = useState([]); const [composeLoading, setComposeLoading] = useState(false); const [envExamples, setEnvExamples] = useState([]); const [envLoading, setEnvLoading] = useState(false); const [envContent, setEnvContent] = useState(""); const [envExampleName, setEnvExampleName] = useState(""); const [showEnv, setShowEnv] = useState(false); const [activeTab, setActiveTab] = useState("details"); const [faviconErrors, setFaviconErrors] = useState>({}); const isEdit = useMemo(() => !!form._id, [form._id]); const loadDeployments = async () => { setLoading(true); try { const data = await fetchDeployments(); setDeployments(data); } catch { toast.error("Deployment listesi alınamadı"); } finally { setLoading(false); } }; useEffect(() => { loadDeployments(); }, []); useEffect(() => { const repoUrl = form.repoUrl.trim(); if (!repoUrl) { setBranchOptions([]); setComposeOptions([]); return; } if (!form._id && !form.name) { const normalized = repoUrl.replace(/\/+$/, ""); const lastPart = normalized.split("/").pop() || ""; const name = lastPart.replace(/\.git$/i, ""); if (name) { setForm((prev) => ({ ...prev, name })); } } const timer = setTimeout(async () => { setBranchLoading(true); try { const branches = await fetchDeploymentBranches(repoUrl); setBranchOptions(branches); if (!form.branch && branches.length > 0) { setForm((prev) => ({ ...prev, branch: branches.includes("main") ? "main" : branches[0] })); } } catch { setBranchOptions([]); } finally { setBranchLoading(false); } }, 400); return () => clearTimeout(timer); }, [form.repoUrl, form.branch]); useEffect(() => { const repoUrl = form.repoUrl.trim(); const branch = form.branch.trim(); if (!repoUrl || !branch) { setEnvExamples([]); setEnvExampleName(""); if (!isEdit) { setEnvContent(""); } setComposeOptions([]); return; } const timer = setTimeout(async () => { setComposeLoading(true); try { const files = await fetchDeploymentComposeFiles(repoUrl, branch); setComposeOptions(files); if (files.length > 0 && !files.includes(form.composeFile)) { setForm((prev) => ({ ...prev, composeFile: files[0] })); } } catch { setComposeOptions([]); } finally { setComposeLoading(false); } }, 400); return () => clearTimeout(timer); }, [form.repoUrl, form.branch, form.composeFile]); useEffect(() => { const repoUrl = form.repoUrl.trim(); const branch = form.branch.trim(); if (!repoUrl || !branch) { return; } const timer = setTimeout(async () => { setEnvLoading(true); try { const examples = await fetchDeploymentEnvExamples(repoUrl, branch); setEnvExamples(examples); if (examples.length === 0) { if (!isEdit) { setEnvExampleName(""); setEnvContent(""); } return; } const selected = examples.find((example) => example.name === envExampleName) || examples[0]; if (!isEdit || !envContent) { setEnvExampleName(selected.name); setEnvContent(selected.content); } } catch { setEnvExamples([]); } finally { setEnvLoading(false); } }, 400); return () => clearTimeout(timer); }, [form.repoUrl, form.branch, envExampleName, isEdit]); useEffect(() => { const state = location.state as { editDeploymentId?: string } | null; if (state?.editDeploymentId) { setPendingEditId(state.editDeploymentId); navigate(location.pathname, { replace: true }); } }, [location.state, navigate, location.pathname]); useEffect(() => { if (!pendingEditId || deployments.length === 0) return; const deployment = deployments.find((d) => d._id === pendingEditId); if (deployment) { handleEdit(deployment); setPendingEditId(null); } }, [pendingEditId, deployments]); const handleOpenNew = async () => { setForm(defaultForm); setBranchOptions([]); setComposeOptions([]); setEnvExamples([]); setEnvContent(""); setEnvExampleName(""); setShowEnv(false); setActiveTab("details"); setModalOpen(true); }; const handleEdit = (deployment: DeploymentProject) => { const { _id, name, repoUrl, branch, composeFile, port } = deployment; setForm({ _id, name, repoUrl, branch, composeFile, port: port ? String(port) : "" }); setEnvContent(deployment.envContent || ""); setEnvExampleName(deployment.envExampleName || ""); setShowEnv(false); setActiveTab("details"); setModalOpen(true); }; const handleClose = () => { setModalOpen(false); }; const handleSave = async () => { setSaving(true); try { const payload: DeploymentInput = { name: form.name, repoUrl: form.repoUrl, branch: form.branch, composeFile: form.composeFile, port: form.port ? Number(form.port) : undefined, envContent: envContent.trim() ? envContent : undefined, envExampleName: envExampleName || undefined }; if (!payload.name || !payload.repoUrl || !payload.branch || !payload.composeFile) { toast.error("Tüm alanları doldurun"); setSaving(false); return; } if (isEdit && form._id) { const updated = await updateDeployment(form._id, { name: payload.name, repoUrl: payload.repoUrl, branch: payload.branch, composeFile: payload.composeFile, port: payload.port, envContent: payload.envContent, envExampleName: payload.envExampleName }); setDeployments((prev) => prev.map((d) => (d._id === updated._id ? updated : d))); try { await runDeployment(updated._id, "update deploy"); } catch { toast.error("Deploy tetiklenemedi"); } toast.success("Deployment güncellendi"); } else { const created = await createDeployment(payload); setDeployments((prev) => [created, ...prev]); toast.success("Deployment oluşturuldu"); } setModalOpen(false); } catch { toast.error("İşlem sırasında hata oluştu"); } finally { setSaving(false); } }; const handleRun = async (id: string) => { try { await runDeployment(id); toast.success("Deploy tetiklendi"); } catch { toast.error("Deploy tetiklenemedi"); } }; const handleRestart = async (id: string) => { try { await restartDeployment(id, "restart"); toast.success("Restart tetiklendi"); } catch { toast.error("Restart tetiklenemedi"); } }; const handleDelete = async (deployment: DeploymentProject) => { const ok = window.confirm("Bu deployment'ı silmek istediğinize emin misiniz?"); if (!ok) return; try { await deleteDeployment(deployment._id); setDeployments((prev) => prev.filter((d) => d._id !== deployment._id)); toast.success("Deployment silindi"); } catch { toast.error("Deployment silinemedi"); } }; const formatDate = (value?: string) => { if (!value) return "-"; return new Date(value).toLocaleString(); }; return ( <>

Deployments

{loading && (
Deployments yükleniyor...
)} {!loading && deployments.length === 0 && (
Henüz deployment eklenmemiş.
)} {deployments.map((deployment) => { const faviconUrl = apiBase ? `${apiBase}/deployments/${deployment._id}/favicon` : `/api/deployments/${deployment._id}/favicon`; return ( navigate(`/deployments/${deployment._id}`)} >
{!faviconErrors[deployment._id] ? ( {`${deployment.name} setFaviconErrors((prev) => ({ ...prev, [deployment._id]: true })) } /> ) : ( )}
{deployment.name}
{deployment.rootPath}
{deployment.env.toUpperCase()} {deployment.composeFile}
Repo: {deployment.repoUrl}
Branch: {deployment.branch}
Last Deploy: {formatDate(deployment.lastDeployAt)}
); })}
{modalOpen && (
{isEdit ? "Deployment Güncelle" : "Yeni Deployment"}
Repo ve branch seçimi sonrası webhook tetiklemeleriyle deploy yapılır.
Genel Environment {!isEdit && (
Repo URL girildiğinde branch ve compose dosyaları listelenir.
)}
setForm((prev) => ({ ...prev, repoUrl: e.target.value }))} placeholder="https://gitea.example.com/org/repo" required />
setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="wisecolt-app" required />
{branchOptions.length > 0 ? ( ) : ( setForm((prev) => ({ ...prev, branch: e.target.value }))} placeholder="main" required /> )}
{branchLoading ? "Branch listesi alınıyor..." : branchOptions.length > 0 ? "Repo üzerindeki branch'lar listelendi." : "Repo URL girildiğinde branch listesi otomatik gelir."}
{composeLoading ? "Compose dosyaları alınıyor..." : composeOptions.length > 0 ? "Repo üzerindeki compose dosyaları listelendi." : "Repo URL ve branch sonrası compose dosyaları listelenir."}
setForm((prev) => ({ ...prev, port: e.target.value }))} placeholder="3000" />
{envExamples.length > 0 ? ( ) : (
{envLoading ? "Env example dosyaları alınıyor..." : "Repo içinde .env.example bulunamadı."}
)}
{envExamples.length > 0 ? "Repo üzerindeki env example dosyaları listelendi." : envLoading ? "Env example dosyaları alınıyor..." : "Repo içinde .env.example bulunamadı."}