Add ShowDeploymentsModal component and refactor ShowDeployments for enhanced deployment handling

- Introduced `ShowDeploymentsModal` component to manage deployment logs and details in a modal interface.
- Updated `ShowDeployments` to accept `serverId` and `refreshToken` props, allowing for more flexible deployment data handling.
- Refactored API queries in `ShowDeployments` to utilize a unified query for fetching deployments by type, improving code efficiency.
- Replaced instances of `ShowSchedulesLogs` with `ShowDeploymentsModal` in relevant components to streamline deployment log access.
This commit is contained in:
Mauricio Siu 2025-05-04 12:58:46 -06:00
parent 2fa0c7dfd2
commit e09447d4b4
8 changed files with 155 additions and 58 deletions

View File

@ -0,0 +1,63 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import type { RouterOutputs } from "@/utils/api";
import { useState } from "react";
import { ShowDeployment } from "../deployments/show-deployment";
import { ShowDeployments } from "./show-deployments";
interface Props {
id: string;
type: "application" | "compose" | "schedule" | "server" | "backup";
serverId?: string;
refreshToken?: string;
children?: React.ReactNode;
}
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 ShowDeploymentsModal = ({
id,
type,
serverId,
refreshToken,
children,
}: Props) => {
const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null
>(null);
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
{children ? (
children
) : (
<Button className="sm:w-auto w-full" size="sm" variant="outline">
View Logs
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl p-0">
<ShowDeployments
id={id}
type={type}
serverId={serverId}
refreshToken={refreshToken}
/>
</DialogContent>
<ShowDeployment
serverId={serverId || ""}
open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)}
logPath={activeLog?.logPath || ""}
errorMessage={activeLog?.errorMessage || ""}
/>
</Dialog>
);
};

View File

