feat(deployments): environment variable desteği ekle
Deployment projelerine environment variable konfigürasyonu eklendi. Backend tarafında DeploymentProject modeline envContent ve envExampleName alanları eklendi. Repo içindeki .env.example dosyalarını listelemek için yeni bir endpoint eklendi. Deployment sürecinde belirlenen env içeriği .proje dizinine .env dosyası olarak yazılıyor. Frontend tarafında deployment formuna "Genel" ve "Environment" sekmeleri eklendi. Remote repodan .env.example dosyaları çekilebiliyor ve içerik düzenlenebiliyor. Env içeriği için göster/gizle toggle'ı eklendi.
This commit is contained in:
@@ -13,6 +13,8 @@ export interface DeploymentProjectDocument extends Document {
|
|||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
env: DeploymentEnv;
|
env: DeploymentEnv;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
envContent?: string;
|
||||||
|
envExampleName?: string;
|
||||||
lastDeployAt?: Date;
|
lastDeployAt?: Date;
|
||||||
lastStatus: DeploymentStatus;
|
lastStatus: DeploymentStatus;
|
||||||
lastMessage?: string;
|
lastMessage?: string;
|
||||||
@@ -34,6 +36,8 @@ const DeploymentProjectSchema = new Schema<DeploymentProjectDocument>(
|
|||||||
webhookToken: { type: String, required: true, unique: true, index: true },
|
webhookToken: { type: String, required: true, unique: true, index: true },
|
||||||
env: { type: String, required: true, enum: ["dev", "prod"] },
|
env: { type: String, required: true, enum: ["dev", "prod"] },
|
||||||
port: { type: Number },
|
port: { type: Number },
|
||||||
|
envContent: { type: String },
|
||||||
|
envExampleName: { type: String },
|
||||||
lastDeployAt: { type: Date },
|
lastDeployAt: { type: Date },
|
||||||
lastStatus: { type: String, enum: ["idle", "running", "success", "failed"], default: "idle" },
|
lastStatus: { type: String, enum: ["idle", "running", "success", "failed"], default: "idle" },
|
||||||
lastMessage: { type: String }
|
lastMessage: { type: String }
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { authMiddleware } from "../middleware/authMiddleware.js";
|
import { authMiddleware } from "../middleware/authMiddleware.js";
|
||||||
import { deploymentService } from "../services/deploymentService.js";
|
import { deploymentService } from "../services/deploymentService.js";
|
||||||
@@ -71,6 +70,22 @@ router.get("/compose-files", async (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/env-examples", async (req, res) => {
|
||||||
|
authMiddleware(req, res, async () => {
|
||||||
|
const repoUrl = req.query.repoUrl as string | undefined;
|
||||||
|
const branch = req.query.branch as string | undefined;
|
||||||
|
if (!repoUrl || !branch) {
|
||||||
|
return res.status(400).json({ message: "repoUrl ve branch gerekli" });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const examples = await deploymentService.listRemoteEnvExamples(repoUrl, branch);
|
||||||
|
return res.json({ examples });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(400).json({ message: "Env example alınamadı", error: (err as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/metrics/summary", async (req, res) => {
|
router.get("/metrics/summary", async (req, res) => {
|
||||||
authMiddleware(req, res, async () => {
|
authMiddleware(req, res, async () => {
|
||||||
const since = new Date();
|
const since = new Date();
|
||||||
@@ -129,7 +144,7 @@ router.get("/:id", async (req, res) => {
|
|||||||
|
|
||||||
router.post("/", async (req, res) => {
|
router.post("/", async (req, res) => {
|
||||||
authMiddleware(req, res, async () => {
|
authMiddleware(req, res, async () => {
|
||||||
const { name, repoUrl, branch, composeFile, port } = req.body;
|
const { name, repoUrl, branch, composeFile, port, envContent, envExampleName } = req.body;
|
||||||
if (!name || !repoUrl || !branch || !composeFile) {
|
if (!name || !repoUrl || !branch || !composeFile) {
|
||||||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||||||
}
|
}
|
||||||
@@ -139,7 +154,9 @@ router.post("/", async (req, res) => {
|
|||||||
repoUrl,
|
repoUrl,
|
||||||
branch,
|
branch,
|
||||||
composeFile,
|
composeFile,
|
||||||
port
|
port,
|
||||||
|
envContent,
|
||||||
|
envExampleName
|
||||||
});
|
});
|
||||||
deploymentService
|
deploymentService
|
||||||
.runDeployment(created._id.toString(), { message: "First deployment" })
|
.runDeployment(created._id.toString(), { message: "First deployment" })
|
||||||
@@ -154,7 +171,7 @@ router.post("/", async (req, res) => {
|
|||||||
router.put("/:id", async (req, res) => {
|
router.put("/:id", async (req, res) => {
|
||||||
authMiddleware(req, res, async () => {
|
authMiddleware(req, res, async () => {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { name, repoUrl, branch, composeFile, port } = req.body;
|
const { name, repoUrl, branch, composeFile, port, envContent, envExampleName } = req.body;
|
||||||
if (!name || !repoUrl || !branch || !composeFile) {
|
if (!name || !repoUrl || !branch || !composeFile) {
|
||||||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||||||
}
|
}
|
||||||
@@ -164,7 +181,9 @@ router.put("/:id", async (req, res) => {
|
|||||||
repoUrl,
|
repoUrl,
|
||||||
branch,
|
branch,
|
||||||
composeFile,
|
composeFile,
|
||||||
port
|
port,
|
||||||
|
envContent,
|
||||||
|
envExampleName
|
||||||
});
|
});
|
||||||
if (!updated) return res.status(404).json({ message: "Deployment bulunamadı" });
|
if (!updated) return res.status(404).json({ message: "Deployment bulunamadı" });
|
||||||
return res.json(updated);
|
return res.json(updated);
|
||||||
|
|||||||
@@ -218,6 +218,32 @@ class DeploymentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listRemoteEnvExamples(repoUrl: string, branch: string) {
|
||||||
|
await fs.promises.mkdir(deploymentsRoot, { recursive: true });
|
||||||
|
const tmpBase = await fs.promises.mkdtemp(path.join(deploymentsRoot, ".tmp-"));
|
||||||
|
try {
|
||||||
|
await runCommand(
|
||||||
|
`git clone --depth 1 --single-branch --branch ${branch} ${repoUrl} ${tmpBase}`,
|
||||||
|
process.cwd(),
|
||||||
|
() => undefined
|
||||||
|
);
|
||||||
|
const entries = await fs.promises.readdir(tmpBase, { withFileTypes: true });
|
||||||
|
const files = entries
|
||||||
|
.filter((entry) => entry.isFile())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.filter((name) => name.toLowerCase().endsWith(".env.example"));
|
||||||
|
const items = await Promise.all(
|
||||||
|
files.map(async (name) => ({
|
||||||
|
name,
|
||||||
|
content: await fs.promises.readFile(path.join(tmpBase, name), "utf8")
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return items;
|
||||||
|
} finally {
|
||||||
|
await fs.promises.rm(tmpBase, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async ensureSettings() {
|
async ensureSettings() {
|
||||||
const existing = await Settings.findOne();
|
const existing = await Settings.findOne();
|
||||||
if (existing) return existing;
|
if (existing) return existing;
|
||||||
@@ -248,6 +274,8 @@ class DeploymentService {
|
|||||||
branch: string;
|
branch: string;
|
||||||
composeFile: ComposeFile;
|
composeFile: ComposeFile;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
envContent?: string;
|
||||||
|
envExampleName?: string;
|
||||||
}) {
|
}) {
|
||||||
const repoUrl = normalizeRepoUrl(input.repoUrl);
|
const repoUrl = normalizeRepoUrl(input.repoUrl);
|
||||||
const existingRepo = await DeploymentProject.findOne({ repoUrl });
|
const existingRepo = await DeploymentProject.findOne({ repoUrl });
|
||||||
@@ -281,7 +309,9 @@ class DeploymentService {
|
|||||||
composeFile: input.composeFile,
|
composeFile: input.composeFile,
|
||||||
webhookToken,
|
webhookToken,
|
||||||
env,
|
env,
|
||||||
port: input.port
|
port: input.port,
|
||||||
|
envContent: input.envContent,
|
||||||
|
envExampleName: input.envExampleName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,6 +323,8 @@ class DeploymentService {
|
|||||||
branch: string;
|
branch: string;
|
||||||
composeFile: ComposeFile;
|
composeFile: ComposeFile;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
envContent?: string;
|
||||||
|
envExampleName?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const project = await DeploymentProject.findById(id);
|
const project = await DeploymentProject.findById(id);
|
||||||
@@ -317,7 +349,9 @@ class DeploymentService {
|
|||||||
branch: input.branch,
|
branch: input.branch,
|
||||||
composeFile: input.composeFile,
|
composeFile: input.composeFile,
|
||||||
env,
|
env,
|
||||||
port: input.port
|
port: input.port,
|
||||||
|
envContent: input.envContent,
|
||||||
|
envExampleName: input.envExampleName
|
||||||
},
|
},
|
||||||
{ new: true, runValidators: true }
|
{ new: true, runValidators: true }
|
||||||
);
|
);
|
||||||
@@ -362,6 +396,10 @@ class DeploymentService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureRepo(project, (line) => pushLog(line));
|
await ensureRepo(project, (line) => pushLog(line));
|
||||||
|
if (project.envContent) {
|
||||||
|
await fs.promises.writeFile(path.join(project.rootPath, ".env"), project.envContent, "utf8");
|
||||||
|
pushLog(".env güncellendi");
|
||||||
|
}
|
||||||
pushLog("Deploy komutları çalıştırılıyor...");
|
pushLog("Deploy komutları çalıştırılıyor...");
|
||||||
await runCompose(project, (line) => pushLog(line));
|
await runCompose(project, (line) => pushLog(line));
|
||||||
const duration = Date.now() - startedAt;
|
const duration = Date.now() - startedAt;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||||
"@radix-ui/react-select": "^2.2.6",
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
"axios": "^1.5.1",
|
"axios": "^1.5.1",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export interface DeploymentProject {
|
|||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
env: DeploymentEnv;
|
env: DeploymentEnv;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
envContent?: string;
|
||||||
|
envExampleName?: string;
|
||||||
lastDeployAt?: string;
|
lastDeployAt?: string;
|
||||||
lastStatus: DeploymentStatus;
|
lastStatus: DeploymentStatus;
|
||||||
lastMessage?: string;
|
lastMessage?: string;
|
||||||
@@ -60,6 +62,8 @@ export interface DeploymentInput {
|
|||||||
branch: string;
|
branch: string;
|
||||||
composeFile: ComposeFile;
|
composeFile: ComposeFile;
|
||||||
port?: number;
|
port?: number;
|
||||||
|
envContent?: string;
|
||||||
|
envExampleName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDeployments(): Promise<DeploymentProject[]> {
|
export async function fetchDeployments(): Promise<DeploymentProject[]> {
|
||||||
@@ -111,3 +115,13 @@ export async function fetchDeploymentComposeFiles(
|
|||||||
});
|
});
|
||||||
return (data as { files: ComposeFile[] }).files;
|
return (data as { files: ComposeFile[] }).files;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchDeploymentEnvExamples(
|
||||||
|
repoUrl: string,
|
||||||
|
branch: string
|
||||||
|
): Promise<Array<{ name: string; content: string }>> {
|
||||||
|
const { data } = await apiClient.get("/deployments/env-examples", {
|
||||||
|
params: { repoUrl, branch }
|
||||||
|
});
|
||||||
|
return (data as { examples: Array<{ name: string; content: string }> }).examples;
|
||||||
|
}
|
||||||
|
|||||||
49
frontend/src/components/ui/tabs.tsx
Normal file
49
frontend/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-lg bg-muted/50 p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 ring-offset-background focus-visible:outline-none", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState, type CSSProperties } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import {
|
import {
|
||||||
faCloudArrowUp,
|
faCloudArrowUp,
|
||||||
|
faEye,
|
||||||
|
faEyeSlash,
|
||||||
faPlus,
|
faPlus,
|
||||||
faRotate,
|
faRotate,
|
||||||
faRocket
|
faRocket
|
||||||
@@ -13,6 +15,7 @@ import { Button } from "../components/ui/button";
|
|||||||
import { Input } from "../components/ui/input";
|
import { Input } from "../components/ui/input";
|
||||||
import { Label } from "../components/ui/label";
|
import { Label } from "../components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs";
|
||||||
import {
|
import {
|
||||||
createDeployment,
|
createDeployment,
|
||||||
deleteDeployment,
|
deleteDeployment,
|
||||||
@@ -20,6 +23,7 @@ import {
|
|||||||
DeploymentProject,
|
DeploymentProject,
|
||||||
fetchDeploymentComposeFiles,
|
fetchDeploymentComposeFiles,
|
||||||
fetchDeploymentBranches,
|
fetchDeploymentBranches,
|
||||||
|
fetchDeploymentEnvExamples,
|
||||||
fetchDeployments,
|
fetchDeployments,
|
||||||
runDeployment,
|
runDeployment,
|
||||||
updateDeployment
|
updateDeployment
|
||||||
@@ -36,6 +40,8 @@ type FormState = {
|
|||||||
port: string;
|
port: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EnvExample = { name: string; content: string };
|
||||||
|
|
||||||
const defaultForm: FormState = {
|
const defaultForm: FormState = {
|
||||||
name: "",
|
name: "",
|
||||||
repoUrl: "",
|
repoUrl: "",
|
||||||
@@ -59,6 +65,12 @@ export function DeploymentsPage() {
|
|||||||
const [branchLoading, setBranchLoading] = useState(false);
|
const [branchLoading, setBranchLoading] = useState(false);
|
||||||
const [composeOptions, setComposeOptions] = useState<DeploymentInput["composeFile"][]>([]);
|
const [composeOptions, setComposeOptions] = useState<DeploymentInput["composeFile"][]>([]);
|
||||||
const [composeLoading, setComposeLoading] = useState(false);
|
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 [faviconErrors, setFaviconErrors] = useState<Record<string, boolean>>({});
|
const [faviconErrors, setFaviconErrors] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
const isEdit = useMemo(() => !!form._id, [form._id]);
|
const isEdit = useMemo(() => !!form._id, [form._id]);
|
||||||
@@ -115,6 +127,11 @@ export function DeploymentsPage() {
|
|||||||
const repoUrl = form.repoUrl.trim();
|
const repoUrl = form.repoUrl.trim();
|
||||||
const branch = form.branch.trim();
|
const branch = form.branch.trim();
|
||||||
if (!repoUrl || !branch) {
|
if (!repoUrl || !branch) {
|
||||||
|
setEnvExamples([]);
|
||||||
|
setEnvExampleName("");
|
||||||
|
if (!isEdit) {
|
||||||
|
setEnvContent("");
|
||||||
|
}
|
||||||
setComposeOptions([]);
|
setComposeOptions([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,6 +152,38 @@ export function DeploymentsPage() {
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [form.repoUrl, form.branch, form.composeFile]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
const state = location.state as { editDeploymentId?: string } | null;
|
const state = location.state as { editDeploymentId?: string } | null;
|
||||||
if (state?.editDeploymentId) {
|
if (state?.editDeploymentId) {
|
||||||
@@ -156,6 +205,11 @@ export function DeploymentsPage() {
|
|||||||
setForm(defaultForm);
|
setForm(defaultForm);
|
||||||
setBranchOptions([]);
|
setBranchOptions([]);
|
||||||
setComposeOptions([]);
|
setComposeOptions([]);
|
||||||
|
setEnvExamples([]);
|
||||||
|
setEnvContent("");
|
||||||
|
setEnvExampleName("");
|
||||||
|
setShowEnv(false);
|
||||||
|
setActiveTab("details");
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -169,6 +223,10 @@ export function DeploymentsPage() {
|
|||||||
composeFile,
|
composeFile,
|
||||||
port: port ? String(port) : ""
|
port: port ? String(port) : ""
|
||||||
});
|
});
|
||||||
|
setEnvContent(deployment.envContent || "");
|
||||||
|
setEnvExampleName(deployment.envExampleName || "");
|
||||||
|
setShowEnv(false);
|
||||||
|
setActiveTab("details");
|
||||||
setModalOpen(true);
|
setModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,7 +242,9 @@ export function DeploymentsPage() {
|
|||||||
repoUrl: form.repoUrl,
|
repoUrl: form.repoUrl,
|
||||||
branch: form.branch,
|
branch: form.branch,
|
||||||
composeFile: form.composeFile,
|
composeFile: form.composeFile,
|
||||||
port: form.port ? Number(form.port) : undefined
|
port: form.port ? Number(form.port) : undefined,
|
||||||
|
envContent: envContent.trim() ? envContent : undefined,
|
||||||
|
envExampleName: envExampleName || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!payload.name || !payload.repoUrl || !payload.branch || !payload.composeFile) {
|
if (!payload.name || !payload.repoUrl || !payload.branch || !payload.composeFile) {
|
||||||
@@ -199,7 +259,9 @@ export function DeploymentsPage() {
|
|||||||
repoUrl: payload.repoUrl,
|
repoUrl: payload.repoUrl,
|
||||||
branch: payload.branch,
|
branch: payload.branch,
|
||||||
composeFile: payload.composeFile,
|
composeFile: payload.composeFile,
|
||||||
port: payload.port
|
port: payload.port,
|
||||||
|
envContent: payload.envContent,
|
||||||
|
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)));
|
||||||
toast.success("Deployment güncellendi");
|
toast.success("Deployment güncellendi");
|
||||||
@@ -388,116 +450,190 @@ export function DeploymentsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-5 py-4">
|
<div className="max-h-[70vh] overflow-y-auto px-5 py-4">
|
||||||
{!isEdit && (
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||||
<div className="text-xs text-muted-foreground">
|
<TabsList>
|
||||||
Repo URL girildiğinde branch ve compose dosyaları listelenir.
|
<TabsTrigger value="details">Genel</TabsTrigger>
|
||||||
</div>
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
)}
|
</TabsList>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<TabsContent value="details" className="space-y-4">
|
||||||
<Label htmlFor="repo">Repo URL</Label>
|
{!isEdit && (
|
||||||
<Input
|
<div className="text-xs text-muted-foreground">
|
||||||
id="repo"
|
Repo URL girildiğinde branch ve compose dosyaları listelenir.
|
||||||
value={form.repoUrl}
|
</div>
|
||||||
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">
|
||||||
<div className="space-y-2">
|
<Label htmlFor="repo">Repo URL</Label>
|
||||||
<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
|
<Input
|
||||||
id="branch"
|
id="repo"
|
||||||
value={form.branch}
|
value={form.repoUrl}
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, branch: e.target.value }))}
|
onChange={(e) => setForm((prev) => ({ ...prev, repoUrl: e.target.value }))}
|
||||||
placeholder="main"
|
placeholder="https://gitea.example.com/org/repo"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<div className="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>
|
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Compose Dosyası</Label>
|
<Label htmlFor="name">Deployment Name</Label>
|
||||||
<Select
|
<Input
|
||||||
value={form.composeFile}
|
id="name"
|
||||||
onValueChange={(value) =>
|
value={form.name}
|
||||||
setForm((prev) => ({ ...prev, composeFile: value as DeploymentInput["composeFile"] }))
|
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||||
}
|
placeholder="wisecolt-app"
|
||||||
>
|
required
|
||||||
<SelectTrigger>
|
/>
|
||||||
<SelectValue placeholder="Compose seçin" />
|
</div>
|
||||||
</SelectTrigger>
|
<div className="space-y-2">
|
||||||
<SelectContent>
|
<Label htmlFor="branch">Branch</Label>
|
||||||
{(composeOptions.length > 0
|
{branchOptions.length > 0 ? (
|
||||||
? composeOptions
|
<Select
|
||||||
: ["docker-compose.yml", "docker-compose.dev.yml"]
|
value={form.branch}
|
||||||
).map((file) => (
|
onValueChange={(value) => setForm((prev) => ({ ...prev, branch: value }))}
|
||||||
<SelectItem key={file} value={file}>
|
>
|
||||||
{file}
|
<SelectTrigger>
|
||||||
</SelectItem>
|
<SelectValue placeholder="Branch seçin" />
|
||||||
))}
|
</SelectTrigger>
|
||||||
</SelectContent>
|
<SelectContent>
|
||||||
</Select>
|
{branchOptions.map((branch) => (
|
||||||
<div className="text-xs text-muted-foreground">
|
<SelectItem key={branch} value={branch}>
|
||||||
{composeLoading
|
{branch}
|
||||||
? "Compose dosyaları alınıyor..."
|
</SelectItem>
|
||||||
: composeOptions.length > 0
|
))}
|
||||||
? "Repo üzerindeki compose dosyaları listelendi."
|
</SelectContent>
|
||||||
: "Repo URL ve branch sonrası compose dosyaları listelenir."}
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
id="branch"
|
||||||
|
value={form.branch}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, branch: e.target.value }))}
|
||||||
|
placeholder="main"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<Label htmlFor="port">Port (opsiyonel)</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label>Compose Dosyası</Label>
|
||||||
id="port"
|
<Select
|
||||||
type="number"
|
value={form.composeFile}
|
||||||
min={1}
|
onValueChange={(value) =>
|
||||||
value={form.port}
|
setForm((prev) => ({
|
||||||
onChange={(e) => setForm((prev) => ({ ...prev, port: e.target.value }))}
|
...prev,
|
||||||
placeholder="3000"
|
composeFile: value as DeploymentInput["composeFile"]
|
||||||
/>
|
}))
|
||||||
</div>
|
}
|
||||||
</div>
|
>
|
||||||
|
<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="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="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="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>
|
||||||
|
|
||||||
|
<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="min-h-[180px] w-full resize-y 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="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>
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
|
<div className="flex items-center justify-end gap-3 border-t border-border px-5 py-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user