feat(deployments): repo tabanlı kurulum sistemi ekle ve root taramayı kaldır
Root dizin taraması yerine repo URL tabanlı otomatik kurulum sistemine geçiş yapıldı.
Deploy klasörü artık repo URL'sinden otomatik oluşturuluyor. Remote repo
üzerinden branch ve compose dosyası listelemesi eklendi.
- `deploymentsRoot` konfigürasyonu kaldırıldı
- `/deployments/scan` endpoint'i kaldırıldı
- `/deployments/compose-files` endpoint'i eklendi
- `repoUrl` alanı unique ve index olarak işaretlendi
- Proje oluştururken `rootPath` zorunluluğu kaldırıldı
- Deploy klasörü otomatik `deployments/{slug}` formatında oluşturuluyor
- Frontend'de root tarama UI'ı kaldırıldı, compose dosyası listeleme eklendi
BREAKING CHANGE: Root dizin tarama özelliği ve `rootPath` alanı kaldırıldı.
Artık deploymentlar sadece repo URL ile oluşturulabiliyor.
This commit is contained in:
@@ -29,7 +29,7 @@
|
||||
- **Log Akışı**: Gerçek zamanlı test loglarının izlenmesi
|
||||
|
||||
### 🚀 Deployment Yönetimi
|
||||
- **Root Tarama**: `DEPLOYMENTS_ROOT_HOST` altında compose dosyası olan projeleri otomatik bulma
|
||||
- **Repo Bazlı Kurulum**: Repo URL ile proje oluşturma ve deploy klasörünü otomatik oluşturma
|
||||
- **Webhook Tetikleme**: Gitea push event ile otomatik deploy
|
||||
- **Branch Seçimi**: Repo URL girince branch listesi alınır ve seçim yapılır
|
||||
- **Deploy Geçmişi**: Her deploy için log ve süre kaydı
|
||||
@@ -223,8 +223,8 @@ docker compose up -d --build
|
||||
|
||||
### Deployment Yönetimi
|
||||
1. **Deployments** sayfasına gidin
|
||||
2. **New Deployment** ile root altında taranan projeyi seçin
|
||||
3. Repo URL + Branch + Compose dosyasını girin
|
||||
2. **New Deployment** ile Repo URL girin
|
||||
3. Branch ve Compose dosyasını seçin
|
||||
4. Kaydettikten sonra **Webhook URL**’i Gitea’da web istemci olarak tanımlayın
|
||||
|
||||
#### Webhook Ayarları (Gitea)
|
||||
@@ -251,7 +251,7 @@ docker compose up -d --build
|
||||
### 📖 API Referansı
|
||||
- **Authentication API'leri**: `/auth/login`, `/auth/me`
|
||||
- **Test Yönetim API'leri**: CRUD operasyonları, manuel çalıştırma
|
||||
- **Deployment API'leri**: `/deployments`, `/deployments/:id`, `/deployments/scan`, `/deployments/branches`
|
||||
- **Deployment API'leri**: `/deployments`, `/deployments/:id`, `/deployments/branches`, `/deployments/compose-files`
|
||||
- **Webhook Endpoint**: `/api/deployments/webhook/:token`
|
||||
- **WebSocket Olayları**: Real-time iletişim ve durum güncellemeleri
|
||||
- **Endpoint Detayları**: Her endpoint için istek/yanıt formatları
|
||||
|
||||
@@ -8,8 +8,7 @@ export const config = {
|
||||
adminUsername: process.env.ADMIN_USERNAME || "admin",
|
||||
adminPassword: process.env.ADMIN_PASSWORD || "password",
|
||||
jwtSecret: process.env.JWT_SECRET || "changeme",
|
||||
clientOrigin: process.env.CLIENT_ORIGIN || "http://localhost:5173",
|
||||
deploymentsRoot: "/workspace"
|
||||
clientOrigin: process.env.CLIENT_ORIGIN || "http://localhost:5173"
|
||||
};
|
||||
|
||||
if (!config.jwtSecret) {
|
||||
|
||||
@@ -24,7 +24,7 @@ const DeploymentProjectSchema = new Schema<DeploymentProjectDocument>(
|
||||
{
|
||||
name: { type: String, required: true, trim: true },
|
||||
rootPath: { type: String, required: true, trim: true },
|
||||
repoUrl: { type: String, required: true, trim: true },
|
||||
repoUrl: { type: String, required: true, trim: true, unique: true, index: true },
|
||||
branch: { type: String, required: true, trim: true },
|
||||
composeFile: {
|
||||
type: String,
|
||||
|
||||
@@ -39,17 +39,6 @@ router.get("/:id/favicon", async (req, res) => {
|
||||
return res.status(404).end();
|
||||
});
|
||||
|
||||
router.get("/scan", async (req, res) => {
|
||||
authMiddleware(req, res, async () => {
|
||||
try {
|
||||
const candidates = await deploymentService.scanRoot();
|
||||
return res.json(candidates);
|
||||
} catch (err) {
|
||||
return res.status(500).json({ message: "Root taraması yapılamadı" });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/branches", async (req, res) => {
|
||||
authMiddleware(req, res, async () => {
|
||||
const repoUrl = req.query.repoUrl as string | undefined;
|
||||
@@ -65,6 +54,22 @@ router.get("/branches", async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/compose-files", 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 files = await deploymentService.listRemoteComposeFiles(repoUrl, branch);
|
||||
return res.json({ files });
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: "Compose listesi alınamadı", error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/metrics/summary", async (req, res) => {
|
||||
authMiddleware(req, res, async () => {
|
||||
const since = new Date();
|
||||
@@ -123,14 +128,13 @@ router.get("/:id", async (req, res) => {
|
||||
|
||||
router.post("/", async (req, res) => {
|
||||
authMiddleware(req, res, async () => {
|
||||
const { name, rootPath, repoUrl, branch, composeFile, port } = req.body;
|
||||
if (!name || !rootPath || !repoUrl || !branch || !composeFile) {
|
||||
const { name, repoUrl, branch, composeFile, port } = req.body;
|
||||
if (!name || !repoUrl || !branch || !composeFile) {
|
||||
return res.status(400).json({ message: "Tüm alanlar gerekli" });
|
||||
}
|
||||
try {
|
||||
const created = await deploymentService.createProject({
|
||||
name,
|
||||
rootPath,
|
||||
repoUrl,
|
||||
branch,
|
||||
composeFile,
|
||||
|
||||
@@ -2,7 +2,6 @@ import fs from "fs";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
import { spawn } from "child_process";
|
||||
import { config } from "../config/env.js";
|
||||
import {
|
||||
DeploymentProject,
|
||||
DeploymentProjectDocument,
|
||||
@@ -14,14 +13,18 @@ import { Settings } from "../models/settings.js";
|
||||
|
||||
const composeFileCandidates: ComposeFile[] = ["docker-compose.yml", "docker-compose.dev.yml"];
|
||||
|
||||
function normalizeRoot(rootPath: string) {
|
||||
return path.resolve(rootPath);
|
||||
const deploymentsRoot = path.join(process.cwd(), "deployments");
|
||||
|
||||
function slugify(value: string) {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/\.git$/i, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
function isWithinRoot(rootPath: string, targetPath: string) {
|
||||
const resolvedRoot = normalizeRoot(rootPath);
|
||||
const resolvedTarget = path.resolve(targetPath);
|
||||
return resolvedTarget === resolvedRoot || resolvedTarget.startsWith(`${resolvedRoot}${path.sep}`);
|
||||
function normalizeRepoUrl(value: string) {
|
||||
return value.trim().replace(/\/+$/, "").replace(/\.git$/i, "");
|
||||
}
|
||||
|
||||
function generateWebhookToken() {
|
||||
@@ -132,10 +135,7 @@ async function ensureRepo(project: DeploymentProjectDocument, onData: (line: str
|
||||
}
|
||||
}
|
||||
|
||||
async function runCompose(
|
||||
project: DeploymentProjectDocument,
|
||||
onData: (line: string) => void
|
||||
) {
|
||||
async function runCompose(project: DeploymentProjectDocument, onData: (line: string) => void) {
|
||||
const composePath = path.join(project.rootPath, project.composeFile);
|
||||
if (!fs.existsSync(composePath)) {
|
||||
throw new Error("Compose dosyası bulunamadı");
|
||||
@@ -153,31 +153,6 @@ async function runCompose(
|
||||
class DeploymentService {
|
||||
private running: Map<string, boolean> = new Map();
|
||||
|
||||
async scanRoot() {
|
||||
const rootPath = normalizeRoot(config.deploymentsRoot);
|
||||
if (!fs.existsSync(rootPath)) {
|
||||
throw new Error("Deployments root bulunamadı");
|
||||
}
|
||||
const entries = await fs.promises.readdir(rootPath, { withFileTypes: true });
|
||||
const candidates = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
if (entry.name.startsWith(".")) continue;
|
||||
const folderPath = path.join(rootPath, entry.name);
|
||||
const available = composeFileCandidates.filter((file) =>
|
||||
fs.existsSync(path.join(folderPath, file))
|
||||
);
|
||||
if (available.length === 0) continue;
|
||||
candidates.push({
|
||||
name: entry.name,
|
||||
rootPath: folderPath,
|
||||
composeFiles: available
|
||||
});
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async listRemoteBranches(repoUrl: string) {
|
||||
const output = await runCommandCapture("git", ["ls-remote", "--heads", repoUrl], process.cwd());
|
||||
const branches = output
|
||||
@@ -190,6 +165,24 @@ class DeploymentService {
|
||||
return branches;
|
||||
}
|
||||
|
||||
async listRemoteComposeFiles(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 available = composeFileCandidates.filter((file) =>
|
||||
fs.existsSync(path.join(tmpBase, file))
|
||||
);
|
||||
return available;
|
||||
} finally {
|
||||
await fs.promises.rm(tmpBase, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async ensureSettings() {
|
||||
const existing = await Settings.findOne();
|
||||
if (existing) return existing;
|
||||
@@ -216,27 +209,15 @@ class DeploymentService {
|
||||
|
||||
async createProject(input: {
|
||||
name: string;
|
||||
rootPath: string;
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
composeFile: ComposeFile;
|
||||
port?: number;
|
||||
}) {
|
||||
const rootPath = path.resolve(input.rootPath);
|
||||
if (!isWithinRoot(config.deploymentsRoot, rootPath)) {
|
||||
throw new Error("Root path deployments root dışında");
|
||||
}
|
||||
if (!fs.existsSync(rootPath)) {
|
||||
throw new Error("Root path bulunamadı");
|
||||
}
|
||||
const composePath = path.join(rootPath, input.composeFile);
|
||||
if (!fs.existsSync(composePath)) {
|
||||
throw new Error("Compose dosyası bulunamadı");
|
||||
}
|
||||
|
||||
const existing = await DeploymentProject.findOne({ rootPath });
|
||||
if (existing) {
|
||||
throw new Error("Bu klasör zaten eklenmiş");
|
||||
const repoUrl = normalizeRepoUrl(input.repoUrl);
|
||||
const existingRepo = await DeploymentProject.findOne({ repoUrl });
|
||||
if (existingRepo) {
|
||||
throw new Error("Bu repo zaten eklenmiş");
|
||||
}
|
||||
|
||||
let webhookToken = generateWebhookToken();
|
||||
@@ -244,11 +225,23 @@ class DeploymentService {
|
||||
webhookToken = generateWebhookToken();
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(deploymentsRoot, { recursive: true });
|
||||
const baseName = slugify(path.basename(repoUrl));
|
||||
const suffix = crypto.randomBytes(3).toString("hex");
|
||||
const slug = baseName ? `${baseName}-${suffix}` : `deployment-${suffix}`;
|
||||
const rootPath = path.join(deploymentsRoot, slug);
|
||||
await fs.promises.mkdir(rootPath, { recursive: true });
|
||||
|
||||
const available = await this.listRemoteComposeFiles(repoUrl, input.branch);
|
||||
if (!available.includes(input.composeFile)) {
|
||||
throw new Error("Compose dosyası repoda bulunamadı");
|
||||
}
|
||||
|
||||
const env = deriveEnv(input.composeFile);
|
||||
return DeploymentProject.create({
|
||||
name: input.name,
|
||||
rootPath,
|
||||
repoUrl: input.repoUrl,
|
||||
repoUrl,
|
||||
branch: input.branch,
|
||||
composeFile: input.composeFile,
|
||||
webhookToken,
|
||||
@@ -269,16 +262,23 @@ class DeploymentService {
|
||||
) {
|
||||
const project = await DeploymentProject.findById(id);
|
||||
if (!project) return null;
|
||||
const composePath = path.join(project.rootPath, input.composeFile);
|
||||
if (!fs.existsSync(composePath)) {
|
||||
throw new Error("Compose dosyası bulunamadı");
|
||||
const repoUrl = normalizeRepoUrl(input.repoUrl);
|
||||
if (repoUrl !== project.repoUrl) {
|
||||
const existingRepo = await DeploymentProject.findOne({ repoUrl });
|
||||
if (existingRepo && existingRepo._id.toString() !== id) {
|
||||
throw new Error("Bu repo zaten eklenmiş");
|
||||
}
|
||||
}
|
||||
const available = await this.listRemoteComposeFiles(repoUrl, input.branch);
|
||||
if (!available.includes(input.composeFile)) {
|
||||
throw new Error("Compose dosyası repoda bulunamadı");
|
||||
}
|
||||
const env = deriveEnv(input.composeFile);
|
||||
const updated = await DeploymentProject.findByIdAndUpdate(
|
||||
id,
|
||||
{
|
||||
name: input.name,
|
||||
repoUrl: input.repoUrl,
|
||||
repoUrl,
|
||||
branch: input.branch,
|
||||
composeFile: input.composeFile,
|
||||
env,
|
||||
|
||||
@@ -54,15 +54,8 @@ export interface DeploymentMetrics {
|
||||
recentRuns: DeploymentRunWithProject[];
|
||||
}
|
||||
|
||||
export interface DeploymentCandidate {
|
||||
name: string;
|
||||
rootPath: string;
|
||||
composeFiles: ComposeFile[];
|
||||
}
|
||||
|
||||
export interface DeploymentInput {
|
||||
name: string;
|
||||
rootPath: string;
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
composeFile: ComposeFile;
|
||||
@@ -81,17 +74,12 @@ export async function fetchDeploymentBranches(repoUrl: string): Promise<string[]
|
||||
return (data as { branches: string[] }).branches;
|
||||
}
|
||||
|
||||
export async function scanDeployments(): Promise<DeploymentCandidate[]> {
|
||||
const { data } = await apiClient.get("/deployments/scan");
|
||||
return data as DeploymentCandidate[];
|
||||
}
|
||||
|
||||
export async function createDeployment(payload: DeploymentInput): Promise<DeploymentProject> {
|
||||
const { data } = await apiClient.post("/deployments", payload);
|
||||
return data as DeploymentProject;
|
||||
}
|
||||
|
||||
export async function updateDeployment(id: string, payload: Omit<DeploymentInput, "rootPath">) {
|
||||
export async function updateDeployment(id: string, payload: DeploymentInput) {
|
||||
const { data } = await apiClient.put(`/deployments/${id}`, payload);
|
||||
return data as DeploymentProject;
|
||||
}
|
||||
@@ -113,3 +101,13 @@ export async function fetchDeploymentMetrics(): Promise<DeploymentMetrics> {
|
||||
const { data } = await apiClient.get("/deployments/metrics/summary");
|
||||
return data as DeploymentMetrics;
|
||||
}
|
||||
|
||||
export async function fetchDeploymentComposeFiles(
|
||||
repoUrl: string,
|
||||
branch: string
|
||||
): Promise<ComposeFile[]> {
|
||||
const { data } = await apiClient.get("/deployments/compose-files", {
|
||||
params: { repoUrl, branch }
|
||||
});
|
||||
return (data as { files: ComposeFile[] }).files;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,12 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
||||
import {
|
||||
createDeployment,
|
||||
deleteDeployment,
|
||||
DeploymentCandidate,
|
||||
DeploymentInput,
|
||||
DeploymentProject,
|
||||
fetchDeploymentComposeFiles,
|
||||
fetchDeploymentBranches,
|
||||
fetchDeployments,
|
||||
runDeployment,
|
||||
scanDeployments,
|
||||
updateDeployment
|
||||
} from "../api/deployments";
|
||||
import { JobStatusBadge } from "../components/JobStatusBadge";
|
||||
@@ -30,7 +29,6 @@ import { JobStatusBadge } from "../components/JobStatusBadge";
|
||||
type FormState = {
|
||||
_id?: string;
|
||||
name: string;
|
||||
rootPath: string;
|
||||
repoUrl: string;
|
||||
branch: string;
|
||||
composeFile: DeploymentInput["composeFile"];
|
||||
@@ -39,7 +37,6 @@ type FormState = {
|
||||
|
||||
const defaultForm: FormState = {
|
||||
name: "",
|
||||
rootPath: "",
|
||||
repoUrl: "",
|
||||
branch: "main",
|
||||
composeFile: "docker-compose.yml",
|
||||
@@ -54,19 +51,15 @@ export function DeploymentsPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [candidates, setCandidates] = useState<DeploymentCandidate[]>([]);
|
||||
const [form, setForm] = useState<FormState>(defaultForm);
|
||||
const [pendingEditId, setPendingEditId] = useState<string | null>(null);
|
||||
const [branchOptions, setBranchOptions] = useState<string[]>([]);
|
||||
const [branchLoading, setBranchLoading] = useState(false);
|
||||
const [composeOptions, setComposeOptions] = useState<DeploymentInput["composeFile"][]>([]);
|
||||
const [composeLoading, setComposeLoading] = useState(false);
|
||||
const [faviconErrors, setFaviconErrors] = useState<Record<string, boolean>>({});
|
||||
|
||||
const isEdit = useMemo(() => !!form._id, [form._id]);
|
||||
const selectedCandidate = useMemo(
|
||||
() => candidates.find((c) => c.rootPath === form.rootPath),
|
||||
[candidates, form.rootPath]
|
||||
);
|
||||
|
||||
const loadDeployments = async () => {
|
||||
setLoading(true);
|
||||
@@ -80,18 +73,6 @@ export function DeploymentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadCandidates = async () => {
|
||||
setScanning(true);
|
||||
try {
|
||||
const data = await scanDeployments();
|
||||
setCandidates(data);
|
||||
} catch {
|
||||
toast.error("Root taraması yapılamadı");
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadDeployments();
|
||||
}, []);
|
||||
@@ -100,6 +81,7 @@ export function DeploymentsPage() {
|
||||
const repoUrl = form.repoUrl.trim();
|
||||
if (!repoUrl) {
|
||||
setBranchOptions([]);
|
||||
setComposeOptions([]);
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(async () => {
|
||||
@@ -119,6 +101,30 @@ export function DeploymentsPage() {
|
||||
return () => clearTimeout(timer);
|
||||
}, [form.repoUrl, form.branch]);
|
||||
|
||||
useEffect(() => {
|
||||
const repoUrl = form.repoUrl.trim();
|
||||
const branch = form.branch.trim();
|
||||
if (!repoUrl || !branch) {
|
||||
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 state = location.state as { editDeploymentId?: string } | null;
|
||||
if (state?.editDeploymentId) {
|
||||
@@ -139,16 +145,15 @@ export function DeploymentsPage() {
|
||||
const handleOpenNew = async () => {
|
||||
setForm(defaultForm);
|
||||
setBranchOptions([]);
|
||||
setComposeOptions([]);
|
||||
setModalOpen(true);
|
||||
await loadCandidates();
|
||||
};
|
||||
|
||||
const handleEdit = (deployment: DeploymentProject) => {
|
||||
const { _id, name, rootPath, repoUrl, branch, composeFile, port } = deployment;
|
||||
const { _id, name, repoUrl, branch, composeFile, port } = deployment;
|
||||
setForm({
|
||||
_id,
|
||||
name,
|
||||
rootPath,
|
||||
repoUrl,
|
||||
branch,
|
||||
composeFile,
|
||||
@@ -166,14 +171,13 @@ export function DeploymentsPage() {
|
||||
try {
|
||||
const payload: DeploymentInput = {
|
||||
name: form.name,
|
||||
rootPath: form.rootPath,
|
||||
repoUrl: form.repoUrl,
|
||||
branch: form.branch,
|
||||
composeFile: form.composeFile,
|
||||
port: form.port ? Number(form.port) : undefined
|
||||
};
|
||||
|
||||
if (!payload.name || !payload.rootPath || !payload.repoUrl || !payload.branch || !payload.composeFile) {
|
||||
if (!payload.name || !payload.repoUrl || !payload.branch || !payload.composeFile) {
|
||||
toast.error("Tüm alanları doldurun");
|
||||
setSaving(false);
|
||||
return;
|
||||
@@ -374,48 +378,8 @@ export function DeploymentsPage() {
|
||||
|
||||
<div className="max-h-[70vh] space-y-4 overflow-y-auto px-5 py-4">
|
||||
{!isEdit && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>Proje Klasörü</Label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={loadCandidates}
|
||||
disabled={scanning}
|
||||
>
|
||||
{scanning ? "Taranıyor..." : "Yeniden Tara"}
|
||||
</Button>
|
||||
</div>
|
||||
<Select
|
||||
value={form.rootPath}
|
||||
onValueChange={(value) => {
|
||||
const candidate = candidates.find((c) => c.rootPath === value);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
rootPath: value,
|
||||
name: candidate?.name || prev.name,
|
||||
composeFile: candidate?.composeFiles[0] || prev.composeFile
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Root altında proje seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{candidates.map((candidate) => (
|
||||
<SelectItem key={candidate.rootPath} value={candidate.rootPath}>
|
||||
{candidate.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{scanning
|
||||
? "Root dizin taranıyor..."
|
||||
: candidates.length === 0
|
||||
? "Root altında compose dosyası bulunan proje yok."
|
||||
: "Compose dosyası bulunan klasörleri listeler."}
|
||||
</div>
|
||||
Repo URL girildiğinde branch ve compose dosyaları listelenir.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -491,15 +455,23 @@ export function DeploymentsPage() {
|
||||
<SelectValue placeholder="Compose seçin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(selectedCandidate?.composeFiles || ["docker-compose.yml", "docker-compose.dev.yml"]).map(
|
||||
(file) => (
|
||||
{(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">
|
||||
|
||||
Reference in New Issue
Block a user