import { ChevronDown, ChevronUp, Clock, Loader2, RefreshCcw, RocketIcon, Settings, Trash2, } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { AlertBlock } from "@/components/shared/alert-block"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { DialogAction } from "@/components/shared/dialog-action"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { api, type RouterOutputs } from "@/utils/api"; import { ShowRollbackSettings } from "../rollbacks/show-rollback-settings"; import { CancelQueues } from "./cancel-queues"; import { ClearDeployments } from "./clear-deployments"; import { KillBuild } from "./kill-build"; import { RefreshToken } from "./refresh-token"; import { ShowDeployment } from "./show-deployment"; interface Props { id: string; type: | "application" | "compose" | "schedule" | "server" | "backup" | "previewDeployment" | "volumeBackup"; refreshToken?: string; serverId?: string; } export const formatDuration = (seconds: number) => { if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}m ${remainingSeconds}s`; }; export const ShowDeployments = ({ id, type, refreshToken, serverId, }: Props) => { const [activeLog, setActiveLog] = useState< RouterOutputs["deployment"]["all"][number] | null >(null); const { data: deployments, isPending: isLoadingDeployments } = api.deployment.allByType.useQuery( { id, type, }, { enabled: !!id, refetchInterval: 1000, }, ); const { data: isCloud } = api.settings.isCloud.useQuery(); const { mutateAsync: rollback, isPending: isRollingBack } = api.rollback.rollback.useMutation(); const { mutateAsync: killProcess, isPending: isKillingProcess } = api.deployment.killProcess.useMutation(); const { mutateAsync: removeDeployment, isPending: isRemovingDeployment } = api.deployment.removeDeployment.useMutation(); // Cancel deployment mutations const { mutateAsync: cancelApplicationDeployment, isPending: isCancellingApp, } = api.application.cancelDeployment.useMutation(); const { mutateAsync: cancelComposeDeployment, isPending: isCancellingCompose, } = api.compose.cancelDeployment.useMutation(); const [url, setUrl] = React.useState(""); const [expandedDescriptions, setExpandedDescriptions] = useState>( new Set(), ); const MAX_DESCRIPTION_LENGTH = 200; const truncateDescription = (description: string): string => { if (description.length <= MAX_DESCRIPTION_LENGTH) { return description; } const truncated = description.slice(0, MAX_DESCRIPTION_LENGTH); const lastSpace = truncated.lastIndexOf(" "); if (lastSpace > MAX_DESCRIPTION_LENGTH - 20 && lastSpace > 0) { return `${truncated.slice(0, lastSpace)}...`; } return `${truncated}...`; }; // Check for stuck deployment (more than 9 minutes) - only for the most recent deployment const stuckDeployment = useMemo(() => { if (!isCloud || !deployments || deployments.length === 0) return null; const now = Date.now(); const NINE_MINUTES = 10 * 60 * 1000; // 9 minutes in milliseconds // Get the most recent deployment (first in the list since they're sorted by date) const mostRecentDeployment = deployments[0]; if ( !mostRecentDeployment || mostRecentDeployment.status !== "running" || !mostRecentDeployment.startedAt ) { return null; } const startTime = new Date(mostRecentDeployment.startedAt).getTime(); const elapsed = now - startTime; return elapsed > NINE_MINUTES ? mostRecentDeployment : null; }, [isCloud, deployments]); useEffect(() => { setUrl(document.location.origin); }, []); return (
Deployments See the last 10 deployments for this {type}
{(type === "application" || type === "compose") && ( )} {(type === "application" || type === "compose") && ( )} {(type === "application" || type === "compose") && ( )} {type === "application" && ( )}
{stuckDeployment && (type === "application" || type === "compose") && (
Build appears to be stuck

Hey! Looks like the build has been running for more than 10 minutes. Would you like to cancel this deployment?

)} {refreshToken && (
If you want to re-deploy this application use this URL in the config of your git provider or docker
Webhook URL:
{`${url}/api/deploy${ type === "compose" ? "/compose" : "" }/${refreshToken}`} {(type === "application" || type === "compose") && ( )}
)} {isLoadingDeployments ? (
Loading deployments...
) : deployments?.length === 0 ? (
No deployments found
) : (
{deployments?.map((deployment, index) => { const titleText = deployment?.title?.trim() || ""; const needsTruncation = titleText.length > MAX_DESCRIPTION_LENGTH; const isExpanded = expandedDescriptions.has( deployment.deploymentId, ); const canDelete = deployment.status === "done" || deployment.status === "error"; return (
{index + 1}. {deployment.status}
{isExpanded || !needsTruncation ? titleText : truncateDescription(titleText)} {needsTruncation && ( )} {/* Hash (from description) - shown in compact form */} {deployment.description?.trim() && ( {deployment.description} )}
{deployment.startedAt && deployment.finishedAt && ( {formatDuration( Math.floor( (new Date(deployment.finishedAt).getTime() - new Date(deployment.startedAt).getTime()) / 1000, ), )} )}
{deployment.pid && deployment.status === "running" && ( { await killProcess({ deploymentId: deployment.deploymentId, }) .then(() => { toast.success("Process killed successfully"); }) .catch(() => { toast.error("Error killing process"); }); }} > )} {canDelete && ( { try { await removeDeployment({ deploymentId: deployment.deploymentId, }); toast.success("Deployment deleted successfully"); } catch (error) { toast.error("Error deleting deployment"); } }} > )} {deployment?.rollback && deployment.status === "done" && type === "application" && (

Are you sure you want to rollback to this deployment?

Please wait a few seconds while the image is pulled from the registry. Your application should be running shortly.
} type="default" onClick={async () => { await rollback({ rollbackId: deployment.rollback.rollbackId, }) .then(() => { toast.success( "Rollback initiated successfully", ); }) .catch(() => { toast.error("Error initiating rollback"); }); }} > )}
); })} )} setActiveLog(null)} logPath={activeLog?.logPath || ""} errorMessage={activeLog?.errorMessage || ""} />
); };