@ -16,30 +16,29 @@ import { RefreshToken } from "./refresh-token";
import { ShowDeployment } from "./show-deployment"; import { ShowDeployment } from "./show-deployment";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { formatDuration } from "../schedules/show-schedules-logs"; import { formatDuration } from "../schedules/show-schedules-logs";
interface Props { interface Props {
id: string; id: string;
type: "application" | "compose"; type: "application" | "compose" | "schedule" | "server" | "backup";
refreshToken?: string;
serverId?: string;
} }
export const ShowDeployments = ({ id, type }: Props) => { export const ShowDeployments = ({
id,
type,
refreshToken,
serverId,
}: Props) => {
const [activeLog, setActiveLog] = useState< const [activeLog, setActiveLog] = useState<
RouterOutputs["deployment"]["all"][number] | null RouterOutputs["deployment"]["all"][number] | null
>(null); >(null);
const { data } =
type === "application"
? api.application.one.useQuery({ applicationId: id })
: api.compose.one.useQuery({ composeId: id });
const { data: deployments, isLoading: isLoadingDeployments } = const { data: deployments, isLoading: isLoadingDeployments } =
type === "application" api.deployment.allByType.useQuery(
? api.deployment.all.useQuery(
{ applicationId: id },
{ {
enabled: !!id, id,
refetchInterval: 1000, type,
}, },
)
: api.deployment.allByCompose.useQuery(
{ composeId: id },
{ {
enabled: !!id, enabled: !!id,
refetchInterval: 1000, refetchInterval: 1000,
@ -52,7 +51,7 @@ export const ShowDeployments = ({ id, type }: Props) => {
}, []); }, []);
return ( return (
<Card className="bg-background"> <Card className="bg-background border-none">
<CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2"> <CardHeader className="flex flex-row items-center justify-between flex-wrap gap-2">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<CardTitle className="text-xl">Deployments</CardTitle> <CardTitle className="text-xl">Deployments</CardTitle>
@ -60,24 +59,27 @@ export const ShowDeployments = ({ id, type }: Props) => {
See all the 10 last deployments for this {type} See all the 10 last deployments for this {type}
</CardDescription> </CardDescription>
</div> </div>
<CancelQueues id={id} type={type} /> {refreshToken && <CancelQueues id={id} type={type} />}
</CardHeader> </CardHeader>
<CardContent className="flex flex-col gap-4"> <CardContent className="flex flex-col gap-4">
{refreshToken && (
<div className="flex flex-col gap-2 text-sm"> <div className="flex flex-col gap-2 text-sm">
<span> <span>
If you want to re-deploy this application use this URL in the config If you want to re-deploy this application use this URL in the
of your git provider or docker config of your git provider or docker
</span> </span>
<div className="flex flex-row items-center gap-2 flex-wrap"> <div className="flex flex-row items-center gap-2 flex-wrap">
<span>Webhook URL: </span> <span>Webhook URL: </span>
<div className="flex flex-row items-center gap-2"> <div className="flex flex-row items-center gap-2">
<span className="break-all text-muted-foreground"> <span className="break-all text-muted-foreground">
{`${url}/api/deploy/${data?.refreshToken}`} {`${url}/api/deploy/${refreshToken}`}
</span> </span>
<RefreshToken id={id} type={type} /> <RefreshToken id={id} type={type} />
</div> </div>
</div> </div>
</div> </div>
)}
{isLoadingDeployments ? ( {isLoadingDeployments ? (
<div className="flex w-full flex-row items-center justify-center gap-3 pt-10 min-h-[25vh]"> <div className="flex w-full flex-row items-center justify-center gap-3 pt-10 min-h-[25vh]">
<Loader2 className="size-6 text-muted-foreground animate-spin" /> <Loader2 className="size-6 text-muted-foreground animate-spin" />
@ -85,7 +87,7 @@ export const ShowDeployments = ({ id, type }: Props) => {
Loading deployments... Loading deployments...
</span> </span>
</div> </div>
) : data?.deployments?.length === 0 ? ( ) : deployments?.length === 0 ? (
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10 min-h-[25vh]"> <div className="flex w-full flex-col items-center justify-center gap-3 pt-10 min-h-[25vh]">
<RocketIcon className="size-8 text-muted-foreground" /> <RocketIcon className="size-8 text-muted-foreground" />
<span className="text-base text-muted-foreground"> <span className="text-base text-muted-foreground">
@ -149,7 +151,7 @@ export const ShowDeployments = ({ id, type }: Props) => {
</div> </div>
)} )}
<ShowDeployment <ShowDeployment
serverId={data?.serverId || ""} serverId={serverId}
open={Boolean(activeLog && activeLog.logPath !== null)} open={Boolean(activeLog && activeLog.logPath !== null)}
onClose={() => setActiveLog(null)} onClose={() => setActiveLog(null)}
logPath={activeLog?.logPath || ""} logPath={activeLog?.logPath || ""}

View File

@ -18,7 +18,6 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { toast } from "sonner"; import { toast } from "sonner";
import { ShowSchedulesLogs } from "./show-schedules-logs";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@ -26,6 +25,7 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import { DialogAction } from "@/components/shared/dialog-action"; import { DialogAction } from "@/components/shared/dialog-action";
import { ShowDeploymentsModal } from "../deployments/show-deployments-modal";
interface Props { interface Props {
id: string; id: string;
@ -88,7 +88,6 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
schedule.serverId || schedule.serverId ||
schedule.application?.serverId || schedule.application?.serverId ||
schedule.compose?.serverId; schedule.compose?.serverId;
const deployments = schedule.deployments;
return ( return (
<div <div
key={schedule.scheduleId} key={schedule.scheduleId}
@ -144,14 +143,15 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ShowSchedulesLogs <ShowDeploymentsModal
deployments={deployments || []} id={schedule.scheduleId}
type="schedule"
serverId={serverId || undefined} serverId={serverId || undefined}
> >
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<ClipboardList className="size-4 transition-colors " /> <ClipboardList className="size-4 transition-colors " />
</Button> </Button>
</ShowSchedulesLogs> </ShowDeploymentsModal>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>

View File

@ -35,7 +35,7 @@ import {
PostgresqlIcon, PostgresqlIcon,
} from "@/components/icons/data-tools-icons"; } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block"; import { AlertBlock } from "@/components/shared/alert-block";
import { ShowSchedulesLogs } from "../../application/schedules/show-schedules-logs"; import { ShowDeploymentsModal } from "../../application/deployments/show-deployments-modal";
interface Props { interface Props {
id: string; id: string;
@ -179,12 +179,6 @@ export const ShowBackups = ({
)} )}
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{postgres?.backups.map((backup) => { {postgres?.backups.map((backup) => {
const orderedDeployments = backup.deployments.sort(
(a, b) =>
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime(),
);
const serverId = const serverId =
"serverId" in postgres ? postgres.serverId : undefined; "serverId" in postgres ? postgres.serverId : undefined;
@ -285,8 +279,9 @@ export const ShowBackups = ({
</div> </div>
<div className="flex flex-row md:flex-col gap-1.5"> <div className="flex flex-row md:flex-col gap-1.5">
<ShowSchedulesLogs <ShowDeploymentsModal
deployments={orderedDeployments} id={backup.backupId}
type="backup"
serverId={serverId || undefined} serverId={serverId || undefined}
> >
<Button <Button
@ -296,7 +291,7 @@ export const ShowBackups = ({
> >
<ClipboardList className="size-4 transition-colors " /> <ClipboardList className="size-4 transition-colors " />
</Button> </Button>
</ShowSchedulesLogs> </ShowDeploymentsModal>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>

View File

@ -318,9 +318,14 @@ const Service = (
/> />
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="deployments" className="w-full"> <TabsContent value="deployments" className="w-full pt-2.5">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 border rounded-lg">
<ShowDeployments id={applicationId} type="application" /> <ShowDeployments
id={applicationId}
type="application"
serverId={data?.serverId || ""}
refreshToken={data?.refreshToken || ""}
/>
</div> </div>
</TabsContent> </TabsContent>
<TabsContent value="preview-deployments" className="w-full"> <TabsContent value="preview-deployments" className="w-full">

View File

@ -6,7 +6,6 @@ import { ShowEnvironment } from "@/components/dashboard/application/environment/
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules"; import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command"; import { AddCommandCompose } from "@/components/dashboard/compose/advanced/add-command";
import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { DeleteService } from "@/components/dashboard/compose/delete-service";
import { ShowDeploymentsCompose } from "@/components/dashboard/compose/deployments/show-deployments-compose";
import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show"; import { ShowGeneralCompose } from "@/components/dashboard/compose/general/show";
import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show"; import { ShowDockerLogsCompose } from "@/components/dashboard/compose/logs/show";
import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack"; import { ShowDockerLogsStack } from "@/components/dashboard/compose/logs/show-stack";
@ -334,7 +333,12 @@ const Service = (
<TabsContent value="deployments"> <TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5"> <div className="flex flex-col gap-4 pt-2.5">
<ShowDeployments id={composeId} type="compose" /> <ShowDeployments
id={composeId}
type="compose"
serverId={data?.serverId || ""}
refreshToken={data?.refreshToken || ""}
/>
</div> </div>
</TabsContent> </TabsContent>

View File

@ -2,6 +2,8 @@ import {
apiFindAllByApplication, apiFindAllByApplication,
apiFindAllByCompose, apiFindAllByCompose,
apiFindAllByServer, apiFindAllByServer,
apiFindAllByType,
deployments,
} from "@/server/db/schema"; } from "@/server/db/schema";
import { import {
findAllDeploymentsByApplicationId, findAllDeploymentsByApplicationId,
@ -12,7 +14,9 @@ import {
findServerById, findServerById,
} from "@dokploy/server"; } from "@dokploy/server";
import { TRPCError } from "@trpc/server"; import { TRPCError } from "@trpc/server";
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, protectedProcedure } from "../trpc"; import { createTRPCRouter, protectedProcedure } from "../trpc";
import { db } from "@/server/db";
export const deploymentRouter = createTRPCRouter({ export const deploymentRouter = createTRPCRouter({
all: protectedProcedure all: protectedProcedure
@ -54,4 +58,14 @@ export const deploymentRouter = createTRPCRouter({
} }
return await findAllDeploymentsByServerId(input.serverId); return await findAllDeploymentsByServerId(input.serverId);
}), }),
allByType: protectedProcedure
.input(apiFindAllByType)
.query(async ({ input }) => {
const deploymentsList = await db.query.deployments.findMany({
where: eq(deployments[`${input.type}Id`], input.id),
orderBy: desc(deployments.createdAt),
});
return deploymentsList;
}),
}); });

View File

@ -195,3 +195,17 @@ export const apiFindAllByServer = schema
serverId: z.string().min(1), serverId: z.string().min(1),
}) })
.required(); .required();
export const apiFindAllByType = z
.object({
id: z.string().min(1),
type: z.enum([
"application",
"compose",
"server",
"schedule",
"previewDeployment",
"backup",
]),
})
.required();