feat(deployments): deployment restart özelliği ekle

Deployment projeleri için yeniden başlatma (restart) yeteneği eklendi.
Backend servisi, API endpoint'i ve kullanıcı arayüzü butonları güncellendi.
This commit is contained in:
2026-02-03 08:53:03 +00:00
parent a117275efe
commit b04ac03739
5 changed files with 154 additions and 2 deletions

View File

@@ -232,4 +232,18 @@ router.post("/:id/run", async (req, res) => {
}); });
}); });
router.post("/:id/restart", async (req, res) => {
authMiddleware(req, res, async () => {
const { id } = req.params;
const project = await DeploymentProject.findById(id);
if (!project) return res.status(404).json({ message: "Deployment bulunamadı" });
const rawMessage = typeof req.body?.message === "string" ? req.body.message.trim() : "";
const message = rawMessage || "restart";
deploymentService
.restartDeployment(id, { message })
.catch(() => undefined);
return res.json({ queued: true });
});
});
export default router; export default router;

View File

@@ -706,6 +706,97 @@ class DeploymentService {
} }
} }
async restartDeployment(projectId: string, options?: { message?: string }) {
if (this.running.get(projectId)) {
return;
}
this.running.set(projectId, true);
const project = await DeploymentProject.findById(projectId);
if (!project) {
this.running.delete(projectId);
return;
}
const normalizedMessage = normalizeCommitMessage(options?.message);
const startedAt = Date.now();
const runLogs: string[] = [];
const pushLog = (line: string) => {
runLogs.push(line);
this.emitLog(projectId, line);
};
const runDoc = await DeploymentRun.create({
project: projectId,
status: "running",
startedAt: new Date(),
message: normalizedMessage ?? options?.message
});
this.emitRun(projectId, runDoc);
await writeRunFile(project.rootPath, runDoc);
await DeploymentProject.findByIdAndUpdate(projectId, {
lastStatus: "running",
lastMessage: normalizedMessage ?? options?.message ?? "Restart başlıyor..."
});
await this.emitStatus(projectId, {
lastStatus: "running",
lastMessage: normalizedMessage ?? options?.message ?? "Restart başlıyor..."
} as DeploymentProjectDocument);
try {
pushLog("Restart komutları çalıştırılıyor...");
await runCompose(project, (line) => pushLog(line));
const duration = Date.now() - startedAt;
await DeploymentProject.findByIdAndUpdate(projectId, {
lastStatus: "success",
lastDeployAt: new Date(),
lastMessage: normalizedMessage ?? options?.message ?? "Restart başarılı"
});
await this.emitStatus(projectId, {
lastStatus: "success",
lastDeployAt: new Date(),
lastMessage: normalizedMessage ?? options?.message ?? "Restart başarılı"
} as DeploymentProjectDocument);
await DeploymentRun.findByIdAndUpdate(runDoc._id, {
status: "success",
finishedAt: new Date(),
durationMs: duration,
logs: runLogs,
message: normalizedMessage ?? options?.message
});
const updatedRun = await DeploymentRun.findById(runDoc._id);
if (updatedRun) this.emitRun(projectId, updatedRun);
if (updatedRun) await writeRunFile(project.rootPath, updatedRun);
pushLog("Restart tamamlandı: Başarılı");
} catch (err) {
const duration = Date.now() - startedAt;
await DeploymentProject.findByIdAndUpdate(projectId, {
lastStatus: "failed",
lastDeployAt: new Date(),
lastMessage: (err as Error).message
});
await this.emitStatus(projectId, {
lastStatus: "failed",
lastDeployAt: new Date(),
lastMessage: (err as Error).message
} as DeploymentProjectDocument);
await DeploymentRun.findByIdAndUpdate(runDoc._id, {
status: "failed",
finishedAt: new Date(),
durationMs: duration,
logs: runLogs,
message: normalizedMessage ?? options?.message
});
const updatedRun = await DeploymentRun.findById(runDoc._id);
if (updatedRun) this.emitRun(projectId, updatedRun);
if (updatedRun) await writeRunFile(project.rootPath, updatedRun);
pushLog(`Hata: ${(err as Error).message}`);
} finally {
this.running.delete(projectId);
}
}
async cleanupProjectResources(project: DeploymentProjectDocument) { async cleanupProjectResources(project: DeploymentProjectDocument) {
const composePath = path.join(project.rootPath, project.composeFile); const composePath = path.join(project.rootPath, project.composeFile);
if (!fs.existsSync(composePath)) { if (!fs.existsSync(composePath)) {

View File

@@ -96,6 +96,10 @@ export async function runDeployment(id: string, message?: string): Promise<void>
await apiClient.post(`/deployments/${id}/run`, message ? { message } : {}); await apiClient.post(`/deployments/${id}/run`, message ? { message } : {});
} }
export async function restartDeployment(id: string, message?: string): Promise<void> {
await apiClient.post(`/deployments/${id}/restart`, message ? { message } : {});
}
export async function fetchDeployment(id: string): Promise<DeploymentDetailResponse> { export async function fetchDeployment(id: string): Promise<DeploymentDetailResponse> {
const { data } = await apiClient.get(`/deployments/${id}`); const { data } = await apiClient.get(`/deployments/${id}`);
return data as DeploymentDetailResponse; return data as DeploymentDetailResponse;

View File

@@ -7,7 +7,8 @@ import {
faCopy, faCopy,
faEye, faEye,
faEyeSlash, faEyeSlash,
faHistory faHistory,
faRotate
} from "@fortawesome/free-solid-svg-icons"; } 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";
@@ -25,6 +26,7 @@ import {
fetchDeploymentBranches, fetchDeploymentBranches,
fetchDeploymentComposeFiles, fetchDeploymentComposeFiles,
fetchDeploymentEnvExamples, fetchDeploymentEnvExamples,
restartDeployment,
runDeployment, runDeployment,
updateDeployment updateDeployment
} from "../api/deployments"; } from "../api/deployments";
@@ -49,6 +51,7 @@ 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 [restarting, setRestarting] = useState(false);
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [form, setForm] = useState<FormState>({ const [form, setForm] = useState<FormState>({
@@ -169,6 +172,19 @@ export function DeploymentDetailPage() {
} }
}; };
const handleRestart = async () => {
if (!id) return;
setRestarting(true);
try {
await restartDeployment(id, "restart");
toast.success("Restart tetiklendi");
} catch {
toast.error("Restart tetiklenemedi");
} finally {
setRestarting(false);
}
};
useEffect(() => { useEffect(() => {
const repoUrl = form.repoUrl.trim(); const repoUrl = form.repoUrl.trim();
if (!repoUrl) { if (!repoUrl) {
@@ -341,6 +357,10 @@ export function DeploymentDetailPage() {
> >
Düzenle Düzenle
</Button> </Button>
<Button onClick={handleRestart} disabled={restarting} className="gap-2">
<FontAwesomeIcon icon={faRotate} className="h-4 w-4" />
{restarting ? "Restarting..." : "Restart"}
</Button>
<Button onClick={handleRun} disabled={triggering} className="gap-2"> <Button onClick={handleRun} disabled={triggering} className="gap-2">
<FontAwesomeIcon icon={faCloudArrowUp} className="h-4 w-4" /> <FontAwesomeIcon icon={faCloudArrowUp} className="h-4 w-4" />
{triggering ? "Deploying..." : "Deploy"} {triggering ? "Deploying..." : "Deploy"}

View File

@@ -6,6 +6,7 @@ import {
faCloudArrowUp, faCloudArrowUp,
faEye, faEye,
faEyeSlash, faEyeSlash,
faPenToSquare,
faPlus, faPlus,
faRotate, faRotate,
faRocket faRocket
@@ -25,6 +26,7 @@ import {
fetchDeploymentBranches, fetchDeploymentBranches,
fetchDeploymentEnvExamples, fetchDeploymentEnvExamples,
fetchDeployments, fetchDeployments,
restartDeployment,
runDeployment, runDeployment,
updateDeployment updateDeployment
} from "../api/deployments"; } from "../api/deployments";
@@ -293,6 +295,15 @@ export function DeploymentsPage() {
} }
}; };
const handleRestart = async (id: string) => {
try {
await restartDeployment(id, "restart");
toast.success("Restart tetiklendi");
} catch {
toast.error("Restart tetiklenemedi");
}
};
const handleDelete = async (deployment: DeploymentProject) => { const handleDelete = async (deployment: DeploymentProject) => {
const ok = window.confirm("Bu deployment'ı silmek istediğinize emin misiniz?"); const ok = window.confirm("Bu deployment'ı silmek istediğinize emin misiniz?");
if (!ok) return; if (!ok) return;
@@ -393,6 +404,18 @@ export function DeploymentsPage() {
> >
<FontAwesomeIcon icon={faCloudArrowUp} className="h-4 w-4" /> <FontAwesomeIcon icon={faCloudArrowUp} className="h-4 w-4" />
</Button> </Button>
<Button
variant="outline"
size="icon"
onClick={(e) => {
e.stopPropagation();
handleRestart(deployment._id);
}}
title="Restart"
aria-label="Restart"
>
<FontAwesomeIcon icon={faRotate} className="h-4 w-4" />
</Button>
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
@@ -402,7 +425,7 @@ export function DeploymentsPage() {
}} }}
title="Düzenle" title="Düzenle"
> >
<FontAwesomeIcon icon={faRotate} className="h-4 w-4" /> <FontAwesomeIcon icon={faPenToSquare} className="h-4 w-4" />
</Button> </Button>
<Button <Button
variant="outline" variant="outline"