mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-27 01:55:24 +02:00
Merge pull request #2989 from Harikrishnan1367709/Better-deployment-logs-for-long-commit-message-#2973
feat: Add expandable commit messages for deployment logs
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user