diff --git a/backend/src/routes/deployments.ts b/backend/src/routes/deployments.ts index 4e76288..2fb264e 100644 --- a/backend/src/routes/deployments.ts +++ b/backend/src/routes/deployments.ts @@ -215,8 +215,10 @@ router.post("/:id/run", async (req, res) => { const { id } = req.params; const project = await DeploymentProject.findById(id); if (!project) return res.status(404).json({ message: "Deployment bulunamadı" }); + const rawMessage = typeof req.body?.message === "string" ? req.body.message.trim() : ""; + const message = rawMessage || "Elle deploy tetikleme"; deploymentService - .runDeployment(id, { message: "Elle deploy tetikleme" }) + .runDeployment(id, { message }) .catch(() => undefined); return res.json({ queued: true }); }); diff --git a/frontend/src/api/deployments.ts b/frontend/src/api/deployments.ts index 32486e1..b8f61c3 100644 --- a/frontend/src/api/deployments.ts +++ b/frontend/src/api/deployments.ts @@ -92,8 +92,8 @@ export async function deleteDeployment(id: string): Promise { await apiClient.delete(`/deployments/${id}`); } -export async function runDeployment(id: string): Promise { - await apiClient.post(`/deployments/${id}/run`); +export async function runDeployment(id: string, message?: string): Promise { + await apiClient.post(`/deployments/${id}/run`, message ? { message } : {}); } export async function fetchDeployment(id: string): Promise { diff --git a/frontend/src/pages/DeploymentDetailPage.tsx b/frontend/src/pages/DeploymentDetailPage.tsx index deedbdd..f2ad46d 100644 --- a/frontend/src/pages/DeploymentDetailPage.tsx +++ b/frontend/src/pages/DeploymentDetailPage.tsx @@ -1,15 +1,47 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, type CSSProperties } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faArrowLeft, faCloudArrowUp, faCopy, faHistory } from "@fortawesome/free-solid-svg-icons"; +import { + faArrowLeft, + faCloudArrowUp, + faCopy, + faEye, + faEyeSlash, + faHistory +} from "@fortawesome/free-solid-svg-icons"; import { toast } from "sonner"; import { Button } from "../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; +import { Input } from "../components/ui/input"; import { JobStatusBadge } from "../components/JobStatusBadge"; -import { DeploymentProject, DeploymentRun, fetchDeployment, runDeployment } from "../api/deployments"; +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 { + DeploymentInput, + DeploymentProject, + DeploymentRun, + fetchDeployment, + fetchDeploymentBranches, + fetchDeploymentComposeFiles, + fetchDeploymentEnvExamples, + runDeployment, + updateDeployment +} from "../api/deployments"; import { useDeploymentStream } from "../providers/live-provider"; import { useSocket } from "../providers/socket-provider"; +type FormState = { + _id?: string; + name: string; + repoUrl: string; + branch: string; + composeFile: DeploymentInput["composeFile"]; + port: string; +}; + +type EnvExample = { name: string; content: string }; + export function DeploymentDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -17,8 +49,28 @@ export function DeploymentDetailPage() { const [runs, setRuns] = useState([]); const [loading, setLoading] = useState(true); const [triggering, setTriggering] = useState(false); + const [modalOpen, setModalOpen] = useState(false); + const [saving, setSaving] = useState(false); + const [form, setForm] = useState({ + name: "", + repoUrl: "", + branch: "main", + composeFile: "docker-compose.yml", + port: "" + }); + 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 stream = useDeploymentStream(id || ""); const socket = useSocket(); + const isEdit = useMemo(() => !!form._id, [form._id]); useEffect(() => { if (!id) return; @@ -117,6 +169,142 @@ export function DeploymentDetailPage() { } }; + useEffect(() => { + const repoUrl = form.repoUrl.trim(); + if (!repoUrl) { + setBranchOptions([]); + setComposeOptions([]); + return; + } + 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(""); + 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) { + 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, envContent]); + + const handleEdit = () => { + if (!project) return; + const { _id, name, repoUrl, branch, composeFile, port } = project; + setForm({ + _id, + name, + repoUrl, + branch, + composeFile, + port: port ? String(port) : "" + }); + setEnvContent(project.envContent || ""); + setEnvExampleName(project.envExampleName || ""); + setShowEnv(false); + setActiveTab("details"); + setModalOpen(true); + }; + + const handleClose = () => { + setModalOpen(false); + }; + + const handleSave = async () => { + if (!form._id) return; + 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; + } + + const updated = await updateDeployment(form._id, payload); + setProject(updated); + try { + await runDeployment(updated._id, "Update deploy"); + } catch { + toast.error("Deploy tetiklenemedi"); + } + toast.success("Deployment güncellendi"); + setModalOpen(false); + } catch { + toast.error("İşlem sırasında hata oluştu"); + } finally { + setSaving(false); + } + }; + if (loading) { return (
@@ -134,7 +322,8 @@ export function DeploymentDetailPage() { } return ( -
+ <> +
@@ -257,6 +446,231 @@ export function DeploymentDetailPage() {
-
+
+ + {modalOpen && ( +
+
+
+
+
Deployment Güncelle
+
+ 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ı."} +
+
+ +
+
+ + +
+