From 2ff3fb6ee6fb129fbf08296d329ca55e50aa86da Mon Sep 17 00:00:00 2001 From: wisecolt Date: Mon, 19 Jan 2026 17:08:50 +0300 Subject: [PATCH] =?UTF-8?q?feat(deployments):=20d=C3=BCzenleme=20modal?= =?UTF-8?q?=C4=B1=20ve=20deploy=20mesaj=C4=B1=20deste=C4=9Fi=20ekle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deployment detay sayfasında düzenleme modalı eklendi. Repo URL, branch, compose dosyası ve environment değişkenleri inline düzenlenebilir hale getirildi. Deploy tetikleme işlemi için özel mesaj parametresi desteği eklendi. Düzenleme sonrası otomatik deploy tetikleme özelliği aktif edildi. --- backend/src/routes/deployments.ts | 4 +- frontend/src/api/deployments.ts | 4 +- frontend/src/pages/DeploymentDetailPage.tsx | 426 +++++++++++++++++++- frontend/src/pages/DeploymentsPage.tsx | 10 +- 4 files changed, 434 insertions(+), 10 deletions(-) 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ı."} +
+
+ +
+
+ + +
+