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:
@@ -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;
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user