feat(deployments): düzenleme modalı ve deploy mesajı desteği ekle
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.
This commit is contained in:
@@ -215,8 +215,10 @@ router.post("/:id/run", async (req, res) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const project = await DeploymentProject.findById(id);
|
const project = await DeploymentProject.findById(id);
|
||||||
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
|
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
|
deploymentService
|
||||||
.runDeployment(id, { message: "Elle deploy tetikleme" })
|
.runDeployment(id, { message })
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
return res.json({ queued: true });
|
return res.json({ queued: true });
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ export async function deleteDeployment(id: string): Promise<void> {
|
|||||||
await apiClient.delete(`/deployments/${id}`);
|
await apiClient.delete(`/deployments/${id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runDeployment(id: string): Promise<void> {
|
export async function runDeployment(id: string, message?: string): Promise<void> {
|
||||||
await apiClient.post(`/deployments/${id}/run`);
|
await apiClient.post(`/deployments/${id}/run`, message ? { message } : {});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDeployment(id: string): Promise<DeploymentDetailResponse> {
|
export async function fetchDeployment(id: string): Promise<DeploymentDetailResponse> {
|
||||||
|
|||||||
@@ -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 { useNavigate, useParams } from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
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 { toast } from "sonner";
|
||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";
|
||||||
|
import { Input } from "../components/ui/input";
|
||||||
import { JobStatusBadge } from "../components/JobStatusBadge";
|
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 { useDeploymentStream } from "../providers/live-provider";
|
||||||
import { useSocket } from "../providers/socket-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() {
|
export function DeploymentDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -17,8 +49,28 @@ export function DeploymentDetailPage() {
|
|||||||
const [runs, setRuns] = useState<DeploymentRun[]>([]);
|
const [runs, setRuns] = useState<DeploymentRun[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [triggering, setTriggering] = useState(false);
|
const [triggering, setTriggering] = useState(false);
|
||||||
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [form, setForm] = useState<FormState>({
|
||||||
|
name: "",
|
||||||
|
repoUrl: "",
|
||||||
|
branch: "main",
|
||||||
|
composeFile: "docker-compose.yml",
|
||||||
|
port: ""
|
||||||
|
});
|
||||||
|
const [branchOptions, setBranchOptions] = useState<string[]>([]);
|
||||||
|
const [branchLoading, setBranchLoading] = useState(false);
|
||||||
|
const [composeOptions, setComposeOptions] = useState<DeploymentInput["composeFile"][]>([]);
|
||||||
|
const [composeLoading, setComposeLoading] = useState(false);
|
||||||
|
const [envExamples, setEnvExamples] = useState<EnvExample[]>([]);
|
||||||
|
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 stream = useDeploymentStream(id || "");
|
||||||
const socket = useSocket();
|
const socket = useSocket();
|
||||||
|
const isEdit = useMemo(() => !!form._id, [form._id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
<div className="rounded-md border border-border bg-muted/30 px-4 py-6 text-sm text-muted-foreground">
|
||||||
@@ -134,7 +322,8 @@ export function DeploymentDetailPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<>
|
||||||
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Button variant="ghost" size="icon" onClick={() => navigate("/deployments")}>
|
<Button variant="ghost" size="icon" onClick={() => navigate("/deployments")}>
|
||||||
@@ -148,7 +337,7 @@ export function DeploymentDetailPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => navigate("/deployments", { state: { editDeploymentId: project._id } })}
|
onClick={handleEdit}
|
||||||
>
|
>
|
||||||
Düzenle
|
Düzenle
|
||||||
</Button>
|
</Button>
|
||||||
@@ -257,6 +446,231 @@ export function DeploymentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{modalOpen && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
|
||||||
|
<div
|
||||||
|
className="flex w-full max-w-lg flex-col overflow-hidden rounded-lg border border-border bg-card card-shadow"
|
||||||
|
style={{ height: 620 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-lg font-semibold text-foreground">Deployment Güncelle</div>
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Repo ve branch seçimi sonrası webhook tetiklemeleriyle deploy yapılır.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="icon" onClick={handleClose}>
|
||||||
|
✕
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-hidden px-5 py-4">
|
||||||
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="details">Genel</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContent value="details" className="h-[420px] space-y-4">
|
||||||
|
{!isEdit && (
|
||||||
|
<div className="h-[1.25rem] text-xs text-muted-foreground">
|
||||||
|
Repo URL girildiğinde branch ve compose dosyaları listelenir.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="repo">Repo URL</Label>
|
||||||
|
<Input
|
||||||
|
id="repo"
|
||||||
|
value={form.repoUrl}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, repoUrl: e.target.value }))}
|
||||||
|
placeholder="https://gitea.example.com/org/repo"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="name">Deployment Name</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={form.name}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder="wisecolt-app"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="branch">Branch</Label>
|
||||||
|
{branchOptions.length > 0 ? (
|
||||||
|
<Select
|
||||||
|
value={form.branch}
|
||||||
|
onValueChange={(value) => setForm((prev) => ({ ...prev, branch: value }))}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Branch seçin" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{branchOptions.map((branch) => (
|
||||||
|
<SelectItem key={branch} value={branch}>
|
||||||
|
{branch}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id="branch"
|
||||||
|
value={form.branch}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, branch: e.target.value }))}
|
||||||
|
placeholder="main"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="h-[1.25rem] text-xs text-muted-foreground">
|
||||||
|
{branchLoading
|
||||||
|
? "Branch listesi alınıyor..."
|
||||||
|
: branchOptions.length > 0
|
||||||
|
? "Repo üzerindeki branch'lar listelendi."
|
||||||
|
: "Repo URL girildiğinde branch listesi otomatik gelir."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Compose Dosyası</Label>
|
||||||
|
<Select
|
||||||
|
value={form.composeFile}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
composeFile: value as DeploymentInput["composeFile"]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Compose seçin" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(composeOptions.length > 0
|
||||||
|
? composeOptions
|
||||||
|
: ["docker-compose.yml", "docker-compose.dev.yml"]
|
||||||
|
).map((file) => (
|
||||||
|
<SelectItem key={file} value={file}>
|
||||||
|
{file}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="h-[1.25rem] text-xs text-muted-foreground">
|
||||||
|
{composeLoading
|
||||||
|
? "Compose dosyaları alınıyor..."
|
||||||
|
: composeOptions.length > 0
|
||||||
|
? "Repo üzerindeki compose dosyaları listelendi."
|
||||||
|
: "Repo URL ve branch sonrası compose dosyaları listelenir."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="port">Port (opsiyonel)</Label>
|
||||||
|
<Input
|
||||||
|
id="port"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={form.port}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
|
||||||
|
placeholder="3000"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="environment" className="h-[420px] space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>.env.example</Label>
|
||||||
|
{envExamples.length > 0 ? (
|
||||||
|
<Select
|
||||||
|
value={envExampleName}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const example = envExamples.find((item) => item.name === value);
|
||||||
|
setEnvExampleName(value);
|
||||||
|
if (example) {
|
||||||
|
setEnvContent(example.content);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Env example seçin" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{envExamples.map((example) => (
|
||||||
|
<SelectItem key={example.name} value={example.name}>
|
||||||
|
{example.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<div className="h-[2.5rem] rounded-md border border-dashed border-border px-3 py-2 text-xs text-muted-foreground">
|
||||||
|
{envLoading
|
||||||
|
? "Env example dosyaları alınıyor..."
|
||||||
|
: "Repo içinde .env.example bulunamadı."}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="h-[1.25rem] text-xs text-muted-foreground">
|
||||||
|
{envExamples.length > 0
|
||||||
|
? "Repo üzerindeki env example dosyaları listelendi."
|
||||||
|
: envLoading
|
||||||
|
? "Env example dosyaları alınıyor..."
|
||||||
|
: "Repo içinde .env.example bulunamadı."}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="env-content">Environment</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setShowEnv((prev) => !prev)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={showEnv ? faEyeSlash : faEye} className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
id="env-content"
|
||||||
|
value={envContent}
|
||||||
|
onChange={(e) => setEnvContent(e.target.value)}
|
||||||
|
className="h-[180px] w-full resize-none rounded-md border border-input bg-background px-3 py-2 text-sm font-mono text-foreground shadow-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
style={
|
||||||
|
showEnv ? undefined : ({ WebkitTextSecurity: "disc" } as CSSProperties)
|
||||||
|
}
|
||||||
|
placeholder="ENV içerikleri burada listelenir."
|
||||||
|
/>
|
||||||
|
<div className="min-h-[1.25rem] text-xs text-muted-foreground">
|
||||||
|
Kaydedince içerik deployment kök dizinine{" "}
|
||||||
|
<span className="font-mono">.env</span> olarak yazılır.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
|
||||||
|
<Button variant="ghost" onClick={handleClose} disabled={saving}>
|
||||||
|
İptal
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSave} disabled={saving}>
|
||||||
|
{saving ? "Kaydediliyor..." : "Kaydet"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,6 +264,11 @@ export function DeploymentsPage() {
|
|||||||
envExampleName: payload.envExampleName
|
envExampleName: payload.envExampleName
|
||||||
});
|
});
|
||||||
setDeployments((prev) => prev.map((d) => (d._id === updated._id ? updated : d)));
|
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");
|
toast.success("Deployment güncellendi");
|
||||||
} else {
|
} else {
|
||||||
const created = await createDeployment(payload);
|
const created = await createDeployment(payload);
|
||||||
@@ -435,7 +440,10 @@ export function DeploymentsPage() {
|
|||||||
|
|
||||||
{modalOpen && (
|
{modalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4 py-8">
|
||||||
<div className="flex h-[620px] w-full max-w-lg flex-col overflow-hidden rounded-lg border border-border bg-card card-shadow">
|
<div
|
||||||
|
className="flex w-full max-w-lg flex-col overflow-hidden rounded-lg border border-border bg-card card-shadow"
|
||||||
|
style={{ height: 626 }}
|
||||||
|
>
|
||||||
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
<div className="flex items-center justify-between border-b border-border px-5 py-4">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="text-lg font-semibold text-foreground">
|
<div className="text-lg font-semibold text-foreground">
|
||||||
|
|||||||
Reference in New Issue
Block a user