Merge branch 'feat/live-build-logs-modal'
This commit is contained in:
@@ -1727,6 +1727,24 @@ CMD ["sh", "-c", "DLL=$(find . -maxdepth 1 -name '*.dll' ! -name '*.deps.dll' !
|
|||||||
throw new Error(`Build job ${jobName} timed out after ${timeoutSeconds}s\nLogs:\n${logs}`);
|
throw new Error(`Build job ${jobName} timed out after ${timeoutSeconds}s\nLogs:\n${logs}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Live build logs for an in-progress build, read straight from the running
|
||||||
|
* build pod (init + kaniko containers). Returns null when there is no active
|
||||||
|
* build session for this deployment (e.g. build already finished/cleaned up),
|
||||||
|
* so callers can fall back to the persisted build log.
|
||||||
|
*/
|
||||||
|
async getLiveBuildLog(deploymentId: string): Promise<string | null> {
|
||||||
|
const session = this.activeBuilds.get(deploymentId);
|
||||||
|
if (!session?.coreApi || !session.namespace || !session.buildPodName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.getBuildLogs(session.coreApi, session.buildPodName, session.namespace);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getBuildLogs(coreApi: k8s.CoreV1Api, jobName: string, namespace: string): Promise<string> {
|
private async getBuildLogs(coreApi: k8s.CoreV1Api, jobName: string, namespace: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const pods = await coreApi.listNamespacedPod({
|
const pods = await coreApi.listNamespacedPod({
|
||||||
|
|||||||
@@ -507,8 +507,19 @@ export class DeploymentsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// While the build is in progress the persisted buildLog isn't written yet —
|
||||||
|
// stream the live build pod logs so the deploy modal can show them in real time.
|
||||||
|
let buildLog = latest.buildLog || null;
|
||||||
|
if (
|
||||||
|
latest.status === DeploymentStatus.BUILDING ||
|
||||||
|
latest.status === DeploymentStatus.DEPLOYING
|
||||||
|
) {
|
||||||
|
const live = await this.buildService.getLiveBuildLog(latest.id);
|
||||||
|
if (live) buildLog = live;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buildLog: latest.buildLog || null,
|
buildLog,
|
||||||
status: latest.status,
|
status: latest.status,
|
||||||
version: latest.version,
|
version: latest.version,
|
||||||
createdAt: latest.createdAt,
|
createdAt: latest.createdAt,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import { Loader2, Upload, Hammer, Rocket, CheckCircle, XCircle, X, Minimize2 } from 'lucide-react';
|
import { Loader2, Upload, Hammer, Rocket, CheckCircle, XCircle, X, Minimize2 } from 'lucide-react';
|
||||||
import { notify } from '@/lib/notify';
|
import { notify } from '@/lib/notify';
|
||||||
@@ -69,9 +70,29 @@ export function BuildProgressModal({
|
|||||||
const showBytes = progress.phase === 'uploading' && progress.totalBytes;
|
const showBytes = progress.phase === 'uploading' && progress.totalBytes;
|
||||||
const isCancelling = cancelMutation.isPending;
|
const isCancelling = cancelMutation.isPending;
|
||||||
|
|
||||||
|
// Live build logs — only meaningful once the build pod exists (building/deploying).
|
||||||
|
const showBuildLog = progress.phase === 'building' || progress.phase === 'deploying';
|
||||||
|
const { data: buildLogsData } = useQuery<{ buildLog: string | null }>({
|
||||||
|
queryKey: ['build-logs', appId],
|
||||||
|
queryFn: () => api.get(`/deployments/applications/${appId}/build-logs`).then((r) => r.data),
|
||||||
|
enabled: showBuildLog,
|
||||||
|
refetchInterval: showBuildLog ? 2000 : false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const logRef = useRef<HTMLPreElement>(null);
|
||||||
|
const buildLog = buildLogsData?.buildLog;
|
||||||
|
useEffect(() => {
|
||||||
|
const el = logRef.current;
|
||||||
|
if (el) el.scrollTop = el.scrollHeight;
|
||||||
|
}, [buildLog]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
|
||||||
<div className="relative bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 space-y-5">
|
<div
|
||||||
|
className={`relative bg-white rounded-2xl shadow-2xl p-8 w-full mx-4 space-y-5 ${
|
||||||
|
showBuildLog ? 'max-w-2xl' : 'max-w-md'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div className="absolute top-4 right-4 rtl:right-auto rtl:left-4 flex items-center gap-1">
|
<div className="absolute top-4 right-4 rtl:right-auto rtl:left-4 flex items-center gap-1">
|
||||||
{isActive && (
|
{isActive && (
|
||||||
<button
|
<button
|
||||||
@@ -129,6 +150,27 @@ export function BuildProgressModal({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showBuildLog && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium text-gray-700 flex items-center gap-1.5">
|
||||||
|
<Hammer className="w-4 h-4" /> {c.buildLogTitle}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-gray-400 flex items-center gap-1.5">
|
||||||
|
<span className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||||
|
{c.buildLogLive}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
ref={logRef}
|
||||||
|
className="bg-gray-900 text-blue-300 p-4 rounded-xl text-xs font-mono overflow-x-auto max-h-64 overflow-y-auto whitespace-pre-wrap break-words text-left rtl:text-left"
|
||||||
|
dir="ltr"
|
||||||
|
>
|
||||||
|
{buildLog || c.buildLogWaiting}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{progress.phase === 'failed' && progress.message && (
|
{progress.phase === 'failed' && progress.message && (
|
||||||
<p className="text-sm text-red-600 text-center">{progress.message}</p>
|
<p className="text-sm text-red-600 text-center">{progress.message}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ const en: Dictionary = {
|
|||||||
deploymentCancelled: 'Deployment cancelled',
|
deploymentCancelled: 'Deployment cancelled',
|
||||||
cancelDeploymentFailed: 'Failed to cancel deployment',
|
cancelDeploymentFailed: 'Failed to cancel deployment',
|
||||||
minimizeHint: 'Use minimize to keep working while deployment continues. Close (×) cancels the deployment.',
|
minimizeHint: 'Use minimize to keep working while deployment continues. Close (×) cancels the deployment.',
|
||||||
|
buildLogTitle: 'Build log',
|
||||||
|
buildLogWaiting: 'Starting build…',
|
||||||
|
buildLogLive: 'live',
|
||||||
continueInBackground: 'Continue in background',
|
continueInBackground: 'Continue in background',
|
||||||
minimizeAria: 'Minimize and continue in background',
|
minimizeAria: 'Minimize and continue in background',
|
||||||
cancelDeploymentAria: 'Cancel deployment',
|
cancelDeploymentAria: 'Cancel deployment',
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ const fa = {
|
|||||||
deploymentCancelled: 'دیپلوی لغو شد',
|
deploymentCancelled: 'دیپلوی لغو شد',
|
||||||
cancelDeploymentFailed: 'لغو دیپلوی ناموفق بود',
|
cancelDeploymentFailed: 'لغو دیپلوی ناموفق بود',
|
||||||
minimizeHint: 'برای ادامهٔ کار حین دیپلوی، کوچک کن. بستن (×) دیپلوی را لغو میکند.',
|
minimizeHint: 'برای ادامهٔ کار حین دیپلوی، کوچک کن. بستن (×) دیپلوی را لغو میکند.',
|
||||||
|
buildLogTitle: 'لاگ بیلد',
|
||||||
|
buildLogWaiting: 'در حال شروع بیلد…',
|
||||||
|
buildLogLive: 'زنده',
|
||||||
continueInBackground: 'ادامه در پسزمینه',
|
continueInBackground: 'ادامه در پسزمینه',
|
||||||
minimizeAria: 'کوچک کن و در پسزمینه ادامه بده',
|
minimizeAria: 'کوچک کن و در پسزمینه ادامه بده',
|
||||||
cancelDeploymentAria: 'لغو دیپلوی',
|
cancelDeploymentAria: 'لغو دیپلوی',
|
||||||
|
|||||||
Reference in New Issue
Block a user