diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 380b22d93..39b09d0ca 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -1,3 +1,5 @@ +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -5,11 +7,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../../docker/logs/terminal-line"; -import { LogLine, parseLogs } from "../../docker/logs/utils"; -import { Badge } from "@/components/ui/badge"; -import { Loader2 } from "lucide-react"; +import { type LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; @@ -19,26 +20,26 @@ interface Props { } export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { const [data, setData] = useState(""); + const [showExtraLogs, setShowExtraLogs] = useState(false); const [filteredLogs, setFilteredLogs] = useState([]); const wsRef = useRef(null); // Ref to hold WebSocket instance const [autoScroll, setAutoScroll] = useState(true); const scrollRef = useRef(null); - const scrollToBottom = () => { if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }; - + }; + const handleScroll = () => { if (!scrollRef.current) return; - + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; setAutoScroll(isAtBottom); - }; - + }; + useEffect(() => { if (!open || !logPath) return; @@ -69,20 +70,34 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => { }; }, [logPath, open]); - useEffect(() => { const logs = parseLogs(data); - setFilteredLogs(logs); - }, [data]); + let filteredLogsResult = logs; + if (serverId) { + let hideSubsequentLogs = false; + filteredLogsResult = logs.filter((log) => { + if ( + log.message.includes( + "===================================EXTRA LOGS============================================", + ) + ) { + hideSubsequentLogs = true; + return showExtraLogs; + } + return showExtraLogs ? true : !hideSubsequentLogs; + }); + } + + setFilteredLogs(filteredLogsResult); + }, [data, showExtraLogs]); useEffect(() => { scrollToBottom(); - + if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [filteredLogs, autoScroll]); - + }, [filteredLogs, autoScroll]); return ( { Deployment - - See all the details of this deployment | {filteredLogs.length} lines + + + See all the details of this deployment |{" "} + + {filteredLogs.length} lines + + + + {serverId && ( +
+ + setShowExtraLogs(checked as boolean) + } + /> + +
+ )}
-
{ - filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( - - )) : - ( -
- -
- )} + > + {" "} + {filteredLogs.length > 0 ? ( + filteredLogs.map((log: LogLine, index: number) => ( + + )) + ) : ( +
+ +
+ )}
diff --git a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx index 4b5d4e09b..38180114d 100644 --- a/apps/dokploy/components/dashboard/application/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/application/domains/add-domain.tsx @@ -264,21 +264,21 @@ export const AddDomain = ({ name="certificateType" render={({ field }) => ( - Certificate + Certificate Provider diff --git a/apps/dokploy/components/dashboard/application/logs/show.tsx b/apps/dokploy/components/dashboard/application/logs/show.tsx index dba3666c7..a73b99d25 100644 --- a/apps/dokploy/components/dashboard/application/logs/show.tsx +++ b/apps/dokploy/components/dashboard/application/logs/show.tsx @@ -1,3 +1,4 @@ +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, @@ -15,6 +16,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { Loader2 } from "lucide-react"; import dynamic from "next/dynamic"; @@ -29,28 +31,67 @@ export const DockerLogs = dynamic( }, ); +export const badgeStateColor = (state: string) => { + switch (state) { + case "running": + return "green"; + case "exited": + case "shutdown": + return "red"; + case "accepted": + case "created": + return "blue"; + default: + return "default"; + } +}; + interface Props { appName: string; serverId?: string; } export const ShowDockerLogs = ({ appName, serverId }: Props) => { - const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery( - { - appName, - serverId, - }, - { - enabled: !!appName, - }, - ); const [containerId, setContainerId] = useState(); + const [option, setOption] = useState<"swarm" | "native">("native"); + + const { data: services, isLoading: servicesLoading } = + api.docker.getServiceContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "swarm", + }, + ); + + const { data: containers, isLoading: containersLoading } = + api.docker.getContainersByAppNameMatch.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "native", + }, + ); useEffect(() => { - if (data && data?.length > 0) { - setContainerId(data[0]?.containerId); + if (option === "native") { + if (containers && containers?.length > 0) { + setContainerId(containers[0]?.containerId); + } + } else { + if (services && services?.length > 0) { + setContainerId(services[0]?.containerId); + } } - }, [data]); + }, [option, services, containers]); + + const isLoading = option === "native" ? containersLoading : servicesLoading; + const containersLenght = + option === "native" ? containers?.length : services?.length; return ( @@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => { - +
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + /> +
+
+
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx index e8a259d15..875a93d49 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/add-preview-domain.tsx @@ -265,21 +265,21 @@ export const AddPreviewDomain = ({ name="certificateType" render={({ field }) => ( - Certificate + Certificate Provider diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx index 4eb2107f6..527d76cce 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-builds.tsx @@ -18,15 +18,26 @@ import { ShowDeployment } from "../deployments/show-deployment"; interface Props { deployments: RouterOutputs["deployment"]["all"]; serverId?: string; + trigger?: React.ReactNode; } -export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => { +export const ShowPreviewBuilds = ({ + deployments, + serverId, + trigger, +}: Props) => { const [activeLog, setActiveLog] = useState(null); const [isOpen, setIsOpen] = useState(false); return ( - + {trigger ? ( + trigger + ) : ( + + )} diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx index 45451e78f..371276bdd 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-deployments.tsx @@ -1,5 +1,8 @@ +import { GithubIcon } from "@/components/icons/data-tools-icons"; 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, @@ -8,30 +11,34 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Switch } from "@/components/ui/switch"; +import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; -import { Pencil, RocketIcon } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import { + ExternalLink, + FileText, + GitPullRequest, + Layers, + PenSquare, + RocketIcon, + Trash2, +} from "lucide-react"; +import React from "react"; import { toast } from "sonner"; -import { ShowDeployment } from "../deployments/show-deployment"; -import Link from "next/link"; import { ShowModalLogs } from "../../settings/web-server/show-modal-logs"; -import { DialogAction } from "@/components/shared/dialog-action"; import { AddPreviewDomain } from "./add-preview-domain"; -import { GithubIcon } from "@/components/icons/data-tools-icons"; -import { ShowPreviewSettings } from "./show-preview-settings"; import { ShowPreviewBuilds } from "./show-preview-builds"; +import { ShowPreviewSettings } from "./show-preview-settings"; interface Props { applicationId: string; } export const ShowPreviewDeployments = ({ applicationId }: Props) => { - const [activeLog, setActiveLog] = useState(null); const { data } = api.application.one.useQuery({ applicationId }); const { mutateAsync: deletePreviewDeployment, isLoading } = api.previewDeployment.delete.useMutation(); + const { data: previewDeployments, refetch: refetchPreviewDeployments } = api.previewDeployment.all.useQuery( { applicationId }, @@ -39,10 +46,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { enabled: !!applicationId, }, ); - // const [url, setUrl] = React.useState(""); - // useEffect(() => { - // setUrl(document.location.origin); - // }, []); + + const handleDeletePreviewDeployment = async (previewDeploymentId: string) => { + deletePreviewDeployment({ + previewDeploymentId: previewDeploymentId, + }) + .then(() => { + refetchPreviewDeployments(); + toast.success("Preview deployment deleted"); + }) + .catch((error) => { + toast.error(error.message); + }); + }; return ( @@ -65,7 +81,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => { each pull request you create. - {data?.previewDeployments?.length === 0 ? ( + {!previewDeployments?.length ? (
@@ -74,120 +90,131 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
) : (
- {previewDeployments?.map((previewDeployment) => { - const { deployments, domain } = previewDeployment; - + {previewDeployments?.map((deployment) => { + const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`; + const status = deployment.previewStatus; return (
-
-
- {deployments?.length === 0 ? ( +
+ +
+
+
+
- - No deployments found - -
- ) : ( -
- - {previewDeployment?.pullRequestTitle} - - -
- )} -
- {previewDeployment?.pullRequestTitle && ( -
- - Title: {previewDeployment?.pullRequestTitle} - +
+ {deployment.pullRequestTitle}
- )} - - {previewDeployment?.pullRequestURL && ( -
- - - Pull Request URL - +
+ {deployment.branch}
- )} -
-
- Domain -
- - {domain?.host} - - - -
+ + + +
-
- {previewDeployment?.createdAt && ( -
- -
- )} - - - - - +
+
+ + window.open(deploymentUrl, "_blank") + } + /> + +
- { - deletePreviewDeployment({ - previewDeploymentId: - previewDeployment.previewDeploymentId, - }) - .then(() => { - refetchPreviewDeployments(); - toast.success("Preview deployment deleted"); - }) - .catch((error) => { - toast.error(error.message); - }); - }} - > - - + + + + + + + Builds + + } + /> + + + + + + handleDeletePreviewDeployment( + deployment.previewDeploymentId, + ) + } + > + + +
diff --git a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx index 6e56bbdd0..bf402457e 100644 --- a/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx +++ b/apps/dokploy/components/dashboard/application/preview-deployments/show-preview-settings.tsx @@ -1,5 +1,3 @@ -import { api } from "@/utils/api"; -import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -20,12 +18,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input, NumberInput } from "@/components/ui/input"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; import { Secrets } from "@/components/ui/secrets"; -import { toast } from "sonner"; -import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, @@ -33,6 +26,14 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Settings2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; const schema = z.object({ env: z.string(), @@ -116,7 +117,10 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
- + @@ -218,21 +222,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => { name="previewCertificateType" render={({ field }) => ( - Certificate + Certificate Provider diff --git a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx index 44ce15c04..6d1b455f9 100644 --- a/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx +++ b/apps/dokploy/components/dashboard/compose/advanced/add-command.tsx @@ -1,3 +1,4 @@ +import { AlertBlock } from "@/components/shared/alert-block"; import { Button } from "@/components/ui/button"; import { Card, @@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
Run Command - Append a custom command to the compose file + Override a custom command to the compose file
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => { onSubmit={form.handleSubmit(onSubmit)} className="grid w-full gap-4" > + + Modifying the default command may affect deployment stability, + impacting logs and monitoring. Proceed carefully and test + thoroughly. By default, the command starts with{" "} + docker. +
; @@ -51,6 +54,7 @@ export const DeleteCompose = ({ composeId }: Props) => { const form = useForm({ defaultValues: { projectName: "", + deleteVolumes: false, }, resolver: zodResolver(deleteComposeSchema), }); @@ -58,7 +62,8 @@ export const DeleteCompose = ({ composeId }: Props) => { const onSubmit = async (formData: DeleteCompose) => { const expectedName = `${data?.name}/${data?.appName}`; if (formData.projectName === expectedName) { - await mutateAsync({ composeId }) + const { deleteVolumes } = formData; + await mutateAsync({ composeId, deleteVolumes }) .then((result) => { push(`/dashboard/project/${result?.projectId}`); toast.success("Compose deleted successfully"); @@ -133,6 +138,27 @@ export const DeleteCompose = ({ composeId }: Props) => { )} /> + ( + +
+ + + + + + Delete volumes associated with this compose + +
+ +
+ )} + />
diff --git a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx index 4a45fb204..45869ed2b 100644 --- a/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/deployments/show-deployment-compose.tsx @@ -1,3 +1,5 @@ +import { Badge } from "@/components/ui/badge"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -5,12 +7,10 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { Loader2 } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { TerminalLine } from "../../docker/logs/terminal-line"; -import { LogLine, parseLogs } from "../../docker/logs/utils"; -import { Badge } from "@/components/ui/badge"; -import { Loader2 } from "lucide-react"; - +import { type LogLine, parseLogs } from "../../docker/logs/utils"; interface Props { logPath: string | null; @@ -26,25 +26,25 @@ export const ShowDeploymentCompose = ({ }: Props) => { const [data, setData] = useState(""); const [filteredLogs, setFilteredLogs] = useState([]); + const [showExtraLogs, setShowExtraLogs] = useState(false); const wsRef = useRef(null); // Ref to hold WebSocket instance const [autoScroll, setAutoScroll] = useState(true); const scrollRef = useRef(null); const scrollToBottom = () => { if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }; - + }; + const handleScroll = () => { if (!scrollRef.current) return; - + const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; setAutoScroll(isAtBottom); - }; - - + }; + useEffect(() => { if (!open || !logPath) return; @@ -76,19 +76,34 @@ export const ShowDeploymentCompose = ({ }; }, [logPath, open]); - useEffect(() => { const logs = parseLogs(data); - setFilteredLogs(logs); - }, [data]); + let filteredLogsResult = logs; + if (serverId) { + let hideSubsequentLogs = false; + filteredLogsResult = logs.filter((log) => { + if ( + log.message.includes( + "===================================EXTRA LOGS============================================", + ) + ) { + hideSubsequentLogs = true; + return showExtraLogs; + } + return showExtraLogs ? true : !hideSubsequentLogs; + }); + } + + setFilteredLogs(filteredLogsResult); + }, [data, showExtraLogs]); useEffect(() => { scrollToBottom(); - + if (autoScroll && scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } - }, [filteredLogs, autoScroll]); + }, [filteredLogs, autoScroll]); return ( Deployment - - See all the details of this deployment | {filteredLogs.length} lines + + + See all the details of this deployment |{" "} + + {filteredLogs.length} lines + + + {serverId && ( +
+ + setShowExtraLogs(checked as boolean) + } + /> + +
+ )}
-
- - - { - filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( - - )) : - ( + {filteredLogs.length > 0 ? ( + filteredLogs.map((log: LogLine, index: number) => ( + + )) + ) : (
- ) - } + )}
diff --git a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx index 9f586467b..6ea38237f 100644 --- a/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx +++ b/apps/dokploy/components/dashboard/compose/domains/add-domain.tsx @@ -400,21 +400,21 @@ export const AddDomainCompose = ({ name="certificateType" render={({ field }) => ( - Certificate + Certificate Provider diff --git a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx index c02a78028..25b59cc73 100644 --- a/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx +++ b/apps/dokploy/components/dashboard/compose/general/deploy-compose.tsx @@ -53,7 +53,7 @@ export const DeployCompose = ({ composeId }: Props) => { }) .then(async () => { router.push( - `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments` + `/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`, ); }) .catch(() => { diff --git a/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx new file mode 100644 index 000000000..d166f933f --- /dev/null +++ b/apps/dokploy/components/dashboard/compose/logs/show-stack.tsx @@ -0,0 +1,165 @@ +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { api } from "@/utils/api"; +import { Loader2 } from "lucide-react"; +import dynamic from "next/dynamic"; +import { useEffect, useState } from "react"; +export const DockerLogs = dynamic( + () => + import("@/components/dashboard/docker/logs/docker-logs-id").then( + (e) => e.DockerLogsId, + ), + { + ssr: false, + }, +); + +interface Props { + appName: string; + serverId?: string; +} + +badgeStateColor; + +export const ShowDockerLogsStack = ({ appName, serverId }: Props) => { + const [option, setOption] = useState<"swarm" | "native">("native"); + const [containerId, setContainerId] = useState(); + + const { data: services, isLoading: servicesLoading } = + api.docker.getStackContainersByAppName.useQuery( + { + appName, + serverId, + }, + { + enabled: !!appName && option === "swarm", + }, + ); + + const { data: containers, isLoading: containersLoading } = + api.docker.getContainersByAppNameMatch.useQuery( + { + appName, + appType: "stack", + serverId, + }, + { + enabled: !!appName && option === "native", + }, + ); + + useEffect(() => { + if (option === "native") { + if (containers && containers?.length > 0) { + setContainerId(containers[0]?.containerId); + } + } else { + if (services && services?.length > 0) { + setContainerId(services[0]?.containerId); + } + } + }, [option, services, containers]); + + const isLoading = option === "native" ? containersLoading : servicesLoading; + const containersLenght = + option === "native" ? containers?.length : services?.length; + + return ( + + + Logs + + Watch the logs of the application in real time + + + + +
+ +
+ + {option === "native" ? "Native" : "Swarm"} + + { + setOption(checked ? "native" : "swarm"); + }} + /> +
+
+ + +
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index 6b39f4137..4530e0ddd 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -1,3 +1,5 @@ +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, @@ -87,7 +89,10 @@ export const ShowDockerLogsCompose = ({ key={container.containerId} value={container.containerId} > - {container.name} ({container.containerId}) {container.state} + {container.name} ({container.containerId}){" "} + + {container.state} + ))} Containers ({data?.length}) @@ -97,6 +102,7 @@ export const ShowDockerLogsCompose = ({ diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 3f30c292c..1fd8cea48 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -12,6 +12,7 @@ import { type LogLine, getLogType, parseLogs } from "./utils"; interface Props { containerId: string; serverId?: string | null; + runType: "swarm" | "native"; } export const priorities = [ @@ -37,7 +38,11 @@ export const priorities = [ }, ]; -export const DockerLogsId: React.FC = ({ containerId, serverId }) => { +export const DockerLogsId: React.FC = ({ + containerId, + serverId, + runType, +}) => { const { data } = api.docker.getConfig.useQuery( { containerId, @@ -104,6 +109,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { tail: lines.toString(), since, search, + runType, }); if (serverId) { diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx index f8531d774..619b25d0c 100644 --- a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-logs.tsx @@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({ View the logs for {containerId}
- +
diff --git a/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx new file mode 100644 index 000000000..36719bb07 --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/show-docker-modal-stack-logs.tsx @@ -0,0 +1,58 @@ +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import dynamic from "next/dynamic"; +import type React from "react"; +export const DockerLogsId = dynamic( + () => + import("@/components/dashboard/docker/logs/docker-logs-id").then( + (e) => e.DockerLogsId, + ), + { + ssr: false, + }, +); + +interface Props { + containerId: string; + children?: React.ReactNode; + serverId?: string | null; +} + +export const ShowDockerModalStackLogs = ({ + containerId, + children, + serverId, +}: Props) => { + return ( + + + e.preventDefault()} + > + {children} + + + + + View Logs + View the logs for {containerId} + +
+ +
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index 2f247e259..c25acc67f 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -7,9 +7,10 @@ import { TooltipTrigger, } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; +import { FancyAnsi } from "fancy-ansi"; import { escapeRegExp } from "lodash"; import React from "react"; -import { type LogLine, getLogType, parseAnsi } from "./utils"; +import { type LogLine, getLogType } from "./utils"; interface LogLineProps { log: LogLine; @@ -17,6 +18,8 @@ interface LogLineProps { searchTerm?: string; } +const fancyAnsi = new FancyAnsi(); + export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { const { timestamp, message, rawTimestamp } = log; const { type, variant, color } = getLogType(message); @@ -34,37 +37,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { const highlightMessage = (text: string, term: string) => { if (!term) { - const segments = parseAnsi(text); - return segments.map((segment, index) => ( - - {segment.text} - - )); - } - - // For search, we need to handle both ANSI and search highlighting - const segments = parseAnsi(text); - return segments.map((segment, index) => { - const parts = segment.text.split( - new RegExp(`(${escapeRegExp(term)})`, "gi"), - ); return ( - - {parts.map((part, partIndex) => - part.toLowerCase() === term.toLowerCase() ? ( - - {part} - - ) : ( - part - ), - )} - + ); - }); + } + + const htmlContent = fancyAnsi.toHtml(text); + const modifiedContent = htmlContent.replace( + /]*)>([^<]*)<\/span>/g, + (match, attrs, content) => { + const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi"); + if (!content.match(searchRegex)) return match; + + const segments = content.split(searchRegex); + const wrappedSegments = segments + .map((segment: string) => + segment.toLowerCase() === term.toLowerCase() + ? `${segment}` + : segment, + ) + .join(""); + + return `${wrappedSegments}`; + }, + ); + + return ( + + ); }; const tooltip = (color: string, timestamp: string | null) => { diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 482194287..cf0b30bbd 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -12,47 +12,6 @@ interface LogStyle { variant: LogVariant; color: string; } -interface AnsiSegment { - text: string; - className: string; -} - -const ansiToTailwind: Record = { - // Reset - 0: "", - // Regular colors - 30: "text-black dark:text-gray-900", - 31: "text-red-600 dark:text-red-500", - 32: "text-green-600 dark:text-green-500", - 33: "text-yellow-600 dark:text-yellow-500", - 34: "text-blue-600 dark:text-blue-500", - 35: "text-purple-600 dark:text-purple-500", - 36: "text-cyan-600 dark:text-cyan-500", - 37: "text-gray-600 dark:text-gray-400", - // Bright colors - 90: "text-gray-500 dark:text-gray-600", - 91: "text-red-500 dark:text-red-600", - 92: "text-green-500 dark:text-green-600", - 93: "text-yellow-500 dark:text-yellow-600", - 94: "text-blue-500 dark:text-blue-600", - 95: "text-purple-500 dark:text-purple-600", - 96: "text-cyan-500 dark:text-cyan-600", - 97: "text-white dark:text-gray-300", - // Background colors - 40: "bg-black", - 41: "bg-red-600", - 42: "bg-green-600", - 43: "bg-yellow-600", - 44: "bg-blue-600", - 45: "bg-purple-600", - 46: "bg-cyan-600", - 47: "bg-white", - // Formatting - 1: "font-bold", - 2: "opacity-75", - 3: "italic", - 4: "underline", -}; const LOG_STYLES: Record = { error: { @@ -191,56 +150,3 @@ export const getLogType = (message: string): LogStyle => { return LOG_STYLES.info; }; - -export function parseAnsi(text: string) { - const segments: { text: string; className: string }[] = []; - let currentIndex = 0; - let currentClasses: string[] = []; - - while (currentIndex < text.length) { - const escStart = text.indexOf("\x1b[", currentIndex); - - // No more escape sequences found - if (escStart === -1) { - if (currentIndex < text.length) { - segments.push({ - text: text.slice(currentIndex), - className: currentClasses.join(" "), - }); - } - break; - } - - // Add text before escape sequence - if (escStart > currentIndex) { - segments.push({ - text: text.slice(currentIndex, escStart), - className: currentClasses.join(" "), - }); - } - - const escEnd = text.indexOf("m", escStart); - if (escEnd === -1) break; - - // Handle multiple codes in one sequence (e.g., \x1b[1;31m) - const codesStr = text.slice(escStart + 2, escEnd); - const codes = codesStr.split(";").map((c) => Number.parseInt(c, 10)); - - if (codes.includes(0)) { - // Reset all formatting - currentClasses = []; - } else { - // Add new classes for each code - for (const code of codes) { - const className = ansiToTailwind[code]; - if (className && !currentClasses.includes(className)) { - currentClasses.push(className); - } - } - } - - currentIndex = escEnd + 1; - } - - return segments; -} \ No newline at end of file diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx index c3dba4f51..90aa2b406 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal-modal.tsx @@ -59,7 +59,10 @@ export const DockerTerminalModal = ({ {children} - + event.preventDefault()} + > Docker Terminal @@ -73,7 +76,7 @@ export const DockerTerminalModal = ({ serverId={serverId || ""} /> - + event.preventDefault()}> Are you sure you want to close the terminal? diff --git a/apps/dokploy/components/dashboard/project/add-application.tsx b/apps/dokploy/components/dashboard/project/add-application.tsx index 2ecacdf63..6d9c5b403 100644 --- a/apps/dokploy/components/dashboard/project/add-application.tsx +++ b/apps/dokploy/components/dashboard/project/add-application.tsx @@ -213,7 +213,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => { name="appName" render={({ field }) => ( - AppName + App Name diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index 91dba943e..4461f3dc6 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -220,7 +220,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => { name="appName" render={({ field }) => ( - AppName + App Name diff --git a/apps/dokploy/components/dashboard/project/add-database.tsx b/apps/dokploy/components/dashboard/project/add-database.tsx index aaf4940b0..665bcab54 100644 --- a/apps/dokploy/components/dashboard/project/add-database.tsx +++ b/apps/dokploy/components/dashboard/project/add-database.tsx @@ -18,6 +18,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; import { Form, FormControl, + FormDescription, FormField, FormItem, FormLabel, @@ -35,6 +36,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { slugify } from "@/lib/slug"; import { api } from "@/utils/api"; @@ -95,6 +97,7 @@ const mySchema = z.discriminatedUnion("type", [ .object({ type: z.literal("mongo"), databaseUser: z.string().default("mongo"), + replicaSets: z.boolean().default(false), }) .merge(baseDatabaseSchema), z @@ -216,6 +219,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { databaseUser: data.databaseUser || databasesUserDefaultPlaceholder[data.type], serverId: data.serverId, + replicaSets: data.replicaSets, }); } else if (data.type === "redis") { promise = redisMutation.mutateAsync({ @@ -412,7 +416,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { name="appName" render={({ field }) => ( - AppName + App Name @@ -471,6 +475,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { @@ -491,6 +496,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { @@ -540,6 +546,30 @@ export const AddDatabase = ({ projectId, projectName }: Props) => { ); }} /> + + {type === "mongo" && ( + { + return ( + +
+ Use Replica Sets +
+ + + + + +
+ ); + }} + /> + )} diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index d05bbba2c..d4d9ac553 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -1,35 +1,35 @@ import { DateTooltip } from "@/components/shared/date-tooltip"; import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { api } from "@/utils/api"; import { - AlertTriangle, - BookIcon, - ExternalLink, - ExternalLinkIcon, - FolderInput, - MoreHorizontalIcon, - TrashIcon, + AlertTriangle, + BookIcon, + ExternalLink, + ExternalLinkIcon, + FolderInput, + MoreHorizontalIcon, + TrashIcon, } from "lucide-react"; import Link from "next/link"; import { Fragment } from "react"; @@ -38,257 +38,257 @@ import { ProjectEnviroment } from "./project-enviroment"; import { UpdateProject } from "./update"; export const ShowProjects = () => { - const utils = api.useUtils(); - const { data } = api.project.all.useQuery(); - const { data: auth } = api.auth.get.useQuery(); - const { data: user } = api.user.byAuthId.useQuery( - { - authId: auth?.id || "", - }, - { - enabled: !!auth?.id && auth?.rol === "user", - } - ); - const { mutateAsync } = api.project.remove.useMutation(); + const utils = api.useUtils(); + const { data } = api.project.all.useQuery(); + const { data: auth } = api.auth.get.useQuery(); + const { data: user } = api.user.byAuthId.useQuery( + { + authId: auth?.id || "", + }, + { + enabled: !!auth?.id && auth?.rol === "user", + }, + ); + const { mutateAsync } = api.project.remove.useMutation(); - return ( - <> - {data?.length === 0 && ( -
- - - No projects added yet. Click on Create project. - -
- )} -
- {data?.map((project) => { - const emptyServices = - project?.mariadb.length === 0 && - project?.mongo.length === 0 && - project?.mysql.length === 0 && - project?.postgres.length === 0 && - project?.redis.length === 0 && - project?.applications.length === 0 && - project?.compose.length === 0; + return ( + <> + {data?.length === 0 && ( +
+ + + No projects added yet. Click on Create project. + +
+ )} +
+ {data?.map((project) => { + const emptyServices = + project?.mariadb.length === 0 && + project?.mongo.length === 0 && + project?.mysql.length === 0 && + project?.postgres.length === 0 && + project?.redis.length === 0 && + project?.applications.length === 0 && + project?.compose.length === 0; - const totalServices = - project?.mariadb.length + - project?.mongo.length + - project?.mysql.length + - project?.postgres.length + - project?.redis.length + - project?.applications.length + - project?.compose.length; + const totalServices = + project?.mariadb.length + + project?.mongo.length + + project?.mysql.length + + project?.postgres.length + + project?.redis.length + + project?.applications.length + + project?.compose.length; - const flattedDomains = [ - ...project.applications.flatMap((a) => a.domains), - ...project.compose.flatMap((a) => a.domains), - ]; + const flattedDomains = [ + ...project.applications.flatMap((a) => a.domains), + ...project.compose.flatMap((a) => a.domains), + ]; - const renderDomainsDropdown = ( - item: typeof project.compose | typeof project.applications - ) => - item[0] ? ( - - - {"applicationId" in item[0] ? "Applications" : "Compose"} - - {item.map((a) => ( - - - - - {a.name} - - - {a.domains.map((domain) => ( - - - {domain.host} - - - - ))} - - - ))} - - ) : null; + const renderDomainsDropdown = ( + item: typeof project.compose | typeof project.applications, + ) => + item[0] ? ( + + + {"applicationId" in item[0] ? "Applications" : "Compose"} + + {item.map((a) => ( + + + + + {a.name} + + + {a.domains.map((domain) => ( + + + {domain.host} + + + + ))} + + + ))} + + ) : null; - return ( -
- - - {flattedDomains.length > 1 ? ( - - - - - e.stopPropagation()} - > - {renderDomainsDropdown(project.applications)} - {renderDomainsDropdown(project.compose)} - - - ) : flattedDomains[0] ? ( - - ) : null} + return ( +
+ + + {flattedDomains.length > 1 ? ( + + + + + e.stopPropagation()} + > + {renderDomainsDropdown(project.applications)} + {renderDomainsDropdown(project.compose)} + + + ) : flattedDomains[0] ? ( + + ) : null} - - - -
- - - {project.name} - -
+ + + +
+ + + {project.name} + +
- - {project.description} - -
-
- - - - - - - Actions - -
e.stopPropagation()}> - -
-
e.stopPropagation()}> - -
+ + {project.description} + + +
+ + + + + + + Actions + +
e.stopPropagation()}> + +
+
e.stopPropagation()}> + +
-
e.stopPropagation()}> - {(auth?.rol === "admin" || - user?.canDeleteProjects) && ( - - - e.preventDefault()} - > - - Delete - - - - - - Are you sure to delete this project? - - {!emptyServices ? ( -
- - - You have active services, please - delete them first - -
- ) : ( - - This action cannot be undone - - )} -
- - - Cancel - - { - await mutateAsync({ - projectId: project.projectId, - }) - .then(() => { - toast.success( - "Project delete succesfully" - ); - }) - .catch(() => { - toast.error( - "Error to delete this project" - ); - }) - .finally(() => { - utils.project.all.invalidate(); - }); - }} - > - Delete - - -
-
- )} -
-
-
-
- - - -
- - Created - - - {totalServices}{" "} - {totalServices === 1 ? "service" : "services"} - -
-
- - -
- ); - })} -
- - ); +
e.stopPropagation()}> + {(auth?.rol === "admin" || + user?.canDeleteProjects) && ( + + + e.preventDefault()} + > + + Delete + + + + + + Are you sure to delete this project? + + {!emptyServices ? ( +
+ + + You have active services, please + delete them first + +
+ ) : ( + + This action cannot be undone + + )} +
+ + + Cancel + + { + await mutateAsync({ + projectId: project.projectId, + }) + .then(() => { + toast.success( + "Project delete succesfully", + ); + }) + .catch(() => { + toast.error( + "Error to delete this project", + ); + }) + .finally(() => { + utils.project.all.invalidate(); + }); + }} + > + Delete + + +
+
+ )} +
+ + +
+ + + +
+ + Created + + + {totalServices}{" "} + {totalServices === 1 ? "service" : "services"} + +
+
+ + +
+ ); + })} +
+ + ); }; diff --git a/apps/dokploy/components/dashboard/search-command.tsx b/apps/dokploy/components/dashboard/search-command.tsx index 8afea6720..4d3c75f98 100644 --- a/apps/dokploy/components/dashboard/search-command.tsx +++ b/apps/dokploy/components/dashboard/search-command.tsx @@ -1,189 +1,189 @@ "use client"; -import React from "react"; import { - Command, - CommandEmpty, - CommandList, - CommandGroup, - CommandInput, - CommandItem, - CommandDialog, - CommandSeparator, + MariadbIcon, + MongodbIcon, + MysqlIcon, + PostgresqlIcon, + RedisIcon, +} from "@/components/icons/data-tools-icons"; +import { Badge } from "@/components/ui/badge"; +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, } from "@/components/ui/command"; -import { useRouter } from "next/router"; import { - extractServices, - type Services, + type Services, + extractServices, } from "@/pages/dashboard/project/[projectId]"; +import { api } from "@/utils/api"; import type { findProjectById } from "@dokploy/server/services/project"; import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react"; -import { - MariadbIcon, - MongodbIcon, - MysqlIcon, - PostgresqlIcon, - RedisIcon, -} from "@/components/icons/data-tools-icons"; -import { api } from "@/utils/api"; -import { Badge } from "@/components/ui/badge"; +import { useRouter } from "next/router"; +import React from "react"; import { StatusTooltip } from "../shared/status-tooltip"; type Project = Awaited>; export const SearchCommand = () => { - const router = useRouter(); - const [open, setOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); + const router = useRouter(); + const [open, setOpen] = React.useState(false); + const [search, setSearch] = React.useState(""); - const { data } = api.project.all.useQuery(); - const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); + const { data } = api.project.all.useQuery(); + const { data: isCloud, isLoading } = api.settings.isCloud.useQuery(); - React.useEffect(() => { - const down = (e: KeyboardEvent) => { - if (e.key === "j" && (e.metaKey || e.ctrlKey)) { - e.preventDefault(); - setOpen((open) => !open); - } - }; + React.useEffect(() => { + const down = (e: KeyboardEvent) => { + if (e.key === "j" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + setOpen((open) => !open); + } + }; - document.addEventListener("keydown", down); - return () => document.removeEventListener("keydown", down); - }, []); + document.addEventListener("keydown", down); + return () => document.removeEventListener("keydown", down); + }, []); - return ( -
- - - - - No projects added yet. Click on Create project. - - - - {data?.map((project) => ( - { - router.push(`/dashboard/project/${project.projectId}`); - setOpen(false); - }} - > - - {project.name} - - ))} - - - - - - {data?.map((project) => { - const applications: Services[] = extractServices(project); - return applications.map((application) => ( - { - router.push( - `/dashboard/project/${project.projectId}/services/${application.type}/${application.id}` - ); - setOpen(false); - }} - > - {application.type === "postgres" && ( - - )} - {application.type === "redis" && ( - - )} - {application.type === "mariadb" && ( - - )} - {application.type === "mongo" && ( - - )} - {application.type === "mysql" && ( - - )} - {application.type === "application" && ( - - )} - {application.type === "compose" && ( - - )} - - {project.name} / {application.name}{" "} -
{application.id}
-
-
- -
-
- )); - })} -
-
- - -
-
-
- ); + return ( +
+ + + + + No projects added yet. Click on Create project. + + + + {data?.map((project) => ( + { + router.push(`/dashboard/project/${project.projectId}`); + setOpen(false); + }} + > + + {project.name} + + ))} + + + + + + {data?.map((project) => { + const applications: Services[] = extractServices(project); + return applications.map((application) => ( + { + router.push( + `/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`, + ); + setOpen(false); + }} + > + {application.type === "postgres" && ( + + )} + {application.type === "redis" && ( + + )} + {application.type === "mariadb" && ( + + )} + {application.type === "mongo" && ( + + )} + {application.type === "mysql" && ( + + )} + {application.type === "application" && ( + + )} + {application.type === "compose" && ( + + )} + + {project.name} / {application.name}{" "} +
{application.id}
+
+
+ +
+
+ )); + })} +
+
+ + +
+
+
+ ); }; diff --git a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx index 50b2dd909..173141235 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/add-certificate.tsx @@ -45,6 +45,9 @@ import { z } from "zod"; const certificateDataHolder = "-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----"; +const privateKeyDataHolder = + "-----BEGIN PRIVATE KEY-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n-----END PRIVATE KEY-----"; + const addCertificate = z.object({ name: z.string().min(1, "Name is required"), certificateData: z.string().min(1, "Certificate data is required"), @@ -154,7 +157,7 @@ export const AddCertificate = () => {