mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'canary' into feat/add-concurrent-builds
This commit is contained in:
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -6,9 +6,9 @@ Please describe in a short paragraph what this PR is about.
|
|||||||
|
|
||||||
Before submitting this PR, please make sure that:
|
Before submitting this PR, please make sure that:
|
||||||
|
|
||||||
- [] You created a dedicated branch based on the `canary` branch.
|
- [ ] You created a dedicated branch based on the `canary` branch.
|
||||||
- [] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
- [ ] You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
|
||||||
- [] You have tested this PR in your local instance.
|
- [ ] You have tested this PR in your local instance.
|
||||||
|
|
||||||
## Issues related (if applicable)
|
## Issues related (if applicable)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -44,7 +45,6 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -52,12 +52,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { TimeBadge } from "@/components/ui/time-badge";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { HandleProject } from "./handle-project";
|
import { HandleProject } from "./handle-project";
|
||||||
import { ProjectEnvironment } from "./project-environment";
|
import { ProjectEnvironment } from "./project-environment";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { data, isLoading } = api.project.all.useQuery();
|
const { data, isLoading } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.user.get.useQuery();
|
const { data: auth } = api.user.get.useQuery();
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
@@ -135,6 +137,11 @@ export const ShowProjects = () => {
|
|||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
/>
|
/>
|
||||||
|
{!isCloud && (
|
||||||
|
<div className="absolute top-5 right-5">
|
||||||
|
<TimeBadge />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl ">
|
||||||
<div className="rounded-xl bg-background shadow-md ">
|
<div className="rounded-xl bg-background shadow-md ">
|
||||||
@@ -148,7 +155,6 @@ export const ShowProjects = () => {
|
|||||||
Create and manage your projects
|
Create and manage your projects
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
{(auth?.role === "owner" || auth?.canCreateProjects) && (
|
||||||
<div className="">
|
<div className="">
|
||||||
<HandleProject />
|
<HandleProject />
|
||||||
@@ -298,7 +304,13 @@ export const ShowProjects = () => {
|
|||||||
<Link
|
<Link
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${
|
||||||
|
domain.https
|
||||||
|
? "https"
|
||||||
|
: "http"
|
||||||
|
}://${domain.host}${
|
||||||
|
domain.path
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{domain.host}
|
{domain.host}
|
||||||
@@ -340,7 +352,13 @@ export const ShowProjects = () => {
|
|||||||
<Link
|
<Link
|
||||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
href={`${domain.https ? "https" : "http"}://${domain.host}${domain.path}`}
|
href={`${
|
||||||
|
domain.https
|
||||||
|
? "https"
|
||||||
|
: "http"
|
||||||
|
}://${domain.host}${
|
||||||
|
domain.path
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate">
|
<span className="truncate">
|
||||||
{domain.host}
|
{domain.host}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import { AddOrganization } from "../dashboard/organization/handle-organization";
|
|||||||
import { DialogAction } from "../shared/dialog-action";
|
import { DialogAction } from "../shared/dialog-action";
|
||||||
import { Logo } from "../shared/logo";
|
import { Logo } from "../shared/logo";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
|
import { TimeBadge } from "../ui/time-badge";
|
||||||
import { UpdateServerButton } from "./update-server";
|
import { UpdateServerButton } from "./update-server";
|
||||||
import { UserNav } from "./user-nav";
|
import { UserNav } from "./user-nav";
|
||||||
|
|
||||||
@@ -1125,6 +1126,7 @@ export default function Page({ children }: Props) {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
|
{!isCloud && <TimeBadge />}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)}
|
)}
|
||||||
|
|||||||
58
apps/dokploy/components/ui/time-badge.tsx
Normal file
58
apps/dokploy/components/ui/time-badge.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
export function TimeBadge() {
|
||||||
|
const { data: serverTime } = api.server.getServerTime.useQuery(undefined);
|
||||||
|
const [time, setTime] = useState<Date | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (serverTime?.time) {
|
||||||
|
setTime(new Date(serverTime.time));
|
||||||
|
}
|
||||||
|
}, [serverTime]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setTime((prevTime) => {
|
||||||
|
if (!prevTime) return null;
|
||||||
|
const newTime = new Date(prevTime.getTime() + 1000);
|
||||||
|
return newTime;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(timer);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!time || !serverTime?.timezone) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUtcOffset = (timeZone: string) => {
|
||||||
|
const date = new Date();
|
||||||
|
const utcDate = new Date(date.toLocaleString("en-US", { timeZone: "UTC" }));
|
||||||
|
const tzDate = new Date(date.toLocaleString("en-US", { timeZone }));
|
||||||
|
const offset = (tzDate.getTime() - utcDate.getTime()) / (1000 * 60 * 60);
|
||||||
|
const sign = offset >= 0 ? "+" : "-";
|
||||||
|
const hours = Math.floor(Math.abs(offset));
|
||||||
|
const minutes = (Math.abs(offset) * 60) % 60;
|
||||||
|
return `UTC${sign}${hours.toString().padStart(2, "0")}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-md border px-2 py-1 text-xs sm:text-sm whitespace-nowrap max-w-full overflow-hidden">
|
||||||
|
<span className="hidden sm:inline">Server Time:</span>
|
||||||
|
<span className="font-medium tabular-nums">
|
||||||
|
{time.toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
<span className="hidden sm:inline text-muted-foreground">
|
||||||
|
({serverTime.timezone} | {getUtcOffset(serverTime.timezone)})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { findProjectById } from "@dokploy/server";
|
import type { findEnvironmentById } from "@dokploy/server";
|
||||||
import { validateRequest } from "@dokploy/server/lib/auth";
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import {
|
import {
|
||||||
@@ -102,6 +102,7 @@ import { api } from "@/utils/api";
|
|||||||
export type Services = {
|
export type Services = {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
|
serverName?: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
type:
|
type:
|
||||||
| "mariadb"
|
| "mariadb"
|
||||||
@@ -118,8 +119,7 @@ export type Services = {
|
|||||||
lastDeployDate?: Date | null;
|
lastDeployDate?: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
type Environment = Awaited<ReturnType<typeof findEnvironmentById>>;
|
||||||
type Environment = Project["environments"][0];
|
|
||||||
|
|
||||||
export const extractServicesFromEnvironment = (
|
export const extractServicesFromEnvironment = (
|
||||||
environment: Environment | undefined,
|
environment: Environment | undefined,
|
||||||
@@ -154,6 +154,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
lastDeployDate,
|
lastDeployDate,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -168,6 +169,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const postgres: Services[] =
|
const postgres: Services[] =
|
||||||
@@ -180,6 +182,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const mongo: Services[] =
|
const mongo: Services[] =
|
||||||
@@ -192,6 +195,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const redis: Services[] =
|
const redis: Services[] =
|
||||||
@@ -204,6 +208,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const mysql: Services[] =
|
const mysql: Services[] =
|
||||||
@@ -216,6 +221,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.applicationStatus,
|
status: item.applicationStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
})) || [];
|
})) || [];
|
||||||
|
|
||||||
const compose: Services[] =
|
const compose: Services[] =
|
||||||
@@ -244,6 +250,7 @@ export const extractServicesFromEnvironment = (
|
|||||||
status: item.composeStatus,
|
status: item.composeStatus,
|
||||||
description: item.description,
|
description: item.description,
|
||||||
serverId: item.serverId,
|
serverId: item.serverId,
|
||||||
|
serverName: item?.server?.name || null,
|
||||||
lastDeployDate,
|
lastDeployDate,
|
||||||
};
|
};
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -392,6 +399,7 @@ const EnvironmentPage = (
|
|||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
||||||
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
const [deleteVolumes, setDeleteVolumes] = useState(false);
|
||||||
|
const [selectedServerId, setSelectedServerId] = useState<string>("all");
|
||||||
|
|
||||||
const handleSelectAll = () => {
|
const handleSelectAll = () => {
|
||||||
if (selectedServices.length === filteredServices.length) {
|
if (selectedServices.length === filteredServices.length) {
|
||||||
@@ -781,6 +789,27 @@ const EnvironmentPage = (
|
|||||||
setIsBulkActionLoading(false);
|
setIsBulkActionLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get unique servers from services
|
||||||
|
const availableServers = useMemo(() => {
|
||||||
|
if (!applications) return [];
|
||||||
|
const servers = new Map<string, { serverId: string; serverName: string }>();
|
||||||
|
applications.forEach((service) => {
|
||||||
|
if (service.serverId && service.serverName) {
|
||||||
|
servers.set(service.serverId, {
|
||||||
|
serverId: service.serverId,
|
||||||
|
serverName: service.serverName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Array.from(servers.values());
|
||||||
|
}, [applications]);
|
||||||
|
|
||||||
|
// Check if there are services without a server (Dokploy server)
|
||||||
|
const hasServicesWithoutServer = useMemo(() => {
|
||||||
|
if (!applications) return false;
|
||||||
|
return applications.some((service) => !service.serverId);
|
||||||
|
}, [applications]);
|
||||||
|
|
||||||
const filteredServices = useMemo(() => {
|
const filteredServices = useMemo(() => {
|
||||||
if (!applications) return [];
|
if (!applications) return [];
|
||||||
const filtered = applications.filter(
|
const filtered = applications.filter(
|
||||||
@@ -789,10 +818,14 @@ const EnvironmentPage = (
|
|||||||
service.description
|
service.description
|
||||||
?.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes(searchQuery.toLowerCase())) &&
|
.includes(searchQuery.toLowerCase())) &&
|
||||||
(selectedTypes.length === 0 || selectedTypes.includes(service.type)),
|
(selectedTypes.length === 0 || selectedTypes.includes(service.type)) &&
|
||||||
|
(selectedServerId === "" ||
|
||||||
|
selectedServerId === "all" ||
|
||||||
|
(selectedServerId === "dokploy-server" && !service.serverId) ||
|
||||||
|
service.serverId === selectedServerId),
|
||||||
);
|
);
|
||||||
return sortServices(filtered);
|
return sortServices(filtered);
|
||||||
}, [applications, searchQuery, selectedTypes, sortBy]);
|
}, [applications, searchQuery, selectedTypes, selectedServerId, sortBy]);
|
||||||
|
|
||||||
const selectedServicesWithRunningStatus = useMemo(() => {
|
const selectedServicesWithRunningStatus = useMemo(() => {
|
||||||
return filteredServices.filter(
|
return filteredServices.filter(
|
||||||
@@ -1366,6 +1399,39 @@ const EnvironmentPage = (
|
|||||||
</Command>
|
</Command>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
|
{(availableServers.length > 0 ||
|
||||||
|
hasServicesWithoutServer) && (
|
||||||
|
<Select
|
||||||
|
value={selectedServerId || "all"}
|
||||||
|
onValueChange={setSelectedServerId}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="lg:w-[200px]">
|
||||||
|
<SelectValue placeholder="Filter by server..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All servers</SelectItem>
|
||||||
|
{hasServicesWithoutServer && (
|
||||||
|
<SelectItem value="dokploy-server">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ServerIcon className="size-4" />
|
||||||
|
<span>Dokploy server</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
)}
|
||||||
|
{availableServers.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ServerIcon className="size-4" />
|
||||||
|
<span>{server.serverName}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1471,7 +1537,15 @@ const EnvironmentPage = (
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="mt-auto">
|
<CardFooter className="mt-auto">
|
||||||
<div className="space-y-1 text-sm">
|
<div className="space-y-1 text-sm w-full">
|
||||||
|
{service.serverName && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-muted-foreground mb-1">
|
||||||
|
<ServerIcon className="size-3" />
|
||||||
|
<span className="truncate">
|
||||||
|
{service.serverName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<DateTooltip date={service.createdAt}>
|
<DateTooltip date={service.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
|
|||||||
@@ -383,6 +383,15 @@ export const serverRouter = createTRPCRouter({
|
|||||||
const ip = await getPublicIpWithFallback();
|
const ip = await getPublicIpWithFallback();
|
||||||
return ip;
|
return ip;
|
||||||
}),
|
}),
|
||||||
|
getServerTime: protectedProcedure.query(() => {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
time: new Date(),
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
};
|
||||||
|
}),
|
||||||
getServerMetrics: protectedProcedure
|
getServerMetrics: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -37,16 +37,38 @@ export const findEnvironmentById = async (environmentId: string) => {
|
|||||||
applications: {
|
applications: {
|
||||||
with: {
|
with: {
|
||||||
deployments: true,
|
deployments: true,
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mariadb: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
with: {
|
||||||
|
server: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mariadb: true,
|
|
||||||
mongo: true,
|
|
||||||
mysql: true,
|
|
||||||
postgres: true,
|
|
||||||
redis: true,
|
|
||||||
compose: {
|
compose: {
|
||||||
with: {
|
with: {
|
||||||
deployments: true,
|
deployments: true,
|
||||||
|
server: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
project: true,
|
project: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export const getPostgresRestoreCommand = (
|
|||||||
database: string,
|
database: string,
|
||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U ${databaseUser} -d ${database} -O --clean --if-exists"`;
|
return `docker exec -i $CONTAINER_ID sh -c "pg_restore -U '${databaseUser}' -d ${database} -O --clean --if-exists"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMariadbRestoreCommand = (
|
export const getMariadbRestoreCommand = (
|
||||||
@@ -15,14 +15,14 @@ export const getMariadbRestoreCommand = (
|
|||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u ${databaseUser} -p${databasePassword} ${database}"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mariadb -u '${databaseUser}' -p'${databasePassword}' ${database}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMysqlRestoreCommand = (
|
export const getMysqlRestoreCommand = (
|
||||||
database: string,
|
database: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p${databasePassword} ${database}"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mysql -u root -p'${databasePassword}' ${database}"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getMongoRestoreCommand = (
|
export const getMongoRestoreCommand = (
|
||||||
@@ -30,7 +30,7 @@ export const getMongoRestoreCommand = (
|
|||||||
databaseUser: string,
|
databaseUser: string,
|
||||||
databasePassword: string,
|
databasePassword: string,
|
||||||
) => {
|
) => {
|
||||||
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username ${databaseUser} --password ${databasePassword} --authenticationDatabase admin --db ${database} --archive"`;
|
return `docker exec -i $CONTAINER_ID sh -c "mongorestore --username '${databaseUser}' --password '${databasePassword}' --authenticationDatabase admin --db ${database} --archive"`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getComposeSearchCommand = (
|
export const getComposeSearchCommand = (
|
||||||
|
|||||||
Reference in New Issue
Block a user