Compare commits
4 Commits
b04ac03739
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 719ae4044e | |||
| e2b9f19800 | |||
| 064a04d898 | |||
| 1f90ce54d4 |
@@ -126,6 +126,7 @@ async function start() {
|
|||||||
try {
|
try {
|
||||||
await mongoose.connect(config.mongoUri);
|
await mongoose.connect(config.mongoUri);
|
||||||
console.log("MongoDB'ye bağlanıldı");
|
console.log("MongoDB'ye bağlanıldı");
|
||||||
|
await deploymentService.ensureSettings();
|
||||||
await jobService.bootstrapFromFilesystem();
|
await jobService.bootstrapFromFilesystem();
|
||||||
await jobService.bootstrap();
|
await jobService.bootstrap();
|
||||||
await deploymentService.normalizeExistingCommitMessages();
|
await deploymentService.normalizeExistingCommitMessages();
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import mongoose, { Schema, Document } from "mongoose";
|
|||||||
export interface SettingsDocument extends Document {
|
export interface SettingsDocument extends Document {
|
||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
|
cleanupIntervalValue?: number;
|
||||||
|
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -10,7 +12,9 @@ export interface SettingsDocument extends Document {
|
|||||||
const SettingsSchema = new Schema<SettingsDocument>(
|
const SettingsSchema = new Schema<SettingsDocument>(
|
||||||
{
|
{
|
||||||
webhookToken: { type: String, required: true },
|
webhookToken: { type: String, required: true },
|
||||||
webhookSecret: { type: String, required: true }
|
webhookSecret: { type: String, required: true },
|
||||||
|
cleanupIntervalValue: { type: Number, min: 1 },
|
||||||
|
cleanupIntervalUnit: { type: String, enum: ["saat", "gün", "hafta"] }
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ router.get("/", async (_req, res) => {
|
|||||||
return res.json({
|
return res.json({
|
||||||
webhookToken: settings.webhookToken,
|
webhookToken: settings.webhookToken,
|
||||||
webhookSecret: settings.webhookSecret,
|
webhookSecret: settings.webhookSecret,
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit,
|
||||||
updatedAt: settings.updatedAt
|
updatedAt: settings.updatedAt
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,4 +33,29 @@ router.post("/secret/rotate", async (_req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post("/cleanup-interval", async (req, res) => {
|
||||||
|
const settings = await deploymentService.ensureSettings();
|
||||||
|
const { value, unit } = req.body as {
|
||||||
|
value?: number;
|
||||||
|
unit?: "saat" | "gün" | "hafta";
|
||||||
|
};
|
||||||
|
if (!value || value < 1 || !unit) {
|
||||||
|
return res.status(400).json({ message: "Geçerli periyot gerekli" });
|
||||||
|
}
|
||||||
|
settings.cleanupIntervalValue = value;
|
||||||
|
settings.cleanupIntervalUnit = unit;
|
||||||
|
await settings.save();
|
||||||
|
await deploymentService.updateCleanupSchedule(value, unit);
|
||||||
|
return res.json({
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit,
|
||||||
|
updatedAt: settings.updatedAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post("/cleanup-images", async (_req, res) => {
|
||||||
|
await deploymentService.cleanupUnusedImages();
|
||||||
|
return res.json({ success: true });
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ type DeploymentMetadata = {
|
|||||||
type SettingsMetadata = {
|
type SettingsMetadata = {
|
||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
|
cleanupIntervalValue?: number;
|
||||||
|
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
|
||||||
};
|
};
|
||||||
|
|
||||||
type StoredRun = {
|
type StoredRun = {
|
||||||
@@ -360,6 +362,7 @@ async function runCompose(project: DeploymentProjectDocument, onData: (line: str
|
|||||||
class DeploymentService {
|
class DeploymentService {
|
||||||
private running: Map<string, boolean> = new Map();
|
private running: Map<string, boolean> = new Map();
|
||||||
private io: Server | null = null;
|
private io: Server | null = null;
|
||||||
|
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
setSocket(io: Server) {
|
setSocket(io: Server) {
|
||||||
this.io = io;
|
this.io = io;
|
||||||
@@ -451,14 +454,23 @@ class DeploymentService {
|
|||||||
|
|
||||||
async ensureSettings() {
|
async ensureSettings() {
|
||||||
const existing = await Settings.findOne();
|
const existing = await Settings.findOne();
|
||||||
if (existing) return existing;
|
if (existing) {
|
||||||
|
await this.updateCleanupSchedule(existing.cleanupIntervalValue, existing.cleanupIntervalUnit);
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
const fileSettings = await readSettingsFile();
|
const fileSettings = await readSettingsFile();
|
||||||
if (fileSettings) {
|
if (fileSettings) {
|
||||||
const createdFromFile = await Settings.create({
|
const createdFromFile = await Settings.create({
|
||||||
webhookToken: fileSettings.webhookToken,
|
webhookToken: fileSettings.webhookToken,
|
||||||
webhookSecret: fileSettings.webhookSecret
|
webhookSecret: fileSettings.webhookSecret,
|
||||||
|
cleanupIntervalValue: fileSettings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: fileSettings.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
|
await this.updateCleanupSchedule(
|
||||||
|
createdFromFile.cleanupIntervalValue,
|
||||||
|
createdFromFile.cleanupIntervalUnit
|
||||||
|
);
|
||||||
return createdFromFile;
|
return createdFromFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,8 +480,11 @@ class DeploymentService {
|
|||||||
});
|
});
|
||||||
await writeSettingsFile({
|
await writeSettingsFile({
|
||||||
webhookToken: created.webhookToken,
|
webhookToken: created.webhookToken,
|
||||||
webhookSecret: created.webhookSecret
|
webhookSecret: created.webhookSecret,
|
||||||
|
cleanupIntervalValue: created.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: created.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
|
await this.updateCleanupSchedule(created.cleanupIntervalValue, created.cleanupIntervalUnit);
|
||||||
return created;
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,7 +494,9 @@ class DeploymentService {
|
|||||||
await settings.save();
|
await settings.save();
|
||||||
await writeSettingsFile({
|
await writeSettingsFile({
|
||||||
webhookToken: settings.webhookToken,
|
webhookToken: settings.webhookToken,
|
||||||
webhookSecret: settings.webhookSecret
|
webhookSecret: settings.webhookSecret,
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
@@ -490,11 +507,35 @@ class DeploymentService {
|
|||||||
await settings.save();
|
await settings.save();
|
||||||
await writeSettingsFile({
|
await writeSettingsFile({
|
||||||
webhookToken: settings.webhookToken,
|
webhookToken: settings.webhookToken,
|
||||||
webhookSecret: settings.webhookSecret
|
webhookSecret: settings.webhookSecret,
|
||||||
|
cleanupIntervalValue: settings.cleanupIntervalValue,
|
||||||
|
cleanupIntervalUnit: settings.cleanupIntervalUnit
|
||||||
});
|
});
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateCleanupSchedule(value?: number, unit?: "saat" | "gün" | "hafta") {
|
||||||
|
if (this.cleanupTimer) {
|
||||||
|
clearInterval(this.cleanupTimer);
|
||||||
|
this.cleanupTimer = null;
|
||||||
|
}
|
||||||
|
if (!value || !unit) return;
|
||||||
|
const intervalMs =
|
||||||
|
unit === "saat"
|
||||||
|
? value * 60 * 60 * 1000
|
||||||
|
: unit === "gün"
|
||||||
|
? value * 24 * 60 * 60 * 1000
|
||||||
|
: value * 7 * 24 * 60 * 60 * 1000;
|
||||||
|
if (!intervalMs || Number.isNaN(intervalMs)) return;
|
||||||
|
this.cleanupTimer = setInterval(() => {
|
||||||
|
this.cleanupUnusedImages().catch(() => undefined);
|
||||||
|
}, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async cleanupUnusedImages() {
|
||||||
|
await runCommand("docker image prune -a -f", process.cwd(), () => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
async createProject(input: {
|
async createProject(input: {
|
||||||
name: string;
|
name: string;
|
||||||
repoUrl: string;
|
repoUrl: string;
|
||||||
|
|||||||
@@ -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-scroll-area": "^1.2.5",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@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",
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { apiClient } from "./client";
|
|||||||
export interface SettingsResponse {
|
export interface SettingsResponse {
|
||||||
webhookToken: string;
|
webhookToken: string;
|
||||||
webhookSecret: string;
|
webhookSecret: string;
|
||||||
|
cleanupIntervalValue?: number;
|
||||||
|
cleanupIntervalUnit?: "saat" | "gün" | "hafta";
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,3 +22,13 @@ export async function rotateWebhookSecret(): Promise<SettingsResponse> {
|
|||||||
const { data } = await apiClient.post("/settings/secret/rotate");
|
const { data } = await apiClient.post("/settings/secret/rotate");
|
||||||
return data as SettingsResponse;
|
return data as SettingsResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveCleanupInterval(value: number, unit: "saat" | "gün" | "hafta") {
|
||||||
|
const { data } = await apiClient.post("/settings/cleanup-interval", { value, unit });
|
||||||
|
return data as SettingsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cleanupImages(): Promise<{ success: boolean }> {
|
||||||
|
const { data } = await apiClient.post("/settings/cleanup-images");
|
||||||
|
return data as { success: boolean };
|
||||||
|
}
|
||||||
|
|||||||
24
frontend/src/components/ui/scroll-area.tsx
Normal file
24
frontend/src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
const ScrollArea = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<ScrollAreaPrimitive.Root ref={ref} className={cn("relative overflow-hidden", className)} {...props}>
|
||||||
|
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||||
|
{children}
|
||||||
|
</ScrollAreaPrimitive.Viewport>
|
||||||
|
<ScrollAreaPrimitive.Scrollbar
|
||||||
|
orientation="vertical"
|
||||||
|
className="flex touch-none select-none p-0.5 transition-colors"
|
||||||
|
>
|
||||||
|
<ScrollAreaPrimitive.Thumb className="relative flex-1 rounded-full bg-border" />
|
||||||
|
</ScrollAreaPrimitive.Scrollbar>
|
||||||
|
<ScrollAreaPrimitive.Corner />
|
||||||
|
</ScrollAreaPrimitive.Root>
|
||||||
|
));
|
||||||
|
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { ScrollArea };
|
||||||
@@ -422,10 +422,11 @@ export function DeploymentDetailPage() {
|
|||||||
Deploy Geçmişi
|
Deploy Geçmişi
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3">
|
<CardContent className="max-h-[520px] overflow-y-auto pr-2">
|
||||||
{runs.length === 0 && (
|
{runs.length === 0 ? (
|
||||||
<div className="text-sm text-muted-foreground">Henüz deploy çalıştırılmadı.</div>
|
<div className="text-sm text-muted-foreground">Henüz deploy çalıştırılmadı.</div>
|
||||||
)}
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
{runs.map((run) => (
|
{runs.map((run) => (
|
||||||
<div
|
<div
|
||||||
key={run._id}
|
key={run._id}
|
||||||
@@ -445,6 +446,8 @@ export function DeploymentDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faCopy, faEye, faEyeSlash, faRotate } from "@fortawesome/free-solid-svg-icons";
|
import { faBroom, faCopy, faEye, faEyeSlash, faFloppyDisk, faRotate } 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 { fetchSettings, rotateWebhookSecret, rotateWebhookToken, SettingsResponse } from "../api/settings";
|
import { Input } from "../components/ui/input";
|
||||||
|
import { Label } from "../components/ui/label";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select";
|
||||||
|
import {
|
||||||
|
cleanupImages,
|
||||||
|
fetchSettings,
|
||||||
|
rotateWebhookSecret,
|
||||||
|
rotateWebhookToken,
|
||||||
|
saveCleanupInterval,
|
||||||
|
SettingsResponse
|
||||||
|
} from "../api/settings";
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
const [settings, setSettings] = useState<SettingsResponse | null>(null);
|
const [settings, setSettings] = useState<SettingsResponse | null>(null);
|
||||||
@@ -13,10 +23,22 @@ export function SettingsPage() {
|
|||||||
const [rotatingSecret, setRotatingSecret] = useState(false);
|
const [rotatingSecret, setRotatingSecret] = useState(false);
|
||||||
const [showToken, setShowToken] = useState(false);
|
const [showToken, setShowToken] = useState(false);
|
||||||
const [showSecret, setShowSecret] = useState(false);
|
const [showSecret, setShowSecret] = useState(false);
|
||||||
|
const [cleanupValue, setCleanupValue] = useState<string>("1");
|
||||||
|
const [cleanupUnit, setCleanupUnit] = useState<"saat" | "gün" | "hafta">("hafta");
|
||||||
|
const [savingCleanup, setSavingCleanup] = useState(false);
|
||||||
|
const [cleaning, setCleaning] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSettings()
|
fetchSettings()
|
||||||
.then((data) => setSettings(data))
|
.then((data) => {
|
||||||
|
setSettings(data);
|
||||||
|
if (data.cleanupIntervalValue) {
|
||||||
|
setCleanupValue(String(data.cleanupIntervalValue));
|
||||||
|
}
|
||||||
|
if (data.cleanupIntervalUnit) {
|
||||||
|
setCleanupUnit(data.cleanupIntervalUnit);
|
||||||
|
}
|
||||||
|
})
|
||||||
.catch(() => toast.error("Settings yüklenemedi"))
|
.catch(() => toast.error("Settings yüklenemedi"))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -77,6 +99,36 @@ export function SettingsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSaveCleanup = async () => {
|
||||||
|
const value = Number(cleanupValue);
|
||||||
|
if (!value || Number.isNaN(value) || value < 1) {
|
||||||
|
toast.error("Geçerli bir periyot girin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSavingCleanup(true);
|
||||||
|
try {
|
||||||
|
const data = await saveCleanupInterval(value, cleanupUnit);
|
||||||
|
setSettings((prev) => (prev ? { ...prev, ...data } : data));
|
||||||
|
toast.success("Temizlik periyodu kaydedildi");
|
||||||
|
} catch {
|
||||||
|
toast.error("Periyot kaydedilemedi");
|
||||||
|
} finally {
|
||||||
|
setSavingCleanup(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCleanupImages = async () => {
|
||||||
|
setCleaning(true);
|
||||||
|
try {
|
||||||
|
await cleanupImages();
|
||||||
|
toast.success("Kullanılmayan image'lar temizlendi");
|
||||||
|
} catch {
|
||||||
|
toast.error("Temizlik başarısız");
|
||||||
|
} finally {
|
||||||
|
setCleaning(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">
|
||||||
@@ -173,6 +225,58 @@ export function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Image Temizliği</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="grid gap-4 sm:grid-cols-[1fr_auto] sm:items-end">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="cleanupValue">Temizlik Periyodu</Label>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="cleanupValue"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={cleanupValue}
|
||||||
|
onChange={(e) => setCleanupValue(e.target.value)}
|
||||||
|
className="bg-white"
|
||||||
|
/>
|
||||||
|
<Select value={cleanupUnit} onValueChange={(value) => setCleanupUnit(value as typeof cleanupUnit)}>
|
||||||
|
<SelectTrigger className="w-32">
|
||||||
|
<SelectValue placeholder="Birim" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="saat">saat</SelectItem>
|
||||||
|
<SelectItem value="gün">gün</SelectItem>
|
||||||
|
<SelectItem value="hafta">hafta</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleSaveCleanup}
|
||||||
|
disabled={savingCleanup}
|
||||||
|
className="gap-2 bg-white text-foreground hover:bg-muted"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFloppyDisk} className="h-4 w-4" />
|
||||||
|
Kaydet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCleanupImages}
|
||||||
|
disabled={cleaning}
|
||||||
|
className="gap-2 bg-black text-white hover:bg-black/90"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faBroom} className="h-4 w-4" />
|
||||||
|
Clean Cache Images
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user