feat: Add expandable commit messages for deployment logs
This commit is contained in:
Mauricio Siu
2025-11-15 17:47:16 -06:00
committed by GitHub

View File

@@ -1,4 +1,12 @@
import { Clock, Loader2, RefreshCcw, RocketIcon, Settings } from "lucide-react"; import {
ChevronDown,
ChevronUp,
Clock,
Loader2,
RefreshCcw,
RocketIcon,
Settings,
} from "lucide-react";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
@@ -80,6 +88,23 @@ export const ShowDeployments = ({
} = api.compose.cancelDeployment.useMutation(); } = api.compose.cancelDeployment.useMutation();
const [url, setUrl] = React.useState(""); const [url, setUrl] = React.useState("");
const [expandedDescriptions, setExpandedDescriptions] = useState<Set<string>>(
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 // Check for stuck deployment (more than 9 minutes) - only for the most recent deployment
const stuckDeployment = useMemo(() => { const stuckDeployment = useMemo(() => {
@@ -217,118 +242,164 @@ export const ShowDeployments = ({
</div> </div>
) : ( ) : (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{deployments?.map((deployment, index) => ( {deployments?.map((deployment, index) => {
<div const titleText = deployment?.title?.trim() || "";
key={deployment.deploymentId} const needsTruncation = titleText.length > MAX_DESCRIPTION_LENGTH;
className="flex items-center justify-between rounded-lg border p-4 gap-2" const isExpanded = expandedDescriptions.has(
> deployment.deploymentId,
<div className="flex flex-col"> );
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
{index + 1}. {deployment.status} return (
<StatusTooltip <div
status={deployment?.status} key={deployment.deploymentId}
className="size-2.5" className="flex items-center justify-between rounded-lg border p-4 gap-2"
/> >
</span> <div className="flex flex-col">
<span className="text-sm text-muted-foreground"> <span className="flex items-center gap-4 font-medium capitalize text-foreground">
{deployment.title} {index + 1}. {deployment.status}
</span> <StatusTooltip
{deployment.description && ( status={deployment?.status}
<span className="break-all text-sm text-muted-foreground"> className="size-2.5"
{deployment.description} />
</span> </span>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-col gap-1">
{deployment.pid && deployment.status === "running" && ( <span className="break-words text-sm text-muted-foreground whitespace-pre-wrap">
<DialogAction {isExpanded || !needsTruncation
title="Kill Process" ? titleText
description="Are you sure you want to kill the process?" : truncateDescription(titleText)}
type="default" </span>
onClick={async () => { {needsTruncation && (
await killProcess({ <button
deploymentId: deployment.deploymentId, type="button"
}) onClick={() => {
.then(() => { const next = new Set(expandedDescriptions);
toast.success("Process killed successfully"); if (next.has(deployment.deploymentId)) {
}) next.delete(deployment.deploymentId);
.catch(() => { } else {
toast.error("Error killing process"); next.add(deployment.deploymentId);
}); }
}} setExpandedDescriptions(next);
> }}
<Button className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors w-fit mt-1 cursor-pointer"
variant="destructive" aria-label={
size="sm" isExpanded
isLoading={isKillingProcess} ? "Collapse commit message"
: "Expand commit message"
}
> >
Kill Process {isExpanded ? (
</Button> <>
</DialogAction> <ChevronUp className="size-3" />
)} Show less
<Button </>
onClick={() => { ) : (
setActiveLog(deployment); <>
}} <ChevronDown className="size-3" />
> Show more
View </>
</Button> )}
</button>
)}
{/* Hash (from description) - shown in compact form */}
{deployment.description?.trim() && (
<span className="text-xs text-muted-foreground font-mono">
{deployment.description}
</span>
)}
</div>
</div>
<div className="flex flex-col items-end gap-2 max-w-[300px] w-full justify-start">
<div className="text-sm capitalize text-muted-foreground flex items-center gap-2">
<DateTooltip date={deployment.createdAt} />
{deployment.startedAt && deployment.finishedAt && (
<Badge
variant="outline"
className="text-[10px] gap-1 flex items-center"
>
<Clock className="size-3" />
{formatDuration(
Math.floor(
(new Date(deployment.finishedAt).getTime() -
new Date(deployment.startedAt).getTime()) /
1000,
),
)}
</Badge>
)}
</div>
{deployment?.rollback && <div className="flex flex-row items-center gap-2">
deployment.status === "done" && {deployment.pid && deployment.status === "running" && (
type === "application" && (
<DialogAction <DialogAction
title="Rollback to this deployment" title="Kill Process"
description="Are you sure you want to rollback to this deployment?" description="Are you sure you want to kill the process?"
type="default" type="default"
onClick={async () => { onClick={async () => {
await rollback({ await killProcess({
rollbackId: deployment.rollback.rollbackId, deploymentId: deployment.deploymentId,
}) })
.then(() => { .then(() => {
toast.success( toast.success("Process killed successfully");
"Rollback initiated successfully",
);
}) })
.catch(() => { .catch(() => {
toast.error("Error initiating rollback"); toast.error("Error killing process");
}); });
}} }}
> >
<Button <Button
variant="secondary" variant="destructive"
size="sm" size="sm"
isLoading={isRollingBack} isLoading={isKillingProcess}
> >
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" /> Kill Process
Rollback
</Button> </Button>
</DialogAction> </DialogAction>
)} )}
<Button
onClick={() => {
setActiveLog(deployment);
}}
>
View
</Button>
{deployment?.rollback &&
deployment.status === "done" &&
type === "application" && (
<DialogAction
title="Rollback to this deployment"
description="Are you sure you want to rollback to this deployment?"
type="default"
onClick={async () => {
await rollback({
rollbackId: deployment.rollback.rollbackId,
})
.then(() => {
toast.success(
"Rollback initiated successfully",
);
})
.catch(() => {
toast.error("Error initiating rollback");
});
}}
>
<Button
variant="secondary"
size="sm"
isLoading={isRollingBack}
>
<RefreshCcw className="size-4 text-primary group-hover:text-red-500" />
Rollback
</Button>
</DialogAction>
)}
</div>
</div> </div>
</div> </div>
</div> );
))} })}
</div> </div>
)} )}
<ShowDeployment <ShowDeployment