mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -5,11 +7,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
import { LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
@@ -19,26 +20,26 @@ interface Props {
|
|||||||
}
|
}
|
||||||
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollRef.current) return;
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
setAutoScroll(isAtBottom);
|
setAutoScroll(isAtBottom);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
@@ -69,20 +70,34 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logs = parseLogs(data);
|
const logs = parseLogs(data);
|
||||||
setFilteredLogs(logs);
|
let filteredLogsResult = logs;
|
||||||
}, [data]);
|
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(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
if (autoScroll && scrollRef.current) {
|
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
|
||||||
}
|
|
||||||
}, [filteredLogs, autoScroll]);
|
|
||||||
|
|
||||||
|
if (autoScroll && scrollRef.current) {
|
||||||
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -103,28 +118,49 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {
|
|||||||
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="flex items-center gap-2">
|
||||||
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge>
|
<span>
|
||||||
|
See all the details of this deployment |{" "}
|
||||||
|
<Badge variant="blank" className="text-xs">
|
||||||
|
{filteredLogs.length} lines
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{serverId && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="show-extra-logs"
|
||||||
|
checked={showExtraLogs}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setShowExtraLogs(checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-extra-logs"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Show Extra Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
> {
|
>
|
||||||
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
|
{" "}
|
||||||
<TerminalLine
|
{filteredLogs.length > 0 ? (
|
||||||
key={index}
|
filteredLogs.map((log: LogLine, index: number) => (
|
||||||
log={log}
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
noTimestamp
|
))
|
||||||
/>
|
) : (
|
||||||
)) :
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
(
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
</div>
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -264,21 +264,21 @@ export const AddDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -15,6 +16,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import dynamic from "next/dynamic";
|
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 {
|
interface Props {
|
||||||
appName: string;
|
appName: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
||||||
const { data, isLoading } = api.docker.getContainersByAppNameMatch.useQuery(
|
|
||||||
{
|
|
||||||
appName,
|
|
||||||
serverId,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
enabled: !!appName,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [containerId, setContainerId] = useState<string | undefined>();
|
const [containerId, setContainerId] = useState<string | undefined>();
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (data && data?.length > 0) {
|
if (option === "native") {
|
||||||
setContainerId(data[0]?.containerId);
|
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 (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -62,7 +103,21 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="flex flex-col gap-4">
|
<CardContent className="flex flex-col gap-4">
|
||||||
<Label>Select a container to view logs</Label>
|
<div className="flex flex-row justify-between items-center gap-2">
|
||||||
|
<Label>Select a container to view logs</Label>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{option === "native" ? "Native" : "Swarm"}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={option === "native"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setOption(checked ? "native" : "swarm");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Select onValueChange={setContainerId} value={containerId}>
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@@ -76,21 +131,45 @@ export const ShowDockerLogs = ({ appName, serverId }: Props) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{data?.map((container) => (
|
{option === "native" ? (
|
||||||
<SelectItem
|
<div>
|
||||||
key={container.containerId}
|
{containers?.map((container) => (
|
||||||
value={container.containerId}
|
<SelectItem
|
||||||
>
|
key={container.containerId}
|
||||||
{container.name} ({container.containerId}) {container.state}
|
value={container.containerId}
|
||||||
</SelectItem>
|
>
|
||||||
))}
|
{container.name} ({container.containerId}){" "}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{services?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}@{container.node}
|
||||||
|
)
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType={option}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -265,21 +265,21 @@ export const AddPreviewDomain = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -18,15 +18,26 @@ import { ShowDeployment } from "../deployments/show-deployment";
|
|||||||
interface Props {
|
interface Props {
|
||||||
deployments: RouterOutputs["deployment"]["all"];
|
deployments: RouterOutputs["deployment"]["all"];
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
trigger?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPreviewBuilds = ({ deployments, serverId }: Props) => {
|
export const ShowPreviewBuilds = ({
|
||||||
|
deployments,
|
||||||
|
serverId,
|
||||||
|
trigger,
|
||||||
|
}: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
const [activeLog, setActiveLog] = useState<string | null>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">View Builds</Button>
|
{trigger ? (
|
||||||
|
trigger
|
||||||
|
) : (
|
||||||
|
<Button className="sm:w-auto w-full" size="sm" variant="outline">
|
||||||
|
View Builds
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -8,30 +11,34 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Pencil, RocketIcon } from "lucide-react";
|
import {
|
||||||
import React, { useEffect, useState } from "react";
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
GitPullRequest,
|
||||||
|
Layers,
|
||||||
|
PenSquare,
|
||||||
|
RocketIcon,
|
||||||
|
Trash2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
import { toast } from "sonner";
|
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 { ShowModalLogs } from "../../settings/web-server/show-modal-logs";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { AddPreviewDomain } from "./add-preview-domain";
|
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 { ShowPreviewBuilds } from "./show-preview-builds";
|
||||||
|
import { ShowPreviewSettings } from "./show-preview-settings";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
applicationId: string;
|
applicationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
||||||
const [activeLog, setActiveLog] = useState<string | null>(null);
|
|
||||||
const { data } = api.application.one.useQuery({ applicationId });
|
const { data } = api.application.one.useQuery({ applicationId });
|
||||||
|
|
||||||
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
const { mutateAsync: deletePreviewDeployment, isLoading } =
|
||||||
api.previewDeployment.delete.useMutation();
|
api.previewDeployment.delete.useMutation();
|
||||||
|
|
||||||
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
const { data: previewDeployments, refetch: refetchPreviewDeployments } =
|
||||||
api.previewDeployment.all.useQuery(
|
api.previewDeployment.all.useQuery(
|
||||||
{ applicationId },
|
{ applicationId },
|
||||||
@@ -39,10 +46,19 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
enabled: !!applicationId,
|
enabled: !!applicationId,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// const [url, setUrl] = React.useState("");
|
|
||||||
// useEffect(() => {
|
const handleDeletePreviewDeployment = async (previewDeploymentId: string) => {
|
||||||
// setUrl(document.location.origin);
|
deletePreviewDeployment({
|
||||||
// }, []);
|
previewDeploymentId: previewDeploymentId,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refetchPreviewDeployments();
|
||||||
|
toast.success("Preview deployment deleted");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error(error.message);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-background">
|
<Card className="bg-background">
|
||||||
@@ -65,7 +81,7 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
each pull request you create.
|
each pull request you create.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{data?.previewDeployments?.length === 0 ? (
|
{!previewDeployments?.length ? (
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
<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">
|
||||||
@@ -74,120 +90,131 @@ export const ShowPreviewDeployments = ({ applicationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{previewDeployments?.map((previewDeployment) => {
|
{previewDeployments?.map((deployment) => {
|
||||||
const { deployments, domain } = previewDeployment;
|
const deploymentUrl = `${deployment.domain?.https ? "https" : "http"}://${deployment.domain?.host}${deployment.domain?.path || "/"}`;
|
||||||
|
const status = deployment.previewStatus;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={previewDeployment?.previewDeploymentId}
|
key={deployment.previewDeploymentId}
|
||||||
className="flex flex-col justify-between rounded-lg border p-4 gap-2"
|
className="group relative overflow-hidden border rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between gap-2 max-sm:flex-wrap">
|
<div
|
||||||
<div className="flex flex-col gap-2">
|
className={`absolute left-0 top-0 w-1 h-full ${
|
||||||
{deployments?.length === 0 ? (
|
status === "done"
|
||||||
<div>
|
? "bg-green-500"
|
||||||
<span className="text-sm text-muted-foreground">
|
: status === "running"
|
||||||
No deployments found
|
? "bg-yellow-500"
|
||||||
</span>
|
: "bg-red-500"
|
||||||
</div>
|
}`}
|
||||||
) : (
|
/>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="flex items-center gap-4 font-medium capitalize text-foreground">
|
|
||||||
{previewDeployment?.pullRequestTitle}
|
|
||||||
</span>
|
|
||||||
<StatusTooltip
|
|
||||||
status={previewDeployment.previewStatus}
|
|
||||||
className="size-2.5"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
{previewDeployment?.pullRequestTitle && (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="break-all text-sm text-muted-foreground w-fit">
|
|
||||||
Title: {previewDeployment?.pullRequestTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{previewDeployment?.pullRequestURL && (
|
<div className="p-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<GithubIcon />
|
<div className="flex items-start gap-3">
|
||||||
<Link
|
<GitPullRequest className="size-5 text-muted-foreground mt-1 flex-shrink-0" />
|
||||||
target="_blank"
|
<div>
|
||||||
href={previewDeployment?.pullRequestURL}
|
<div className="font-medium text-sm">
|
||||||
className="break-all text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
{deployment.pullRequestTitle}
|
||||||
>
|
</div>
|
||||||
Pull Request URL
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
</Link>
|
{deployment.branch}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col ">
|
|
||||||
<span>Domain </span>
|
|
||||||
<div className="flex flex-row items-center gap-4">
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href={`http://${domain?.host}`}
|
|
||||||
className="text-sm text-muted-foreground w-fit hover:underline hover:text-foreground"
|
|
||||||
>
|
|
||||||
{domain?.host}
|
|
||||||
</Link>
|
|
||||||
<AddPreviewDomain
|
|
||||||
previewDeploymentId={
|
|
||||||
previewDeployment.previewDeploymentId
|
|
||||||
}
|
|
||||||
domainId={domain?.domainId}
|
|
||||||
>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
<Pencil className="size-4 text-muted-foreground" />
|
|
||||||
</Button>
|
|
||||||
</AddPreviewDomain>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Badge variant="outline" className="gap-2">
|
||||||
|
<StatusTooltip
|
||||||
|
status={deployment.previewStatus}
|
||||||
|
className="size-2"
|
||||||
|
/>
|
||||||
|
<DateTooltip date={deployment.createdAt} />
|
||||||
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col sm:items-end gap-2 max-sm:w-full">
|
<div className="pl-8 space-y-3">
|
||||||
{previewDeployment?.createdAt && (
|
<div className="relative flex-grow">
|
||||||
<div className="text-sm capitalize text-muted-foreground">
|
<Input
|
||||||
<DateTooltip
|
value={deploymentUrl}
|
||||||
date={previewDeployment?.createdAt}
|
readOnly
|
||||||
/>
|
className="pr-8 text-sm text-blue-500 hover:text-blue-600 cursor-pointer"
|
||||||
</div>
|
onClick={() =>
|
||||||
)}
|
window.open(deploymentUrl, "_blank")
|
||||||
<ShowPreviewBuilds
|
}
|
||||||
deployments={previewDeployment?.deployments || []}
|
/>
|
||||||
serverId={data?.serverId || ""}
|
<ExternalLink className="absolute right-3 top-1/2 -translate-y-1/2 size-4 text-gray-400" />
|
||||||
/>
|
</div>
|
||||||
|
|
||||||
<ShowModalLogs
|
<div className="flex gap-2 opacity-80 group-hover:opacity-100 transition-opacity">
|
||||||
appName={previewDeployment.appName}
|
<Button
|
||||||
serverId={data?.serverId || ""}
|
variant="outline"
|
||||||
>
|
size="sm"
|
||||||
<Button variant="outline">View Logs</Button>
|
className="gap-2"
|
||||||
</ShowModalLogs>
|
onClick={() =>
|
||||||
|
window.open(deployment.pullRequestURL, "_blank")
|
||||||
<DialogAction
|
}
|
||||||
title="Delete Preview"
|
>
|
||||||
description="Are you sure you want to delete this preview?"
|
<GithubIcon className="size-4" />
|
||||||
onClick={() => {
|
Pull Request
|
||||||
deletePreviewDeployment({
|
|
||||||
previewDeploymentId:
|
|
||||||
previewDeployment.previewDeploymentId,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
refetchPreviewDeployments();
|
|
||||||
toast.success("Preview deployment deleted");
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
toast.error(error.message);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="destructive" isLoading={isLoading}>
|
|
||||||
Delete Preview
|
|
||||||
</Button>
|
</Button>
|
||||||
</DialogAction>
|
<ShowModalLogs
|
||||||
|
appName={deployment.appName}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<FileText className="size-4" />
|
||||||
|
Logs
|
||||||
|
</Button>
|
||||||
|
</ShowModalLogs>
|
||||||
|
|
||||||
|
<ShowPreviewBuilds
|
||||||
|
deployments={deployment.deployments || []}
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Layers className="size-4" />
|
||||||
|
Builds
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<AddPreviewDomain
|
||||||
|
previewDeploymentId={`${deployment.previewDeploymentId}`}
|
||||||
|
domainId={deployment.domain?.domainId}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<PenSquare className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</AddPreviewDomain>
|
||||||
|
<DialogAction
|
||||||
|
title="Delete Preview"
|
||||||
|
description="Are you sure you want to delete this preview?"
|
||||||
|
onClick={() =>
|
||||||
|
handleDeletePreviewDeployment(
|
||||||
|
deployment.previewDeploymentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
<Trash2 className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</DialogAction>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { api } from "@/utils/api";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,12 +18,7 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input, NumberInput } from "@/components/ui/input";
|
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 { Secrets } from "@/components/ui/secrets";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -33,6 +26,14 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} 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({
|
const schema = z.object({
|
||||||
env: z.string(),
|
env: z.string(),
|
||||||
@@ -116,7 +117,10 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">View Settings</Button>
|
<Button variant="outline">
|
||||||
|
<Settings2 className="size-4" />
|
||||||
|
Configure
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-5xl w-full">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -218,21 +222,21 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
|
|||||||
name="previewCertificateType"
|
name="previewCertificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -91,7 +92,7 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-xl">Run Command</CardTitle>
|
<CardTitle className="text-xl">Run Command</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Append a custom command to the compose file
|
Override a custom command to the compose file
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -101,6 +102,12 @@ export const AddCommandCompose = ({ composeId }: Props) => {
|
|||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="grid w-full gap-4"
|
className="grid w-full gap-4"
|
||||||
>
|
>
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
Modifying the default command may affect deployment stability,
|
||||||
|
impacting logs and monitoring. Proceed carefully and test
|
||||||
|
thoroughly. By default, the command starts with{" "}
|
||||||
|
<strong>docker</strong>.
|
||||||
|
</AlertBlock>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,6 +13,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -32,6 +34,7 @@ const deleteComposeSchema = z.object({
|
|||||||
projectName: z.string().min(1, {
|
projectName: z.string().min(1, {
|
||||||
message: "Compose name is required",
|
message: "Compose name is required",
|
||||||
}),
|
}),
|
||||||
|
deleteVolumes: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
type DeleteCompose = z.infer<typeof deleteComposeSchema>;
|
||||||
@@ -51,6 +54,7 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
const form = useForm<DeleteCompose>({
|
const form = useForm<DeleteCompose>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
projectName: "",
|
projectName: "",
|
||||||
|
deleteVolumes: false,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(deleteComposeSchema),
|
resolver: zodResolver(deleteComposeSchema),
|
||||||
});
|
});
|
||||||
@@ -58,7 +62,8 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
const onSubmit = async (formData: DeleteCompose) => {
|
const onSubmit = async (formData: DeleteCompose) => {
|
||||||
const expectedName = `${data?.name}/${data?.appName}`;
|
const expectedName = `${data?.name}/${data?.appName}`;
|
||||||
if (formData.projectName === expectedName) {
|
if (formData.projectName === expectedName) {
|
||||||
await mutateAsync({ composeId })
|
const { deleteVolumes } = formData;
|
||||||
|
await mutateAsync({ composeId, deleteVolumes })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
push(`/dashboard/project/${result?.projectId}`);
|
push(`/dashboard/project/${result?.projectId}`);
|
||||||
toast.success("Compose deleted successfully");
|
toast.success("Compose deleted successfully");
|
||||||
@@ -133,6 +138,27 @@ export const DeleteCompose = ({ composeId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="deleteVolumes"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormLabel className="ml-2">
|
||||||
|
Delete volumes associated with this compose
|
||||||
|
</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -5,12 +7,10 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { TerminalLine } from "../../docker/logs/terminal-line";
|
import { TerminalLine } from "../../docker/logs/terminal-line";
|
||||||
import { LogLine, parseLogs } from "../../docker/logs/utils";
|
import { type LogLine, parseLogs } from "../../docker/logs/utils";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
logPath: string | null;
|
logPath: string | null;
|
||||||
@@ -26,25 +26,25 @@ export const ShowDeploymentCompose = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [data, setData] = useState("");
|
const [data, setData] = useState("");
|
||||||
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
const [filteredLogs, setFilteredLogs] = useState<LogLine[]>([]);
|
||||||
|
const [showExtraLogs, setShowExtraLogs] = useState(false);
|
||||||
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
const wsRef = useRef<WebSocket | null>(null); // Ref to hold WebSocket instance
|
||||||
const [autoScroll, setAutoScroll] = useState(true);
|
const [autoScroll, setAutoScroll] = useState(true);
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
if (!scrollRef.current) return;
|
if (!scrollRef.current) return;
|
||||||
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
||||||
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10;
|
||||||
setAutoScroll(isAtBottom);
|
setAutoScroll(isAtBottom);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !logPath) return;
|
if (!open || !logPath) return;
|
||||||
|
|
||||||
@@ -76,19 +76,34 @@ export const ShowDeploymentCompose = ({
|
|||||||
};
|
};
|
||||||
}, [logPath, open]);
|
}, [logPath, open]);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const logs = parseLogs(data);
|
const logs = parseLogs(data);
|
||||||
setFilteredLogs(logs);
|
let filteredLogsResult = logs;
|
||||||
}, [data]);
|
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(() => {
|
useEffect(() => {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
if (autoScroll && scrollRef.current) {
|
if (autoScroll && scrollRef.current) {
|
||||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||||
}
|
}
|
||||||
}, [filteredLogs, autoScroll]);
|
}, [filteredLogs, autoScroll]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -109,32 +124,47 @@ export const ShowDeploymentCompose = ({
|
|||||||
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
<DialogContent className={"sm:max-w-5xl max-h-screen"}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Deployment</DialogTitle>
|
<DialogTitle>Deployment</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="flex items-center gap-2">
|
||||||
See all the details of this deployment | <Badge variant="blank" className="text-xs">{filteredLogs.length} lines</Badge>
|
<span>
|
||||||
|
See all the details of this deployment |{" "}
|
||||||
|
<Badge variant="blank" className="text-xs">
|
||||||
|
{filteredLogs.length} lines
|
||||||
|
</Badge>
|
||||||
|
</span>
|
||||||
|
{serverId && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
id="show-extra-logs"
|
||||||
|
checked={showExtraLogs}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
setShowExtraLogs(checked as boolean)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="show-extra-logs"
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
Show Extra Logs
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
className="h-[720px] overflow-y-auto space-y-0 border p-4 bg-[#fafafa] dark:bg-[#050506] rounded custom-logs-scrollbar"
|
||||||
>
|
>
|
||||||
|
{filteredLogs.length > 0 ? (
|
||||||
|
filteredLogs.map((log: LogLine, index: number) => (
|
||||||
{
|
<TerminalLine key={index} log={log} noTimestamp />
|
||||||
filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => (
|
))
|
||||||
<TerminalLine
|
) : (
|
||||||
key={index}
|
|
||||||
log={log}
|
|
||||||
noTimestamp
|
|
||||||
/>
|
|
||||||
)) :
|
|
||||||
(
|
|
||||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||||
<Loader2 className="h-6 w-6 animate-spin" />
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -400,21 +400,21 @@ export const AddDomainCompose = ({
|
|||||||
name="certificateType"
|
name="certificateType"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Certificate</FormLabel>
|
<FormLabel>Certificate Provider</FormLabel>
|
||||||
<Select
|
<Select
|
||||||
onValueChange={field.onChange}
|
onValueChange={field.onChange}
|
||||||
defaultValue={field.value || ""}
|
defaultValue={field.value || ""}
|
||||||
>
|
>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select a certificate" />
|
<SelectValue placeholder="Select a certificate provider" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="none">None</SelectItem>
|
<SelectItem value="none">None</SelectItem>
|
||||||
<SelectItem value={"letsencrypt"}>
|
<SelectItem value={"letsencrypt"}>
|
||||||
Letsencrypt (Default)
|
Let's Encrypt
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const DeployCompose = ({ composeId }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`
|
`/dashboard/project/${data?.project.projectId}/services/compose/${composeId}?tab=deployments`,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
|
|||||||
165
apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
Normal file
165
apps/dokploy/components/dashboard/compose/logs/show-stack.tsx
Normal file
@@ -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<string | undefined>();
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Card className="bg-background">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-xl">Logs</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Watch the logs of the application in real time
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-row justify-between items-center gap-2">
|
||||||
|
<Label>Select a container to view logs</Label>
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{option === "native" ? "Native" : "Swarm"}
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
checked={option === "native"}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
setOption(checked ? "native" : "swarm");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select onValueChange={setContainerId} value={containerId}>
|
||||||
|
<SelectTrigger>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
<span>Loading...</span>
|
||||||
|
<Loader2 className="animate-spin size-4" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<SelectValue placeholder="Select a container" />
|
||||||
|
)}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
{option === "native" ? (
|
||||||
|
<div>
|
||||||
|
{containers?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}){" "}
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{services?.map((container) => (
|
||||||
|
<SelectItem
|
||||||
|
key={container.containerId}
|
||||||
|
value={container.containerId}
|
||||||
|
>
|
||||||
|
{container.name} ({container.containerId}@{container.node}
|
||||||
|
)
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<SelectLabel>Containers ({containersLenght})</SelectLabel>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<DockerLogs
|
||||||
|
serverId={serverId || ""}
|
||||||
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType={option}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { badgeStateColor } from "@/components/dashboard/application/logs/show";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -87,7 +89,10 @@ export const ShowDockerLogsCompose = ({
|
|||||||
key={container.containerId}
|
key={container.containerId}
|
||||||
value={container.containerId}
|
value={container.containerId}
|
||||||
>
|
>
|
||||||
{container.name} ({container.containerId}) {container.state}
|
{container.name} ({container.containerId}){" "}
|
||||||
|
<Badge variant={badgeStateColor(container.state)}>
|
||||||
|
{container.state}
|
||||||
|
</Badge>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
<SelectLabel>Containers ({data?.length})</SelectLabel>
|
||||||
@@ -97,6 +102,7 @@ export const ShowDockerLogsCompose = ({
|
|||||||
<DockerLogs
|
<DockerLogs
|
||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
|
runType="native"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { type LogLine, getLogType, parseLogs } from "./utils";
|
|||||||
interface Props {
|
interface Props {
|
||||||
containerId: string;
|
containerId: string;
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
|
runType: "swarm" | "native";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const priorities = [
|
export const priorities = [
|
||||||
@@ -37,7 +38,11 @@ export const priorities = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
export const DockerLogsId: React.FC<Props> = ({
|
||||||
|
containerId,
|
||||||
|
serverId,
|
||||||
|
runType,
|
||||||
|
}) => {
|
||||||
const { data } = api.docker.getConfig.useQuery(
|
const { data } = api.docker.getConfig.useQuery(
|
||||||
{
|
{
|
||||||
containerId,
|
containerId,
|
||||||
@@ -104,6 +109,7 @@ export const DockerLogsId: React.FC<Props> = ({ containerId, serverId }) => {
|
|||||||
tail: lines.toString(),
|
tail: lines.toString(),
|
||||||
since,
|
since,
|
||||||
search,
|
search,
|
||||||
|
runType,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
|
|||||||
@@ -46,7 +46,11 @@ export const ShowDockerModalLogs = ({
|
|||||||
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
<DockerLogsId
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
runType="native"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer space-x-3"
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>View Logs</DialogTitle>
|
||||||
|
<DialogDescription>View the logs for {containerId}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<DockerLogsId
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
runType="swarm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,9 +7,10 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { FancyAnsi } from "fancy-ansi";
|
||||||
import { escapeRegExp } from "lodash";
|
import { escapeRegExp } from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { type LogLine, getLogType, parseAnsi } from "./utils";
|
import { type LogLine, getLogType } from "./utils";
|
||||||
|
|
||||||
interface LogLineProps {
|
interface LogLineProps {
|
||||||
log: LogLine;
|
log: LogLine;
|
||||||
@@ -17,6 +18,8 @@ interface LogLineProps {
|
|||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fancyAnsi = new FancyAnsi();
|
||||||
|
|
||||||
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
||||||
const { timestamp, message, rawTimestamp } = log;
|
const { timestamp, message, rawTimestamp } = log;
|
||||||
const { type, variant, color } = getLogType(message);
|
const { type, variant, color } = getLogType(message);
|
||||||
@@ -34,37 +37,42 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) {
|
|||||||
|
|
||||||
const highlightMessage = (text: string, term: string) => {
|
const highlightMessage = (text: string, term: string) => {
|
||||||
if (!term) {
|
if (!term) {
|
||||||
const segments = parseAnsi(text);
|
return (
|
||||||
return segments.map((segment, index) => (
|
<span
|
||||||
<span key={index} className={segment.className || undefined}>
|
className="transition-colors"
|
||||||
{segment.text}
|
dangerouslySetInnerHTML={{
|
||||||
</span>
|
__html: fancyAnsi.toHtml(text),
|
||||||
));
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For search, we need to handle both ANSI and search highlighting
|
const htmlContent = fancyAnsi.toHtml(text);
|
||||||
const segments = parseAnsi(text);
|
const modifiedContent = htmlContent.replace(
|
||||||
return segments.map((segment, index) => {
|
/<span([^>]*)>([^<]*)<\/span>/g,
|
||||||
const parts = segment.text.split(
|
(match, attrs, content) => {
|
||||||
new RegExp(`(${escapeRegExp(term)})`, "gi"),
|
const searchRegex = new RegExp(`(${escapeRegExp(term)})`, "gi");
|
||||||
);
|
if (!content.match(searchRegex)) return match;
|
||||||
return (
|
|
||||||
<span key={index} className={segment.className || undefined}>
|
const segments = content.split(searchRegex);
|
||||||
{parts.map((part, partIndex) =>
|
const wrappedSegments = segments
|
||||||
part.toLowerCase() === term.toLowerCase() ? (
|
.map((segment: string) =>
|
||||||
<span
|
segment.toLowerCase() === term.toLowerCase()
|
||||||
key={partIndex}
|
? `<span${attrs} class="bg-yellow-200/50 dark:bg-yellow-900/50">${segment}</span>`
|
||||||
className="bg-yellow-200 dark:bg-yellow-900"
|
: segment,
|
||||||
>
|
)
|
||||||
{part}
|
.join("");
|
||||||
</span>
|
|
||||||
) : (
|
return `<span${attrs}>${wrappedSegments}</span>`;
|
||||||
part
|
},
|
||||||
),
|
);
|
||||||
)}
|
|
||||||
</span>
|
return (
|
||||||
);
|
<span
|
||||||
});
|
className="transition-colors"
|
||||||
|
dangerouslySetInnerHTML={{ __html: modifiedContent }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const tooltip = (color: string, timestamp: string | null) => {
|
const tooltip = (color: string, timestamp: string | null) => {
|
||||||
|
|||||||
@@ -12,47 +12,6 @@ interface LogStyle {
|
|||||||
variant: LogVariant;
|
variant: LogVariant;
|
||||||
color: string;
|
color: string;
|
||||||
}
|
}
|
||||||
interface AnsiSegment {
|
|
||||||
text: string;
|
|
||||||
className: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ansiToTailwind: Record<number, string> = {
|
|
||||||
// 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<LogType, LogStyle> = {
|
const LOG_STYLES: Record<LogType, LogStyle> = {
|
||||||
error: {
|
error: {
|
||||||
@@ -191,56 +150,3 @@ export const getLogType = (message: string): LogStyle => {
|
|||||||
|
|
||||||
return LOG_STYLES.info;
|
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;
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,10 @@ export const DockerTerminalModal = ({
|
|||||||
{children}
|
{children}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl">
|
<DialogContent
|
||||||
|
className="max-h-screen overflow-y-auto sm:max-w-7xl"
|
||||||
|
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Docker Terminal</DialogTitle>
|
<DialogTitle>Docker Terminal</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -73,7 +76,7 @@ export const DockerTerminalModal = ({
|
|||||||
serverId={serverId || ""}
|
serverId={serverId || ""}
|
||||||
/>
|
/>
|
||||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Are you sure you want to close the terminal?
|
Are you sure you want to close the terminal?
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ export const AddApplication = ({ projectId, projectName }: Props) => {
|
|||||||
name="appName"
|
name="appName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>AppName</FormLabel>
|
<FormLabel>App Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="my-app" {...field} />
|
<Input placeholder="my-app" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ export const AddCompose = ({ projectId, projectName }: Props) => {
|
|||||||
name="appName"
|
name="appName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>AppName</FormLabel>
|
<FormLabel>App Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="my-app" {...field} />
|
<Input placeholder="my-app" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
@@ -35,6 +36,7 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { slugify } from "@/lib/slug";
|
import { slugify } from "@/lib/slug";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
@@ -95,6 +97,7 @@ const mySchema = z.discriminatedUnion("type", [
|
|||||||
.object({
|
.object({
|
||||||
type: z.literal("mongo"),
|
type: z.literal("mongo"),
|
||||||
databaseUser: z.string().default("mongo"),
|
databaseUser: z.string().default("mongo"),
|
||||||
|
replicaSets: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
.merge(baseDatabaseSchema),
|
.merge(baseDatabaseSchema),
|
||||||
z
|
z
|
||||||
@@ -216,6 +219,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
databaseUser:
|
databaseUser:
|
||||||
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
data.databaseUser || databasesUserDefaultPlaceholder[data.type],
|
||||||
serverId: data.serverId,
|
serverId: data.serverId,
|
||||||
|
replicaSets: data.replicaSets,
|
||||||
});
|
});
|
||||||
} else if (data.type === "redis") {
|
} else if (data.type === "redis") {
|
||||||
promise = redisMutation.mutateAsync({
|
promise = redisMutation.mutateAsync({
|
||||||
@@ -412,7 +416,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
name="appName"
|
name="appName"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>AppName</FormLabel>
|
<FormLabel>App Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="my-app" {...field} />
|
<Input placeholder="my-app" {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -471,6 +475,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder={`Default ${databasesUserDefaultPlaceholder[type]}`}
|
placeholder={`Default ${databasesUserDefaultPlaceholder[type]}`}
|
||||||
|
autoComplete="off"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -491,6 +496,7 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="******************"
|
placeholder="******************"
|
||||||
|
autoComplete="off"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@@ -540,6 +546,30 @@ export const AddDatabase = ({ projectId, projectName }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{type === "mongo" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="replicaSets"
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-row items-center justify-between p-3 mt-4 border rounded-lg shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Use Replica Sets</FormLabel>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -1,35 +1,35 @@
|
|||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
AlertDialogCancel,
|
AlertDialogCancel,
|
||||||
AlertDialogContent,
|
AlertDialogContent,
|
||||||
AlertDialogDescription,
|
AlertDialogDescription,
|
||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuGroup,
|
DropdownMenuGroup,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
BookIcon,
|
BookIcon,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
FolderInput,
|
FolderInput,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
@@ -38,257 +38,257 @@ import { ProjectEnviroment } from "./project-enviroment";
|
|||||||
import { UpdateProject } from "./update";
|
import { UpdateProject } from "./update";
|
||||||
|
|
||||||
export const ShowProjects = () => {
|
export const ShowProjects = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data } = api.project.all.useQuery();
|
const { data } = api.project.all.useQuery();
|
||||||
const { data: auth } = api.auth.get.useQuery();
|
const { data: auth } = api.auth.get.useQuery();
|
||||||
const { data: user } = api.user.byAuthId.useQuery(
|
const { data: user } = api.user.byAuthId.useQuery(
|
||||||
{
|
{
|
||||||
authId: auth?.id || "",
|
authId: auth?.id || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!auth?.id && auth?.rol === "user",
|
enabled: !!auth?.id && auth?.rol === "user",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const { mutateAsync } = api.project.remove.useMutation();
|
const { mutateAsync } = api.project.remove.useMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data?.length === 0 && (
|
{data?.length === 0 && (
|
||||||
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
<div className="mt-6 flex h-[50vh] w-full flex-col items-center justify-center space-y-4">
|
||||||
<FolderInput className="size-10 md:size-28 text-muted-foreground" />
|
<FolderInput className="size-10 md:size-28 text-muted-foreground" />
|
||||||
<span className="text-center font-medium text-muted-foreground">
|
<span className="text-center font-medium text-muted-foreground">
|
||||||
No projects added yet. Click on Create project.
|
No projects added yet. Click on Create project.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-6 w-full grid sm:grid-cols-2 lg:grid-cols-3 flex-wrap gap-5 pb-10">
|
<div className="mt-6 w-full grid sm:grid-cols-2 lg:grid-cols-3 flex-wrap gap-5 pb-10">
|
||||||
{data?.map((project) => {
|
{data?.map((project) => {
|
||||||
const emptyServices =
|
const emptyServices =
|
||||||
project?.mariadb.length === 0 &&
|
project?.mariadb.length === 0 &&
|
||||||
project?.mongo.length === 0 &&
|
project?.mongo.length === 0 &&
|
||||||
project?.mysql.length === 0 &&
|
project?.mysql.length === 0 &&
|
||||||
project?.postgres.length === 0 &&
|
project?.postgres.length === 0 &&
|
||||||
project?.redis.length === 0 &&
|
project?.redis.length === 0 &&
|
||||||
project?.applications.length === 0 &&
|
project?.applications.length === 0 &&
|
||||||
project?.compose.length === 0;
|
project?.compose.length === 0;
|
||||||
|
|
||||||
const totalServices =
|
const totalServices =
|
||||||
project?.mariadb.length +
|
project?.mariadb.length +
|
||||||
project?.mongo.length +
|
project?.mongo.length +
|
||||||
project?.mysql.length +
|
project?.mysql.length +
|
||||||
project?.postgres.length +
|
project?.postgres.length +
|
||||||
project?.redis.length +
|
project?.redis.length +
|
||||||
project?.applications.length +
|
project?.applications.length +
|
||||||
project?.compose.length;
|
project?.compose.length;
|
||||||
|
|
||||||
const flattedDomains = [
|
const flattedDomains = [
|
||||||
...project.applications.flatMap((a) => a.domains),
|
...project.applications.flatMap((a) => a.domains),
|
||||||
...project.compose.flatMap((a) => a.domains),
|
...project.compose.flatMap((a) => a.domains),
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderDomainsDropdown = (
|
const renderDomainsDropdown = (
|
||||||
item: typeof project.compose | typeof project.applications
|
item: typeof project.compose | typeof project.applications,
|
||||||
) =>
|
) =>
|
||||||
item[0] ? (
|
item[0] ? (
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
{"applicationId" in item[0] ? "Applications" : "Compose"}
|
{"applicationId" in item[0] ? "Applications" : "Compose"}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
{item.map((a) => (
|
{item.map((a) => (
|
||||||
<Fragment
|
<Fragment
|
||||||
key={"applicationId" in a ? a.applicationId : a.composeId}
|
key={"applicationId" in a ? a.applicationId : a.composeId}
|
||||||
>
|
>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel className="font-normal capitalize text-xs ">
|
<DropdownMenuLabel className="font-normal capitalize text-xs ">
|
||||||
{a.name}
|
{a.name}
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{a.domains.map((domain) => (
|
{a.domains.map((domain) => (
|
||||||
<DropdownMenuItem key={domain.domainId} asChild>
|
<DropdownMenuItem key={domain.domainId} asChild>
|
||||||
<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"}://${
|
href={`${domain.https ? "https" : "http"}://${
|
||||||
domain.host
|
domain.host
|
||||||
}${domain.path}`}
|
}${domain.path}`}
|
||||||
>
|
>
|
||||||
<span>{domain.host}</span>
|
<span>{domain.host}</span>
|
||||||
<ExternalLink className="size-4 shrink-0" />
|
<ExternalLink className="size-4 shrink-0" />
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</DropdownMenuGroup>
|
</DropdownMenuGroup>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={project.projectId} className="w-full lg:max-w-md">
|
<div key={project.projectId} className="w-full lg:max-w-md">
|
||||||
<Link href={`/dashboard/project/${project.projectId}`}>
|
<Link href={`/dashboard/project/${project.projectId}`}>
|
||||||
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
<Card className="group relative w-full bg-transparent transition-colors hover:bg-card">
|
||||||
{flattedDomains.length > 1 ? (
|
{flattedDomains.length > 1 ? (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-[200px] space-y-2"
|
className="w-[200px] space-y-2"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{renderDomainsDropdown(project.applications)}
|
{renderDomainsDropdown(project.applications)}
|
||||||
{renderDomainsDropdown(project.compose)}
|
{renderDomainsDropdown(project.compose)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : flattedDomains[0] ? (
|
) : flattedDomains[0] ? (
|
||||||
<Button
|
<Button
|
||||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`${
|
href={`${
|
||||||
flattedDomains[0].https ? "https" : "http"
|
flattedDomains[0].https ? "https" : "http"
|
||||||
}://${flattedDomains[0].host}${flattedDomains[0].path}`}
|
}://${flattedDomains[0].host}${flattedDomains[0].path}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<ExternalLinkIcon className="size-3.5" />
|
<ExternalLinkIcon className="size-3.5" />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center justify-between gap-2">
|
<CardTitle className="flex items-center justify-between gap-2">
|
||||||
<span className="flex flex-col gap-1.5">
|
<span className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<BookIcon className="size-4 text-muted-foreground" />
|
<BookIcon className="size-4 text-muted-foreground" />
|
||||||
<span className="text-base font-medium leading-none">
|
<span className="text-base font-medium leading-none">
|
||||||
{project.name}
|
{project.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">
|
<span className="text-sm font-medium text-muted-foreground">
|
||||||
{project.description}
|
{project.description}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="flex self-start space-x-1">
|
<div className="flex self-start space-x-1">
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="px-2"
|
className="px-2"
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-5" />
|
<MoreHorizontalIcon className="size-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-[200px] space-y-2">
|
<DropdownMenuContent className="w-[200px] space-y-2">
|
||||||
<DropdownMenuLabel className="font-normal">
|
<DropdownMenuLabel className="font-normal">
|
||||||
Actions
|
Actions
|
||||||
</DropdownMenuLabel>
|
</DropdownMenuLabel>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<ProjectEnviroment
|
<ProjectEnviroment
|
||||||
projectId={project.projectId}
|
projectId={project.projectId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
<UpdateProject projectId={project.projectId} />
|
<UpdateProject projectId={project.projectId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div onClick={(e) => e.stopPropagation()}>
|
<div onClick={(e) => e.stopPropagation()}>
|
||||||
{(auth?.rol === "admin" ||
|
{(auth?.rol === "admin" ||
|
||||||
user?.canDeleteProjects) && (
|
user?.canDeleteProjects) && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger className="w-full">
|
<AlertDialogTrigger className="w-full">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="w-full cursor-pointer space-x-3"
|
className="w-full cursor-pointer space-x-3"
|
||||||
onSelect={(e) => e.preventDefault()}
|
onSelect={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-4" />
|
<TrashIcon className="size-4" />
|
||||||
<span>Delete</span>
|
<span>Delete</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>
|
<AlertDialogTitle>
|
||||||
Are you sure to delete this project?
|
Are you sure to delete this project?
|
||||||
</AlertDialogTitle>
|
</AlertDialogTitle>
|
||||||
{!emptyServices ? (
|
{!emptyServices ? (
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
<div className="flex flex-row gap-4 rounded-lg bg-yellow-50 p-2 dark:bg-yellow-950">
|
||||||
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
You have active services, please
|
You have active services, please
|
||||||
delete them first
|
delete them first
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AlertDialogDescription>
|
<AlertDialogDescription>
|
||||||
This action cannot be undone
|
This action cannot be undone
|
||||||
</AlertDialogDescription>
|
</AlertDialogDescription>
|
||||||
)}
|
)}
|
||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>
|
<AlertDialogCancel>
|
||||||
Cancel
|
Cancel
|
||||||
</AlertDialogCancel>
|
</AlertDialogCancel>
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
disabled={!emptyServices}
|
disabled={!emptyServices}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
projectId: project.projectId,
|
projectId: project.projectId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success(
|
toast.success(
|
||||||
"Project delete succesfully"
|
"Project delete succesfully",
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error(
|
toast.error(
|
||||||
"Error to delete this project"
|
"Error to delete this project",
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
utils.project.all.invalidate();
|
utils.project.all.invalidate();
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</AlertDialogAction>
|
</AlertDialogAction>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="pt-4">
|
<CardFooter className="pt-4">
|
||||||
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
|
||||||
<DateTooltip date={project.createdAt}>
|
<DateTooltip date={project.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
<span>
|
<span>
|
||||||
{totalServices}{" "}
|
{totalServices}{" "}
|
||||||
{totalServices === 1 ? "service" : "services"}
|
{totalServices === 1 ? "service" : "services"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,189 +1,189 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
MariadbIcon,
|
||||||
CommandEmpty,
|
MongodbIcon,
|
||||||
CommandList,
|
MysqlIcon,
|
||||||
CommandGroup,
|
PostgresqlIcon,
|
||||||
CommandInput,
|
RedisIcon,
|
||||||
CommandItem,
|
} from "@/components/icons/data-tools-icons";
|
||||||
CommandDialog,
|
import { Badge } from "@/components/ui/badge";
|
||||||
CommandSeparator,
|
import {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
CommandSeparator,
|
||||||
} from "@/components/ui/command";
|
} from "@/components/ui/command";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import {
|
import {
|
||||||
extractServices,
|
type Services,
|
||||||
type Services,
|
extractServices,
|
||||||
} from "@/pages/dashboard/project/[projectId]";
|
} from "@/pages/dashboard/project/[projectId]";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
import type { findProjectById } from "@dokploy/server/services/project";
|
import type { findProjectById } from "@dokploy/server/services/project";
|
||||||
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
import { BookIcon, CircuitBoard, GlobeIcon } from "lucide-react";
|
||||||
import {
|
import { useRouter } from "next/router";
|
||||||
MariadbIcon,
|
import React from "react";
|
||||||
MongodbIcon,
|
|
||||||
MysqlIcon,
|
|
||||||
PostgresqlIcon,
|
|
||||||
RedisIcon,
|
|
||||||
} from "@/components/icons/data-tools-icons";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { StatusTooltip } from "../shared/status-tooltip";
|
import { StatusTooltip } from "../shared/status-tooltip";
|
||||||
|
|
||||||
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
type Project = Awaited<ReturnType<typeof findProjectById>>;
|
||||||
|
|
||||||
export const SearchCommand = () => {
|
export const SearchCommand = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [search, setSearch] = React.useState("");
|
const [search, setSearch] = React.useState("");
|
||||||
|
|
||||||
const { data } = api.project.all.useQuery();
|
const { data } = api.project.all.useQuery();
|
||||||
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
const { data: isCloud, isLoading } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "j" && (e.metaKey || e.ctrlKey)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setOpen((open) => !open);
|
setOpen((open) => !open);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", down);
|
document.addEventListener("keydown", down);
|
||||||
return () => document.removeEventListener("keydown", down);
|
return () => document.removeEventListener("keydown", down);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={"Search projects or settings"}
|
placeholder={"Search projects or settings"}
|
||||||
value={search}
|
value={search}
|
||||||
onValueChange={setSearch}
|
onValueChange={setSearch}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>
|
<CommandEmpty>
|
||||||
No projects added yet. Click on Create project.
|
No projects added yet. Click on Create project.
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
<CommandGroup heading={"Projects"}>
|
<CommandGroup heading={"Projects"}>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{data?.map((project) => (
|
{data?.map((project) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={project.projectId}
|
key={project.projectId}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push(`/dashboard/project/${project.projectId}`);
|
router.push(`/dashboard/project/${project.projectId}`);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
<BookIcon className="size-4 text-muted-foreground mr-2" />
|
||||||
{project.name}
|
{project.name}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))}
|
))}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading={"Services"}>
|
<CommandGroup heading={"Services"}>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
{data?.map((project) => {
|
{data?.map((project) => {
|
||||||
const applications: Services[] = extractServices(project);
|
const applications: Services[] = extractServices(project);
|
||||||
return applications.map((application) => (
|
return applications.map((application) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={application.id}
|
key={application.id}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push(
|
router.push(
|
||||||
`/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`
|
`/dashboard/project/${project.projectId}/services/${application.type}/${application.id}`,
|
||||||
);
|
);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{application.type === "postgres" && (
|
{application.type === "postgres" && (
|
||||||
<PostgresqlIcon className="h-6 w-6 mr-2" />
|
<PostgresqlIcon className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
{application.type === "redis" && (
|
{application.type === "redis" && (
|
||||||
<RedisIcon className="h-6 w-6 mr-2" />
|
<RedisIcon className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
{application.type === "mariadb" && (
|
{application.type === "mariadb" && (
|
||||||
<MariadbIcon className="h-6 w-6 mr-2" />
|
<MariadbIcon className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
{application.type === "mongo" && (
|
{application.type === "mongo" && (
|
||||||
<MongodbIcon className="h-6 w-6 mr-2" />
|
<MongodbIcon className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
{application.type === "mysql" && (
|
{application.type === "mysql" && (
|
||||||
<MysqlIcon className="h-6 w-6 mr-2" />
|
<MysqlIcon className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
{application.type === "application" && (
|
{application.type === "application" && (
|
||||||
<GlobeIcon className="h-6 w-6 mr-2" />
|
<GlobeIcon className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
{application.type === "compose" && (
|
{application.type === "compose" && (
|
||||||
<CircuitBoard className="h-6 w-6 mr-2" />
|
<CircuitBoard className="h-6 w-6 mr-2" />
|
||||||
)}
|
)}
|
||||||
<span className="flex-grow">
|
<span className="flex-grow">
|
||||||
{project.name} / {application.name}{" "}
|
{project.name} / {application.name}{" "}
|
||||||
<div style={{ display: "none" }}>{application.id}</div>
|
<div style={{ display: "none" }}>{application.id}</div>
|
||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<StatusTooltip status={application.status} />
|
<StatusTooltip status={application.status} />
|
||||||
</div>
|
</div>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
));
|
));
|
||||||
})}
|
})}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandGroup heading={"Application"} hidden={true}>
|
<CommandGroup heading={"Application"} hidden={true}>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/projects");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Projects
|
Projects
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
{!isCloud && (
|
{!isCloud && (
|
||||||
<>
|
<>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/monitoring");
|
router.push("/dashboard/monitoring");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Monitoring
|
Monitoring
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/traefik");
|
router.push("/dashboard/traefik");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Traefik
|
Traefik
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/docker");
|
router.push("/dashboard/docker");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Docker
|
Docker
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/requests");
|
router.push("/dashboard/requests");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Requests
|
Requests
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/settings/server");
|
router.push("/dashboard/settings/server");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Settings
|
Settings
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandDialog>
|
</CommandDialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,9 @@ import { z } from "zod";
|
|||||||
const certificateDataHolder =
|
const certificateDataHolder =
|
||||||
"-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----";
|
"-----BEGIN CERTIFICATE-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n------END CERTIFICATE-----";
|
||||||
|
|
||||||
|
const privateKeyDataHolder =
|
||||||
|
"-----BEGIN PRIVATE KEY-----\nMIIFRDCCAyygAwIBAgIUEPOR47ys6VDwMVB9tYoeEka83uQwDQYJKoZIhvcNAQELBQAwGTEXMBUGA1UEAwwObWktZG9taW5pby5jb20wHhcNMjQwMzExMDQyNzU3WhcN\n-----END PRIVATE KEY-----";
|
||||||
|
|
||||||
const addCertificate = z.object({
|
const addCertificate = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
certificateData: z.string().min(1, "Certificate data is required"),
|
certificateData: z.string().min(1, "Certificate data is required"),
|
||||||
@@ -154,7 +157,7 @@ export const AddCertificate = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="h-32"
|
className="h-32"
|
||||||
placeholder={certificateDataHolder}
|
placeholder={privateKeyDataHolder}
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -159,7 +159,11 @@ export const AddRegistry = () => {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Username</FormLabel>
|
<FormLabel>Username</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="Username" {...field} />
|
<Input
|
||||||
|
placeholder="Username"
|
||||||
|
autoComplete="off"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -177,6 +181,7 @@ export const AddRegistry = () => {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
|
autoComplete="off"
|
||||||
{...field}
|
{...field}
|
||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
.object({
|
.object({
|
||||||
type: z.literal("discord"),
|
type: z.literal("discord"),
|
||||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||||
|
decoration: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
.merge(notificationBaseSchema),
|
.merge(notificationBaseSchema),
|
||||||
z
|
z
|
||||||
@@ -195,6 +196,7 @@ export const AddNotification = () => {
|
|||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
webhookUrl: data.webhookUrl,
|
webhookUrl: data.webhookUrl,
|
||||||
|
decoration: data.decoration,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
dockerCleanup: dockerCleanup,
|
dockerCleanup: dockerCleanup,
|
||||||
});
|
});
|
||||||
@@ -397,23 +399,47 @@ export const AddNotification = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "discord" && (
|
{type === "discord" && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="webhookUrl"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="webhookUrl"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
<Input
|
||||||
{...field}
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
/>
|
{...field}
|
||||||
</FormControl>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="decoration"
|
||||||
|
defaultValue={true}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Decoration</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Decorate the notification with emojis.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "email" && (
|
{type === "email" && (
|
||||||
@@ -708,6 +734,7 @@ export const AddNotification = () => {
|
|||||||
} else if (type === "discord") {
|
} else if (type === "discord") {
|
||||||
await testDiscordConnection({
|
await testDiscordConnection({
|
||||||
webhookUrl: form.getValues("webhookUrl"),
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
decoration: form.getValues("decoration"),
|
||||||
});
|
});
|
||||||
} else if (type === "email") {
|
} else if (type === "email") {
|
||||||
await testEmailConnection({
|
await testEmailConnection({
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ export const DeleteNotification = ({ notificationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-9 w-9 group hover:bg-red-500/10"
|
className="h-9 w-9 group hover:bg-red-500/10"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-muted-foreground group-hover:text-red-500" />
|
<Trash2 className="size-4 text-muted-foreground group-hover:text-red-500" />
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
|
|||||||
@@ -40,58 +40,60 @@ export const ShowNotifications = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="grid lg:grid-cols-1 xl:grid-cols-2 gap-4">
|
<div className="grid lg:grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
{data?.map((notification, index) => (
|
{data?.map((notification, index) => (
|
||||||
<div
|
<div
|
||||||
key={notification.notificationId}
|
key={notification.notificationId}
|
||||||
className="flex items-center justify-between rounded-xl p-4 transition-colors dark:bg-zinc-900/50 hover:bg-zinc-900 border border-zinc-800/50"
|
className="flex items-center justify-between rounded-xl p-4 transition-colors dark:bg-zinc-900/50 bg-gray-200/50 border border-card"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{notification.notificationType === "slack" && (
|
{notification.notificationType === "slack" && (
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10">
|
||||||
<SlackIcon className="h-6 w-6 text-indigo-400" />
|
<SlackIcon className="h-6 w-6 text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{notification.notificationType === "telegram" && (
|
{notification.notificationType === "telegram" && (
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-cyan-500/10">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-cyan-500/10">
|
||||||
<TelegramIcon className="h-6 w-6 text-indigo-400" />
|
<TelegramIcon className="h-6 w-6 text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{notification.notificationType === "discord" && (
|
{notification.notificationType === "discord" && (
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-indigo-500/10">
|
||||||
<DiscordIcon className="h-6 w-6 text-indigo-400" />
|
<DiscordIcon className="h-6 w-6 text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{notification.notificationType === "email" && (
|
{notification.notificationType === "email" && (
|
||||||
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-500/10">
|
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-zinc-500/10">
|
||||||
<Mail className="h-6 w-6 text-indigo-400" />
|
<Mail className="h-6 w-6 text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-sm font-medium text-zinc-300">
|
<span className="text-sm font-medium dark:text-zinc-300 text-zinc-800">
|
||||||
{notification.name}
|
{notification.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-medium text-muted-foreground">
|
<span className="text-xs font-medium text-muted-foreground">
|
||||||
{notification.notificationType?.[0]?.toUpperCase() + notification.notificationType?.slice(1)} notification
|
{notification.notificationType?.[0]?.toUpperCase() +
|
||||||
</span>
|
notification.notificationType?.slice(1)}{" "}
|
||||||
</div>
|
notification
|
||||||
</div>
|
</span>
|
||||||
<div className="flex items-center gap-2">
|
</div>
|
||||||
<UpdateNotification
|
</div>
|
||||||
notificationId={notification.notificationId}
|
<div className="flex items-center gap-2">
|
||||||
/>
|
<UpdateNotification
|
||||||
<DeleteNotification
|
notificationId={notification.notificationId}
|
||||||
notificationId={notification.notificationId}
|
/>
|
||||||
/>
|
<DeleteNotification
|
||||||
</div>
|
notificationId={notification.notificationId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-4 justify-end w-full items-end">
|
||||||
|
<AddNotification />
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-4 justify-end w-full items-end">
|
|
||||||
<AddNotification />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import { api } from "@/utils/api";
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Mail, Pen } from "lucide-react";
|
import { Mail, Pen } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
type NotificationSchema,
|
type NotificationSchema,
|
||||||
@@ -113,6 +113,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
databaseBackup: data.databaseBackup,
|
databaseBackup: data.databaseBackup,
|
||||||
type: data.notificationType,
|
type: data.notificationType,
|
||||||
webhookUrl: data.discord?.webhookUrl,
|
webhookUrl: data.discord?.webhookUrl,
|
||||||
|
decoration: data.discord?.decoration || undefined,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
dockerCleanup: data.dockerCleanup,
|
dockerCleanup: data.dockerCleanup,
|
||||||
});
|
});
|
||||||
@@ -178,6 +179,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
dokployRestart: dokployRestart,
|
dokployRestart: dokployRestart,
|
||||||
databaseBackup: databaseBackup,
|
databaseBackup: databaseBackup,
|
||||||
webhookUrl: formData.webhookUrl,
|
webhookUrl: formData.webhookUrl,
|
||||||
|
decoration: formData.decoration,
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
notificationId: notificationId,
|
notificationId: notificationId,
|
||||||
discordId: data?.discordId,
|
discordId: data?.discordId,
|
||||||
@@ -218,9 +220,11 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<DialogTrigger className="" asChild>
|
<DialogTrigger className="" asChild>
|
||||||
<Button variant="ghost"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
className="h-9 w-9">
|
size="icon"
|
||||||
|
className="h-9 w-9 dark:hover:bg-zinc-900/80 hover:bg-gray-200/80"
|
||||||
|
>
|
||||||
<Pen className="size-4 text-muted-foreground" />
|
<Pen className="size-4 text-muted-foreground" />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
@@ -358,23 +362,46 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{type === "discord" && (
|
{type === "discord" && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
<FormField
|
||||||
name="webhookUrl"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="webhookUrl"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Webhook URL</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Webhook URL</FormLabel>
|
||||||
<Input
|
<FormControl>
|
||||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
<Input
|
||||||
{...field}
|
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
/>
|
{...field}
|
||||||
</FormControl>
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="decoration"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
<FormLabel>Decoration</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
Decorate the notification with emojis.
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
<FormControl>
|
||||||
|
<Switch
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{type === "email" && (
|
{type === "email" && (
|
||||||
<>
|
<>
|
||||||
@@ -669,6 +696,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
} else if (type === "discord") {
|
} else if (type === "discord") {
|
||||||
await testDiscordConnection({
|
await testDiscordConnection({
|
||||||
webhookUrl: form.getValues("webhookUrl"),
|
webhookUrl: form.getValues("webhookUrl"),
|
||||||
|
decoration: form.getValues("decoration"),
|
||||||
});
|
});
|
||||||
} else if (type === "email") {
|
} else if (type === "email") {
|
||||||
await testEmailConnection({
|
await testEmailConnection({
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -26,7 +27,6 @@ import { toast } from "sonner";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Disable2FA } from "./disable-2fa";
|
import { Disable2FA } from "./disable-2fa";
|
||||||
import { Enable2FA } from "./enable-2fa";
|
import { Enable2FA } from "./enable-2fa";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
email: z.string(),
|
email: z.string(),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -18,13 +20,11 @@ import { Input } from "@/components/ui/input";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
const profileSchema = z.object({
|
const profileSchema = z.object({
|
||||||
password: z.string().min(1, {
|
password: z.string().min(1, {
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ import { toast } from "sonner";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
import { EditTraefikEnv } from "../../web-server/edit-traefik-env";
|
||||||
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
|
||||||
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
import { ManageTraefikPorts } from "../../web-server/manage-traefik-ports";
|
||||||
|
import { ShowModalLogs } from "../../web-server/show-modal-logs";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
|
|||||||
@@ -108,7 +108,8 @@ export const EditScript = ({ serverId }: Props) => {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
|
|
||||||
<AlertBlock type="warning">
|
<AlertBlock type="warning">
|
||||||
We recommend not modifying this script unless you know what you are doing.
|
We recommend not modifying this script unless you know what you are
|
||||||
|
doing.
|
||||||
</AlertBlock>
|
</AlertBlock>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ import { toast } from "sonner";
|
|||||||
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
import { ShowDeployment } from "../../application/deployments/show-deployment";
|
||||||
import { EditScript } from "./edit-script";
|
import { EditScript } from "./edit-script";
|
||||||
import { GPUSupport } from "./gpu-support";
|
import { GPUSupport } from "./gpu-support";
|
||||||
import { ValidateServer } from "./validate-server";
|
|
||||||
import { SecurityAudit } from "./security-audit";
|
import { SecurityAudit } from "./security-audit";
|
||||||
|
import { ValidateServer } from "./validate-server";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
|
|||||||
@@ -23,15 +23,16 @@ import { api } from "@/utils/api";
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react";
|
import { KeyIcon, MoreHorizontal, ServerIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { TerminalModal } from "../web-server/terminal-modal";
|
import { TerminalModal } from "../web-server/terminal-modal";
|
||||||
import { ShowServerActions } from "./actions/show-server-actions";
|
import { ShowServerActions } from "./actions/show-server-actions";
|
||||||
import { AddServer } from "./add-server";
|
import { AddServer } from "./add-server";
|
||||||
import { SetupServer } from "./setup-server";
|
import { SetupServer } from "./setup-server";
|
||||||
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
import { ShowDockerContainersModal } from "./show-docker-containers-modal";
|
||||||
|
import { ShowSwarmOverviewModal } from "./show-swarm-overview-modal";
|
||||||
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
import { ShowTraefikFileSystemModal } from "./show-traefik-file-system-modal";
|
||||||
import { UpdateServer } from "./update-server";
|
import { UpdateServer } from "./update-server";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
import { WelcomeSuscription } from "./welcome-stripe/welcome-suscription";
|
||||||
|
|
||||||
export const ShowServers = () => {
|
export const ShowServers = () => {
|
||||||
@@ -259,6 +260,9 @@ export const ShowServers = () => {
|
|||||||
<ShowDockerContainersModal
|
<ShowDockerContainersModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
<ShowSwarmOverviewModal
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||||
|
import { ContainerIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import SwarmMonitorCard from "../../swarm/monitoring-card";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowSwarmOverviewModal = ({ serverId }: Props) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<DropdownMenuItem
|
||||||
|
className="w-full cursor-pointer "
|
||||||
|
onSelect={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
Show Swarm Overview
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-7xl overflow-y-auto max-h-screen ">
|
||||||
|
<DialogHeader>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<ContainerIcon className="size-5" />
|
||||||
|
Swarm Overview
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
See all details of your swarm node
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid w-full gap-1">
|
||||||
|
<div className="flex flex-wrap gap-4 py-4">
|
||||||
|
<SwarmMonitorCard serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { ExternalLinkIcon, Loader2 } from "lucide-react";
|
|
||||||
import copy from "copy-to-clipboard";
|
import copy from "copy-to-clipboard";
|
||||||
|
import { ExternalLinkIcon, Loader2 } from "lucide-react";
|
||||||
import { CopyIcon } from "lucide-react";
|
import { CopyIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { CodeEditor } from "@/components/shared/code-editor";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export const CreateSSHKey = () => {
|
export const CreateSSHKey = () => {
|
||||||
const { data, refetch } = api.sshKey.all.useQuery();
|
const { data, refetch } = api.sshKey.all.useQuery();
|
||||||
|
|||||||
@@ -5,26 +5,26 @@ import { StatusTooltip } from "@/components/shared/status-tooltip";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { RocketIcon } from "lucide-react";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { EditScript } from "../edit-script";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { RocketIcon } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { EditScript } from "../edit-script";
|
||||||
|
|
||||||
export const Setup = () => {
|
export const Setup = () => {
|
||||||
const { data: servers } = api.server.all.useQuery();
|
const { data: servers } = api.server.all.useQuery();
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
CardDescription,
|
|
||||||
CardContent,
|
|
||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Loader2, PcCase, RefreshCw } from "lucide-react";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Loader2, PcCase, RefreshCw } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
SelectContent,
|
SelectContent,
|
||||||
SelectGroup,
|
SelectGroup,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { StatusRow } from "../gpu-support";
|
import { StatusRow } from "../gpu-support";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
|
|
||||||
export const Verify = () => {
|
export const Verify = () => {
|
||||||
const { data: servers } = api.server.all.useQuery();
|
const { data: servers } = api.server.all.useQuery();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -7,21 +9,19 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { defineStepper } from "@stepperize/react";
|
||||||
import { BookIcon, Puzzle } from "lucide-react";
|
import { BookIcon, Puzzle } from "lucide-react";
|
||||||
|
import { Code2, Database, GitMerge, Globe, Plug, Users } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { defineStepper } from "@stepperize/react";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import ConfettiExplosion from "react-confetti-explosion";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
|
||||||
import { CreateServer } from "./create-server";
|
import { CreateServer } from "./create-server";
|
||||||
import { CreateSSHKey } from "./create-ssh-key";
|
import { CreateSSHKey } from "./create-ssh-key";
|
||||||
import { Setup } from "./setup";
|
import { Setup } from "./setup";
|
||||||
import { Verify } from "./verify";
|
import { Verify } from "./verify";
|
||||||
import { Database, Globe, GitMerge, Users, Code2, Plug } from "lucide-react";
|
|
||||||
import ConfettiExplosion from "react-confetti-explosion";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { GithubIcon } from "@/components/icons/data-tools-icons";
|
|
||||||
|
|
||||||
export const { useStepper, steps, Scoped } = defineStepper(
|
export const { useStepper, steps, Scoped } = defineStepper(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
|
<Dialog open={mainDialogOpen} onOpenChange={handleMainDialogOpenChange}>
|
||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-7xl">
|
<DialogContent
|
||||||
|
className="max-h-[85vh] overflow-y-auto sm:max-w-7xl"
|
||||||
|
onEscapeKeyDown={(event) => event.preventDefault()}
|
||||||
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Docker Terminal</DialogTitle>
|
<DialogTitle>Docker Terminal</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
@@ -119,7 +122,7 @@ export const DockerTerminalModal = ({ children, appName, serverId }: Props) => {
|
|||||||
containerId={containerId || "select-a-container"}
|
containerId={containerId || "select-a-container"}
|
||||||
/>
|
/>
|
||||||
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
<Dialog open={confirmDialogOpen} onOpenChange={setConfirmDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent onEscapeKeyDown={(event) => event.preventDefault()}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Are you sure you want to close the terminal?
|
Are you sure you want to close the terminal?
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ export const EditTraefikEnv = ({ children, serverId }: Props) => {
|
|||||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-4xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Update Traefik Env</DialogTitle>
|
<DialogTitle>Update Traefik Environment</DialogTitle>
|
||||||
<DialogDescription>Update the traefik env</DialogDescription>
|
<DialogDescription>
|
||||||
|
Update the traefik environment variables
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogDescription,
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -13,60 +25,48 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ArrowRightLeft, Plus, Trash2 } from "lucide-react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useFieldArray, useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
/**
|
|
||||||
* Props for the ManageTraefikPorts component
|
|
||||||
* @interface Props
|
|
||||||
* @property {React.ReactNode} children - The trigger element that opens the ports management modal
|
|
||||||
* @property {string} [serverId] - Optional ID of the server whose ports are being managed
|
|
||||||
*/
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const PortSchema = z.object({
|
||||||
* Represents a port mapping configuration for Traefik
|
targetPort: z.number().min(1, "Target port is required"),
|
||||||
* @interface AdditionalPort
|
publishedPort: z.number().min(1, "Published port is required"),
|
||||||
* @property {number} targetPort - The internal port that the service is listening on
|
publishMode: z.enum(["ingress", "host"]),
|
||||||
* @property {number} publishedPort - The external port that will be exposed
|
});
|
||||||
* @property {"ingress" | "host"} publishMode - The Docker Swarm publish mode:
|
|
||||||
* - "host": Publishes the port directly on the host
|
const TraefikPortsSchema = z.object({
|
||||||
* - "ingress": Publishes the port through the Swarm routing mesh
|
ports: z.array(PortSchema),
|
||||||
*/
|
});
|
||||||
interface AdditionalPort {
|
|
||||||
targetPort: number;
|
type TraefikPortsForm = z.infer<typeof TraefikPortsSchema>;
|
||||||
publishedPort: number;
|
|
||||||
publishMode: "ingress" | "host";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ManageTraefikPorts is a component that provides a modal interface for managing
|
|
||||||
* additional port mappings for Traefik in a Docker Swarm environment.
|
|
||||||
*
|
|
||||||
* Features:
|
|
||||||
* - Add, remove, and edit port mappings
|
|
||||||
* - Configure target port, published port, and publish mode for each mapping
|
|
||||||
* - Persist port configurations through API calls
|
|
||||||
*
|
|
||||||
* @component
|
|
||||||
* @example
|
|
||||||
* ```tsx
|
|
||||||
* <ManageTraefikPorts serverId="server-123">
|
|
||||||
* <Button>Manage Ports</Button>
|
|
||||||
* </ManageTraefikPorts>
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
||||||
const { t } = useTranslation("settings");
|
const { t } = useTranslation("settings");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [additionalPorts, setAdditionalPorts] = useState<AdditionalPort[]>([]);
|
|
||||||
|
const form = useForm<TraefikPortsForm>({
|
||||||
|
resolver: zodResolver(TraefikPortsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
ports: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { fields, append, remove } = useFieldArray({
|
||||||
|
control: form.control,
|
||||||
|
name: "ports",
|
||||||
|
});
|
||||||
|
|
||||||
const { data: currentPorts, refetch: refetchPorts } =
|
const { data: currentPorts, refetch: refetchPorts } =
|
||||||
api.settings.getTraefikPorts.useQuery({
|
api.settings.getTraefikPorts.useQuery({
|
||||||
@@ -82,22 +82,19 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentPorts) {
|
if (currentPorts) {
|
||||||
setAdditionalPorts(currentPorts);
|
form.reset({ ports: currentPorts });
|
||||||
}
|
}
|
||||||
}, [currentPorts]);
|
}, [currentPorts, form]);
|
||||||
|
|
||||||
const handleAddPort = () => {
|
const handleAddPort = () => {
|
||||||
setAdditionalPorts([
|
append({ targetPort: 0, publishedPort: 0, publishMode: "host" });
|
||||||
...additionalPorts,
|
|
||||||
{ targetPort: 0, publishedPort: 0, publishMode: "host" },
|
|
||||||
]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdatePorts = async () => {
|
const onSubmit = async (data: TraefikPortsForm) => {
|
||||||
try {
|
try {
|
||||||
await updatePorts({
|
await updatePorts({
|
||||||
serverId,
|
serverId,
|
||||||
additionalPorts,
|
additionalPorts: data.ports,
|
||||||
});
|
});
|
||||||
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
toast.success(t("settings.server.webServer.traefik.portsUpdated"));
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -110,121 +107,197 @@ export const ManageTraefikPorts = ({ children, serverId }: Props) => {
|
|||||||
<>
|
<>
|
||||||
<div onClick={() => setOpen(true)}>{children}</div>
|
<div onClick={() => setOpen(true)}>{children}</div>
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent className="sm:max-w-2xl">
|
<DialogContent className="sm:max-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle className="flex items-center gap-2 text-xl">
|
||||||
{t("settings.server.webServer.traefik.managePorts")}
|
{t("settings.server.webServer.traefik.managePorts")}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription className="text-base w-full">
|
||||||
{t("settings.server.webServer.traefik.managePortsDescription")}
|
<div className="flex items-center justify-between">
|
||||||
|
{t("settings.server.webServer.traefik.managePortsDescription")}
|
||||||
|
<Button
|
||||||
|
onClick={handleAddPort}
|
||||||
|
variant="default"
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add Mapping
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
{additionalPorts.map((port, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="grid grid-cols-[120px_120px_minmax(120px,1fr)_80px] gap-4 items-end"
|
|
||||||
>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`target-port-${index}`}>
|
|
||||||
{t("settings.server.webServer.traefik.targetPort")}
|
|
||||||
</Label>
|
|
||||||
<input
|
|
||||||
id={`target-port-${index}`}
|
|
||||||
type="number"
|
|
||||||
value={port.targetPort}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newPorts = [...additionalPorts];
|
|
||||||
|
|
||||||
if (newPorts[index]) {
|
<Form {...form}>
|
||||||
newPorts[index].targetPort = Number.parseInt(
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||||
e.target.value,
|
<div className="grid gap-6 py-4">
|
||||||
);
|
{fields.length === 0 ? (
|
||||||
}
|
<div className="flex w-full flex-col items-center justify-center gap-3 pt-10">
|
||||||
|
<ArrowRightLeft className="size-8 text-muted-foreground" />
|
||||||
|
<span className="text-base text-muted-foreground text-center">
|
||||||
|
No port mappings configured
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-muted-foreground text-center">
|
||||||
|
Add one to get started
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Card key={field.id}>
|
||||||
|
<CardContent className="grid grid-cols-[1fr_1fr_1.5fr_auto] gap-4 p-4 transparent">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name={`ports.${index}.targetPort`}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel className="text-sm font-medium text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"settings.server.webServer.traefik.targetPort",
|
||||||
|
)}
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(Number(e.target.value))
|
||||||
|
}
|
||||||
|
className="w-full dark:bg-black"
|
||||||
|
placeholder="e.g. 8080"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
setAdditionalPorts(newPorts);
|
<FormField
|
||||||
}}
|
control={form.control}
|
||||||
className="w-full rounded border p-2"
|
name={`ports.${index}.publishedPort`}
|
||||||
/>
|
render={({ field }) => (
|
||||||
</div>
|
<FormItem>
|
||||||
<div className="space-y-2">
|
<FormLabel className="text-sm font-medium text-muted-foreground">
|
||||||
<Label htmlFor={`published-port-${index}`}>
|
{t(
|
||||||
{t("settings.server.webServer.traefik.publishedPort")}
|
"settings.server.webServer.traefik.publishedPort",
|
||||||
</Label>
|
)}
|
||||||
<input
|
</FormLabel>
|
||||||
id={`published-port-${index}`}
|
<FormControl>
|
||||||
type="number"
|
<Input
|
||||||
value={port.publishedPort}
|
type="number"
|
||||||
onChange={(e) => {
|
{...field}
|
||||||
const newPorts = [...additionalPorts];
|
onChange={(e) =>
|
||||||
if (newPorts[index]) {
|
field.onChange(Number(e.target.value))
|
||||||
newPorts[index].publishedPort = Number.parseInt(
|
}
|
||||||
e.target.value,
|
className="w-full dark:bg-black"
|
||||||
);
|
placeholder="e.g. 80"
|
||||||
}
|
/>
|
||||||
setAdditionalPorts(newPorts);
|
</FormControl>
|
||||||
}}
|
<FormMessage />
|
||||||
className="w-full rounded border p-2"
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
</div>
|
/>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={`publish-mode-${index}`}>
|
|
||||||
{t("settings.server.webServer.traefik.publishMode")}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={port.publishMode}
|
|
||||||
onValueChange={(value: "ingress" | "host") => {
|
|
||||||
const newPorts = [...additionalPorts];
|
|
||||||
|
|
||||||
if (newPorts[index]) {
|
<FormField
|
||||||
newPorts[index].publishMode = value;
|
control={form.control}
|
||||||
}
|
name={`ports.${index}.publishMode`}
|
||||||
setAdditionalPorts(newPorts);
|
render={({ field }) => (
|
||||||
}}
|
<FormItem>
|
||||||
>
|
<FormLabel className="text-sm font-medium text-muted-foreground">
|
||||||
<SelectTrigger
|
{t(
|
||||||
id={`publish-mode-${index}`}
|
"settings.server.webServer.traefik.publishMode",
|
||||||
className="w-full"
|
)}
|
||||||
>
|
</FormLabel>
|
||||||
<SelectValue />
|
<Select
|
||||||
</SelectTrigger>
|
onValueChange={field.onChange}
|
||||||
<SelectContent>
|
value={field.value}
|
||||||
<SelectItem value="host">Host</SelectItem>
|
>
|
||||||
<SelectItem value="ingress">Ingress</SelectItem>
|
<FormControl>
|
||||||
</SelectContent>
|
<SelectTrigger className="dark:bg-black">
|
||||||
</Select>
|
<SelectValue />
|
||||||
</div>
|
</SelectTrigger>
|
||||||
<div>
|
</FormControl>
|
||||||
<Button
|
<SelectContent>
|
||||||
onClick={() => {
|
<SelectItem value="host">
|
||||||
const newPorts = additionalPorts.filter(
|
Host Mode
|
||||||
(_, i) => i !== index,
|
</SelectItem>
|
||||||
);
|
<SelectItem value="ingress">
|
||||||
setAdditionalPorts(newPorts);
|
Ingress Mode
|
||||||
}}
|
</SelectItem>
|
||||||
variant="destructive"
|
</SelectContent>
|
||||||
size="sm"
|
</Select>
|
||||||
>
|
<FormMessage />
|
||||||
Remove
|
</FormItem>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="text-muted-foreground hover:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{fields.length > 0 && (
|
||||||
|
<AlertBlock type="info">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<span className="text-sm">
|
||||||
|
<strong>
|
||||||
|
Each port mapping defines how external traffic reaches
|
||||||
|
your containers.
|
||||||
|
</strong>
|
||||||
|
<ul className="pt-2">
|
||||||
|
<li>
|
||||||
|
<strong>Host Mode:</strong> Directly binds the port
|
||||||
|
to the host machine.
|
||||||
|
<ul className="p-2 list-inside list-disc">
|
||||||
|
<li>
|
||||||
|
Best for single-node deployments or when you
|
||||||
|
need guaranteed port availability.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Ingress Mode:</strong> Routes through Docker
|
||||||
|
Swarm's load balancer.
|
||||||
|
<ul className="p-2 list-inside list-disc">
|
||||||
|
<li>
|
||||||
|
Recommended for multi-node deployments and
|
||||||
|
better scalability.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
<div className="mt-4 flex justify-between">
|
<DialogFooter>
|
||||||
<Button onClick={handleAddPort} variant="outline" size="sm">
|
<Button
|
||||||
{t("settings.server.webServer.traefik.addPort")}
|
type="submit"
|
||||||
</Button>
|
variant="default"
|
||||||
<Button
|
className="text-sm"
|
||||||
onClick={handleUpdatePorts}
|
isLoading={isLoading}
|
||||||
size="sm"
|
>
|
||||||
disabled={isLoading}
|
Save
|
||||||
>
|
</Button>
|
||||||
Save
|
</DialogFooter>
|
||||||
</Button>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default ManageTraefikPorts;
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ export const ShowModalLogs = ({ appName, children, serverId }: Props) => {
|
|||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<DockerLogsId containerId={containerId || ""} serverId={serverId} />
|
<DockerLogsId
|
||||||
|
containerId={containerId || ""}
|
||||||
|
serverId={serverId}
|
||||||
|
runType="native"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
245
apps/dokploy/components/dashboard/swarm/applications/columns.tsx
Normal file
245
apps/dokploy/components/dashboard/swarm/applications/columns.tsx
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { ShowDockerModalStackLogs } from "../../docker/logs/show-docker-modal-stack-logs";
|
||||||
|
|
||||||
|
export interface ApplicationList {
|
||||||
|
ID: string;
|
||||||
|
Image: string;
|
||||||
|
Mode: string;
|
||||||
|
Name: string;
|
||||||
|
Ports: string;
|
||||||
|
Replicas: string;
|
||||||
|
CurrentState: string;
|
||||||
|
DesiredState: string;
|
||||||
|
Error: string;
|
||||||
|
Node: string;
|
||||||
|
serverId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columns: ColumnDef<ApplicationList>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: "ID",
|
||||||
|
accessorFn: (row) => row.ID,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
ID
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("ID")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Name",
|
||||||
|
accessorFn: (row) => row.Name,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("Name")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Image",
|
||||||
|
accessorFn: (row) => row.Image,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Image
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("Image")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Mode",
|
||||||
|
accessorFn: (row) => row.Mode,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Mode
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("Mode")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "CurrentState",
|
||||||
|
accessorFn: (row) => row.CurrentState,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Current State
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const value = row.getValue("CurrentState") as string;
|
||||||
|
const valueStart = value.startsWith("Running")
|
||||||
|
? "Running"
|
||||||
|
: value.startsWith("Shutdown")
|
||||||
|
? "Shutdown"
|
||||||
|
: value;
|
||||||
|
return (
|
||||||
|
<div className="capitalize">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
valueStart === "Running"
|
||||||
|
? "default"
|
||||||
|
: value === "Shutdown"
|
||||||
|
? "destructive"
|
||||||
|
: "secondary"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "DesiredState",
|
||||||
|
accessorFn: (row) => row.DesiredState,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Desired State
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("DesiredState")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
accessorKey: "Replicas",
|
||||||
|
accessorFn: (row) => row.Replicas,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Replicas
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("Replicas")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
accessorKey: "Ports",
|
||||||
|
accessorFn: (row) => row.Ports,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Ports
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div>{row.getValue("Ports")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Errors",
|
||||||
|
accessorFn: (row) => row.Error,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
|
||||||
|
>
|
||||||
|
Errors
|
||||||
|
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div className="w-[10rem]">{row.getValue("Errors")}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Logs",
|
||||||
|
accessorFn: (row) => row.Error,
|
||||||
|
header: ({ column }) => {
|
||||||
|
return <span>Logs</span>;
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<span className="w-[10rem]">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||||
|
<span className="sr-only">Open menu</span>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
|
<ShowDockerModalStackLogs
|
||||||
|
containerId={row.original.ID}
|
||||||
|
serverId={row.original.serverId}
|
||||||
|
>
|
||||||
|
View Logs
|
||||||
|
</ShowDockerModalStackLogs>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
type ColumnDef,
|
||||||
|
type ColumnFiltersState,
|
||||||
|
type SortingState,
|
||||||
|
type VisibilityState,
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
getFilteredRowModel,
|
||||||
|
getPaginationRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
useReactTable,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { ChevronDown } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
interface DataTableProps<TData, TValue> {
|
||||||
|
columns: ColumnDef<TData, TValue>[];
|
||||||
|
data: TData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTable<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||||
|
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [columnVisibility, setColumnVisibility] =
|
||||||
|
React.useState<VisibilityState>({});
|
||||||
|
const [rowSelection, setRowSelection] = React.useState({});
|
||||||
|
const [pagination, setPagination] = React.useState({
|
||||||
|
pageIndex: 0, //initial page index
|
||||||
|
pageSize: 8, //default page size
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
onSortingChange: setSorting,
|
||||||
|
onColumnFiltersChange: setColumnFilters,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
onColumnVisibilityChange: setColumnVisibility,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
state: {
|
||||||
|
sorting,
|
||||||
|
columnFilters,
|
||||||
|
columnVisibility,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-6 grid gap-4 pb-20 w-full">
|
||||||
|
<div className="flex flex-col gap-4 </div>w-full overflow-auto">
|
||||||
|
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||||
|
<Input
|
||||||
|
placeholder="Filter by name..."
|
||||||
|
value={(table.getColumn("Name")?.getFilterValue() as string) ?? ""}
|
||||||
|
onChange={(event) =>
|
||||||
|
table.getColumn("Name")?.setFilterValue(event.target.value)
|
||||||
|
}
|
||||||
|
className="md:max-w-sm"
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" className="sm:ml-auto max-sm:w-full">
|
||||||
|
Columns <ChevronDown className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{table
|
||||||
|
.getAllColumns()
|
||||||
|
.filter((column) => column.getCanHide())
|
||||||
|
.map((column) => {
|
||||||
|
return (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.id}
|
||||||
|
className="capitalize"
|
||||||
|
checked={column.getIsVisible()}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
column.toggleVisibility(!!value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{column.id}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table?.getRowModel()?.rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(
|
||||||
|
cell.column.columnDef.cell,
|
||||||
|
cell.getContext(),
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={columns.length}
|
||||||
|
className="h-24 text-center"
|
||||||
|
>
|
||||||
|
No results.
|
||||||
|
{/* {isLoading ? (
|
||||||
|
<div className="w-full flex-col gap-2 flex items-center justify-center h-[55vh]">
|
||||||
|
<span className="text-muted-foreground text-lg font-medium">
|
||||||
|
Loading...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>No results.</>
|
||||||
|
)} */}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{data && data?.length > 0 && (
|
||||||
|
<div className="flex items-center justify-end space-x-2 py-4">
|
||||||
|
<div className="space-x-2 flex flex-wrap">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => table.previousPage()}
|
||||||
|
disabled={!table.getCanPreviousPage()}
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => table.nextPage()}
|
||||||
|
disabled={!table.getCanNextPage()}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Layers, Loader2 } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
import { type ApplicationList, columns } from "./columns";
|
||||||
|
import { DataTable } from "./data-table";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowNodeApplications = ({ serverId }: Props) => {
|
||||||
|
const { data: NodeApps, isLoading: NodeAppsLoading } =
|
||||||
|
api.swarm.getNodeApps.useQuery({ serverId });
|
||||||
|
|
||||||
|
let applicationList = "";
|
||||||
|
|
||||||
|
if (NodeApps && NodeApps.length > 0) {
|
||||||
|
applicationList = NodeApps.map((app) => app.Name).join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: NodeAppDetails, isLoading: NodeAppDetailsLoading } =
|
||||||
|
api.swarm.getAppInfos.useQuery({ appName: applicationList, serverId });
|
||||||
|
|
||||||
|
if (NodeAppsLoading || NodeAppDetailsLoading) {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NodeApps || !NodeAppDetails) {
|
||||||
|
return (
|
||||||
|
<span className="text-sm w-full flex text-center justify-center items-center">
|
||||||
|
No data found
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const combinedData: ApplicationList[] = NodeApps.flatMap((app) => {
|
||||||
|
const appDetails =
|
||||||
|
NodeAppDetails?.filter((detail) =>
|
||||||
|
detail.Name.startsWith(`${app.Name}.`),
|
||||||
|
) || [];
|
||||||
|
|
||||||
|
if (appDetails.length === 0) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...app,
|
||||||
|
CurrentState: "N/A",
|
||||||
|
DesiredState: "N/A",
|
||||||
|
Error: "",
|
||||||
|
Node: "N/A",
|
||||||
|
Ports: app.Ports,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return appDetails.map((detail) => ({
|
||||||
|
...app,
|
||||||
|
CurrentState: detail.CurrentState,
|
||||||
|
DesiredState: detail.DesiredState,
|
||||||
|
Error: detail.Error,
|
||||||
|
Node: detail.Node,
|
||||||
|
Ports: detail.Ports || app.Ports,
|
||||||
|
serverId: serverId || "",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<Layers className="h-4 w-4 mr-2" />
|
||||||
|
Services
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className={"sm:max-w-6xl overflow-y-auto max-h-screen"}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Node Applications</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See in detail the applications running on this node
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="max-h-[80vh]">
|
||||||
|
<DataTable columns={columns} data={combinedData ?? []} />
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
140
apps/dokploy/components/dashboard/swarm/details/details-card.tsx
Normal file
140
apps/dokploy/components/dashboard/swarm/details/details-card.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
HelpCircle,
|
||||||
|
Loader2,
|
||||||
|
LoaderIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { ShowNodeApplications } from "../applications/show-applications";
|
||||||
|
import { ShowNodeConfig } from "./show-node-config";
|
||||||
|
|
||||||
|
export interface SwarmList {
|
||||||
|
ID: string;
|
||||||
|
Hostname: string;
|
||||||
|
Availability: string;
|
||||||
|
EngineVersion: string;
|
||||||
|
Status: string;
|
||||||
|
ManagerStatus: string;
|
||||||
|
TLSStatus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
node: SwarmList;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NodeCard({ node, serverId }: Props) {
|
||||||
|
const { data, isLoading } = api.swarm.getNodeInfo.useQuery({
|
||||||
|
nodeId: node.ID,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "Ready":
|
||||||
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||||
|
case "Down":
|
||||||
|
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
||||||
|
default:
|
||||||
|
return <HelpCircle className="h-4 w-4 text-yellow-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Card className="w-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between text-lg">
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(node.Status)}
|
||||||
|
{node.Hostname}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{node.ManagerStatus || "Worker"}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full bg-transparent">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-2 text-lg">
|
||||||
|
{getStatusIcon(node.Status)}
|
||||||
|
{node.Hostname}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{node.ManagerStatus || "Worker"}
|
||||||
|
</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-2 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">Status:</span>
|
||||||
|
<span>{node.Status}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">IP Address:</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span>{data?.Status?.Addr}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">Availability:</span>
|
||||||
|
<span>{node.Availability}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">Engine Version:</span>
|
||||||
|
<span>{node.EngineVersion}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">CPU:</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{(data?.Description?.Resources?.NanoCPUs / 1e9).toFixed(2)} GHz
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">Memory:</span>
|
||||||
|
{isLoading ? (
|
||||||
|
<LoaderIcon className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
{(
|
||||||
|
data?.Description?.Resources?.MemoryBytes /
|
||||||
|
1024 ** 3
|
||||||
|
).toFixed(2)}{" "}
|
||||||
|
GB
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="font-medium">TLS Status:</span>
|
||||||
|
<span>{node.TLSStatus}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 mt-4">
|
||||||
|
<ShowNodeConfig nodeId={node.ID} serverId={serverId} />
|
||||||
|
<ShowNodeApplications serverId={serverId} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { CodeEditor } from "@/components/shared/code-editor";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { Settings } from "lucide-react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
nodeId: string;
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowNodeConfig = ({ nodeId, serverId }: Props) => {
|
||||||
|
const { data, isLoading } = api.swarm.getNodeInfo.useQuery({
|
||||||
|
nodeId,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="w-full">
|
||||||
|
<Settings className="h-4 w-4 mr-2" />
|
||||||
|
Config
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className={"sm:max-w-5xl overflow-y-auto max-h-screen"}>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Node Config</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
See in detail the metadata of this node
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="text-wrap rounded-lg border p-4 text-sm sm:max-w-[59rem] bg-card max-h-[70vh] overflow-auto ">
|
||||||
|
<code>
|
||||||
|
<pre className="whitespace-pre-wrap break-words items-center justify-center">
|
||||||
|
{/* {JSON.stringify(data, null, 2)} */}
|
||||||
|
<CodeEditor
|
||||||
|
language="json"
|
||||||
|
lineWrapping={false}
|
||||||
|
lineNumbers={false}
|
||||||
|
readOnly
|
||||||
|
value={JSON.stringify(data, null, 2)}
|
||||||
|
/>
|
||||||
|
</pre>
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
188
apps/dokploy/components/dashboard/swarm/monitoring-card.tsx
Normal file
188
apps/dokploy/components/dashboard/swarm/monitoring-card.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
CheckCircle,
|
||||||
|
HelpCircle,
|
||||||
|
Loader2,
|
||||||
|
Server,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { NodeCard } from "./details/details-card";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SwarmMonitorCard({ serverId }: Props) {
|
||||||
|
const { data: nodes, isLoading } = api.swarm.getNodes.useQuery({
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-7xl mx-auto">
|
||||||
|
<div className="mb-6 border min-h-[55vh] rounded-lg h-full">
|
||||||
|
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nodes) {
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-7xl mx-auto">
|
||||||
|
<div className="mb-6 border min-h-[55vh] rounded-lg h-full">
|
||||||
|
<div className="flex items-center justify-center h-full text-destructive">
|
||||||
|
<span>Failed to load data</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalNodes = nodes.length;
|
||||||
|
const activeNodesCount = nodes.filter(
|
||||||
|
(node) => node.Status === "Ready",
|
||||||
|
).length;
|
||||||
|
const managerNodesCount = nodes.filter(
|
||||||
|
(node) =>
|
||||||
|
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
|
||||||
|
).length;
|
||||||
|
|
||||||
|
const activeNodes = nodes.filter((node) => node.Status === "Ready");
|
||||||
|
const managerNodes = nodes.filter(
|
||||||
|
(node) =>
|
||||||
|
node.ManagerStatus === "Leader" || node.ManagerStatus === "Reachable",
|
||||||
|
);
|
||||||
|
|
||||||
|
const getStatusIcon = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "Ready":
|
||||||
|
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
||||||
|
case "Down":
|
||||||
|
return <AlertCircle className="h-4 w-4 text-red-500" />;
|
||||||
|
case "Disconnected":
|
||||||
|
return <AlertCircle className="h-4 w-4 text-red-800" />;
|
||||||
|
default:
|
||||||
|
return <HelpCircle className="h-4 w-4 text-yellow-500" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-7xl mx-auto">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h1 className="text-xl font-bold">Docker Swarm Overview</h1>
|
||||||
|
{!serverId && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
window.location.replace("/dashboard/settings/cluster")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Manage Cluster
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Card className="mb-6 bg-transparent">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center gap-2 text-xl">
|
||||||
|
<Server className="size-4" />
|
||||||
|
Monitor
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Total Nodes:</span>
|
||||||
|
<Badge variant="secondary">{totalNodes}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Active Nodes:</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-green-100 dark:bg-green-400 text-black"
|
||||||
|
>
|
||||||
|
{activeNodesCount} / {totalNodes}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{activeNodes.map((node) => (
|
||||||
|
<div key={node.ID} className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(node.Status)}
|
||||||
|
{node.Hostname}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-sm font-medium">Manager Nodes:</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-blue-100 dark:bg-blue-400 text-black"
|
||||||
|
>
|
||||||
|
{managerNodesCount} / {totalNodes}
|
||||||
|
</Badge>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<div className="max-h-48 overflow-y-auto">
|
||||||
|
{managerNodes.map((node) => (
|
||||||
|
<div key={node.ID} className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(node.Status)}
|
||||||
|
{node.Hostname}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t pt-4 mt-4">
|
||||||
|
<h4 className="text-sm font-semibold mb-2">Node Status:</h4>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<li
|
||||||
|
key={node.ID}
|
||||||
|
className="flex justify-between items-center text-sm"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{getStatusIcon(node.Status)}
|
||||||
|
{node.Hostname}
|
||||||
|
</span>
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
{node.ManagerStatus || "Worker"}
|
||||||
|
</Badge>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<NodeCard key={node.ID} node={node} serverId={serverId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,7 +21,8 @@ export type TabState =
|
|||||||
| "settings"
|
| "settings"
|
||||||
| "traefik"
|
| "traefik"
|
||||||
| "requests"
|
| "requests"
|
||||||
| "docker";
|
| "docker"
|
||||||
|
| "swarm";
|
||||||
|
|
||||||
const getTabMaps = (isCloud: boolean) => {
|
const getTabMaps = (isCloud: boolean) => {
|
||||||
const elements: TabInfo[] = [
|
const elements: TabInfo[] = [
|
||||||
@@ -60,6 +61,15 @@ const getTabMaps = (isCloud: boolean) => {
|
|||||||
},
|
},
|
||||||
type: "docker",
|
type: "docker",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Swarm",
|
||||||
|
description: "Manage your docker swarm and Servers",
|
||||||
|
index: "/dashboard/swarm",
|
||||||
|
isShow: ({ rol, user }) => {
|
||||||
|
return Boolean(rol === "admin" || user?.canAccessToDocker);
|
||||||
|
},
|
||||||
|
type: "swarm",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Requests",
|
label: "Requests",
|
||||||
description: "Manage your requests",
|
description: "Manage your requests",
|
||||||
|
|||||||
1
apps/dokploy/drizzle/0052_bumpy_luckman.sql
Normal file
1
apps/dokploy/drizzle/0052_bumpy_luckman.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "discord" ADD COLUMN "decoration" boolean;
|
||||||
1
apps/dokploy/drizzle/0053_broken_kulan_gath.sql
Normal file
1
apps/dokploy/drizzle/0053_broken_kulan_gath.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "mongo" ADD COLUMN "replicaSets" boolean DEFAULT false;
|
||||||
4246
apps/dokploy/drizzle/meta/0052_snapshot.json
Normal file
4246
apps/dokploy/drizzle/meta/0052_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
4253
apps/dokploy/drizzle/meta/0053_snapshot.json
Normal file
4253
apps/dokploy/drizzle/meta/0053_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -365,6 +365,20 @@
|
|||||||
"when": 1734241482851,
|
"when": 1734241482851,
|
||||||
"tag": "0051_hard_gorgon",
|
"tag": "0051_hard_gorgon",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 52,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734809337308,
|
||||||
|
"tag": "0052_bumpy_luckman",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 53,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1735118844878,
|
||||||
|
"tag": "0053_broken_kulan_gath",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,8 @@ export enum Languages {
|
|||||||
Portuguese = "pt-br",
|
Portuguese = "pt-br",
|
||||||
Italian = "it",
|
Italian = "it",
|
||||||
Japanese = "ja",
|
Japanese = "ja",
|
||||||
|
Spanish = "es",
|
||||||
|
Norwegian = "no",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Language = keyof typeof Languages;
|
export type Language = keyof typeof Languages;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.15.1",
|
"version": "v0.16.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -35,8 +35,6 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-confetti-explosion":"2.1.2",
|
|
||||||
"@stepperize/react": "4.0.1",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
@@ -64,6 +62,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@stepperize/react": "4.0.1",
|
||||||
"@stripe/stripe-js": "4.8.0",
|
"@stripe/stripe-js": "4.8.0",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
@@ -87,6 +86,7 @@
|
|||||||
"dotenv": "16.4.5",
|
"dotenv": "16.4.5",
|
||||||
"drizzle-orm": "^0.30.8",
|
"drizzle-orm": "^0.30.8",
|
||||||
"drizzle-zod": "0.5.1",
|
"drizzle-zod": "0.5.1",
|
||||||
|
"fancy-ansi": "^0.1.3",
|
||||||
"i18next": "^23.16.4",
|
"i18next": "^23.16.4",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -104,6 +104,7 @@
|
|||||||
"postgres": "3.4.4",
|
"postgres": "3.4.4",
|
||||||
"public-ip": "6.0.2",
|
"public-ip": "6.0.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
"react-confetti-explosion": "2.1.2",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
"react-i18next": "^15.1.0",
|
"react-i18next": "^15.1.0",
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ import { applications, compose, github } from "@/server/db/schema";
|
|||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
|
import { generateRandomDomain } from "@/templates/utils";
|
||||||
import {
|
import {
|
||||||
createPreviewDeployment,
|
|
||||||
type Domain,
|
type Domain,
|
||||||
|
IS_CLOUD,
|
||||||
|
createPreviewDeployment,
|
||||||
findPreviewDeploymentByApplicationId,
|
findPreviewDeploymentByApplicationId,
|
||||||
findPreviewDeploymentsByPullRequestId,
|
findPreviewDeploymentsByPullRequestId,
|
||||||
IS_CLOUD,
|
|
||||||
removePreviewDeployment,
|
removePreviewDeployment,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
||||||
import { generateRandomDomain } from "@/templates/utils";
|
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ShowDomainsCompose } from "@/components/dashboard/compose/domains/show-
|
|||||||
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
|
import { ShowEnvironmentCompose } from "@/components/dashboard/compose/enviroment/show";
|
||||||
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 { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
|
import { ShowMonitoringCompose } from "@/components/dashboard/compose/monitoring/show";
|
||||||
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
import { UpdateCompose } from "@/components/dashboard/compose/update-compose";
|
||||||
import { ProjectLayout } from "@/components/layouts/project-layout";
|
import { ProjectLayout } from "@/components/layouts/project-layout";
|
||||||
@@ -251,11 +252,18 @@ const Service = (
|
|||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogsCompose
|
{data?.composeType === "docker-compose" ? (
|
||||||
serverId={data?.serverId || ""}
|
<ShowDockerLogsCompose
|
||||||
appName={data?.appName || ""}
|
serverId={data?.serverId || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appName={data?.appName || ""}
|
||||||
/>
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<ShowDockerLogsStack
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|||||||
86
apps/dokploy/pages/dashboard/swarm.tsx
Normal file
86
apps/dokploy/pages/dashboard/swarm.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap gap-4 py-4">
|
||||||
|
<SwarmMonitorCard />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
|
|
||||||
|
Dashboard.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout tab={"swarm"}>{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { req, res } = ctx;
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await helpers.project.all.prefetch();
|
||||||
|
const auth = await helpers.auth.get.fetch();
|
||||||
|
|
||||||
|
if (auth.rol === "user") {
|
||||||
|
const user = await helpers.user.byAuthId.fetch({
|
||||||
|
authId: auth.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user.canAccessToDocker) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
"settings.server.domain.description": "Add a domain to your server application.",
|
"settings.server.domain.description": "Add a domain to your server application.",
|
||||||
"settings.server.domain.form.domain": "Domain",
|
"settings.server.domain.form.domain": "Domain",
|
||||||
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
|
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Email",
|
||||||
"settings.server.domain.form.certificate.label": "Certificate",
|
"settings.server.domain.form.certificate.label": "Certificate Provider",
|
||||||
"settings.server.domain.form.certificate.placeholder": "Select a certificate",
|
"settings.server.domain.form.certificate.placeholder": "Select a certificate",
|
||||||
"settings.server.domain.form.certificateOptions.none": "None",
|
"settings.server.domain.form.certificateOptions.none": "None",
|
||||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Default)",
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
|
||||||
|
|
||||||
"settings.server.webServer.title": "Web Server",
|
"settings.server.webServer.title": "Web Server",
|
||||||
"settings.server.webServer.description": "Reload or clean the web server.",
|
"settings.server.webServer.description": "Reload or clean the web server.",
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
"settings.server.webServer.updateServerIp": "Update Server IP",
|
"settings.server.webServer.updateServerIp": "Update Server IP",
|
||||||
"settings.server.webServer.server.label": "Server",
|
"settings.server.webServer.server.label": "Server",
|
||||||
"settings.server.webServer.traefik.label": "Traefik",
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
"settings.server.webServer.traefik.modifyEnv": "Modify Env",
|
"settings.server.webServer.traefik.modifyEnv": "Modify Environment",
|
||||||
"settings.server.webServer.traefik.managePorts": "Additional Ports",
|
"settings.server.webServer.traefik.managePorts": "Additional Port Mappings",
|
||||||
"settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik",
|
"settings.server.webServer.traefik.managePortsDescription": "Add or remove additional ports for Traefik",
|
||||||
"settings.server.webServer.traefik.targetPort": "Target Port",
|
"settings.server.webServer.traefik.targetPort": "Target Port",
|
||||||
"settings.server.webServer.traefik.publishedPort": "Published Port",
|
"settings.server.webServer.traefik.publishedPort": "Published Port",
|
||||||
|
|||||||
1
apps/dokploy/public/locales/es/common.json
Normal file
1
apps/dokploy/public/locales/es/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
52
apps/dokploy/public/locales/es/settings.json
Normal file
52
apps/dokploy/public/locales/es/settings.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Guardar",
|
||||||
|
"settings.server.domain.title": "Dominio del Servidor",
|
||||||
|
"settings.server.domain.description": "Añade un dominio a tu aplicación de servidor.",
|
||||||
|
"settings.server.domain.form.domain": "Dominio",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Correo de Let's Encrypt",
|
||||||
|
"settings.server.domain.form.certificate.label": "Proveedor de Certificado",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Selecciona un certificado",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "Ninguno",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Servidor Web",
|
||||||
|
"settings.server.webServer.description": "Recarga o limpia el servidor web.",
|
||||||
|
"settings.server.webServer.actions": "Acciones",
|
||||||
|
"settings.server.webServer.reload": "Recargar",
|
||||||
|
"settings.server.webServer.watchLogs": "Ver registros",
|
||||||
|
"settings.server.webServer.updateServerIp": "Actualizar IP del Servidor",
|
||||||
|
"settings.server.webServer.server.label": "Servidor",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Modificar Entorno",
|
||||||
|
"settings.server.webServer.traefik.managePorts": "Asignación Adicional de Puertos",
|
||||||
|
"settings.server.webServer.traefik.managePortsDescription": "Añadir o eliminar puertos adicionales para Traefik",
|
||||||
|
"settings.server.webServer.traefik.targetPort": "Puerto de Destino",
|
||||||
|
"settings.server.webServer.traefik.publishedPort": "Puerto Publicado",
|
||||||
|
"settings.server.webServer.traefik.addPort": "Añadir Puerto",
|
||||||
|
"settings.server.webServer.traefik.portsUpdated": "Puertos actualizados correctamente",
|
||||||
|
"settings.server.webServer.traefik.portsUpdateError": "Error al actualizar los puertos",
|
||||||
|
"settings.server.webServer.traefik.publishMode": "Modo de Publicación",
|
||||||
|
"settings.server.webServer.storage.label": "Espacio",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Limpiar imágenes no utilizadas",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Limpiar volúmenes no utilizados",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Limpiar contenedores detenidos",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Limpiar Constructor de Docker y Sistema",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Limpiar Monitoreo",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Limpiar todo",
|
||||||
|
|
||||||
|
"settings.profile.title": "Cuenta",
|
||||||
|
"settings.profile.description": "Cambia los detalles de tu perfil aquí.",
|
||||||
|
"settings.profile.email": "Correo electrónico",
|
||||||
|
"settings.profile.password": "Contraseña",
|
||||||
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Apariencia",
|
||||||
|
"settings.appearance.description": "Personaliza el tema de tu panel.",
|
||||||
|
"settings.appearance.theme": "Tema",
|
||||||
|
"settings.appearance.themeDescription": "Selecciona un tema para tu panel",
|
||||||
|
"settings.appearance.themes.light": "Claro",
|
||||||
|
"settings.appearance.themes.dark": "Oscuro",
|
||||||
|
"settings.appearance.themes.system": "Sistema",
|
||||||
|
"settings.appearance.language": "Idioma",
|
||||||
|
"settings.appearance.languageDescription": "Selecciona un idioma para tu panel"
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
{}
|
{}
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
{
|
{
|
||||||
"settings.common.save": "Salva",
|
"settings.common.save": "Salva",
|
||||||
"settings.server.domain.title": "Dominio del server",
|
"settings.server.domain.title": "Dominio del server",
|
||||||
"settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.",
|
"settings.server.domain.description": "Aggiungi un dominio alla tua applicazione server.",
|
||||||
"settings.server.domain.form.domain": "Dominio",
|
"settings.server.domain.form.domain": "Dominio",
|
||||||
"settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt",
|
"settings.server.domain.form.letsEncryptEmail": "Email di Let's Encrypt",
|
||||||
"settings.server.domain.form.certificate.label": "Certificato",
|
"settings.server.domain.form.certificate.label": "Certificato",
|
||||||
"settings.server.domain.form.certificate.placeholder": "Seleziona un certificato",
|
"settings.server.domain.form.certificate.placeholder": "Seleziona un certificato",
|
||||||
"settings.server.domain.form.certificateOptions.none": "Nessuno",
|
"settings.server.domain.form.certificateOptions.none": "Nessuno",
|
||||||
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)",
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt (Predefinito)",
|
||||||
|
|
||||||
"settings.server.webServer.title": "Server Web",
|
"settings.server.webServer.title": "Server Web",
|
||||||
"settings.server.webServer.description": "Ricarica o pulisci il server web.",
|
"settings.server.webServer.description": "Ricarica o pulisci il server web.",
|
||||||
"settings.server.webServer.actions": "Azioni",
|
"settings.server.webServer.actions": "Azioni",
|
||||||
"settings.server.webServer.reload": "Ricarica",
|
"settings.server.webServer.reload": "Ricarica",
|
||||||
"settings.server.webServer.watchLogs": "Guarda i log",
|
"settings.server.webServer.watchLogs": "Guarda i log",
|
||||||
"settings.server.webServer.updateServerIp": "Aggiorna IP del server",
|
"settings.server.webServer.updateServerIp": "Aggiorna IP del server",
|
||||||
"settings.server.webServer.server.label": "Server",
|
"settings.server.webServer.server.label": "Server",
|
||||||
"settings.server.webServer.traefik.label": "Traefik",
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
"settings.server.webServer.traefik.modifyEnv": "Modifica Env",
|
"settings.server.webServer.traefik.modifyEnv": "Modifica Env",
|
||||||
"settings.server.webServer.storage.label": "Spazio",
|
"settings.server.webServer.storage.label": "Spazio",
|
||||||
"settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate",
|
"settings.server.webServer.storage.cleanUnusedImages": "Pulisci immagini inutilizzate",
|
||||||
"settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati",
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Pulisci volumi inutilizzati",
|
||||||
"settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati",
|
"settings.server.webServer.storage.cleanStoppedContainers": "Pulisci container fermati",
|
||||||
"settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema",
|
"settings.server.webServer.storage.cleanDockerBuilder": "Pulisci Docker Builder e sistema",
|
||||||
"settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio",
|
"settings.server.webServer.storage.cleanMonitoring": "Pulisci monitoraggio",
|
||||||
"settings.server.webServer.storage.cleanAll": "Pulisci tutto",
|
"settings.server.webServer.storage.cleanAll": "Pulisci tutto",
|
||||||
|
|
||||||
"settings.profile.title": "Account",
|
"settings.profile.title": "Account",
|
||||||
"settings.profile.description": "Modifica i dettagli del tuo profilo qui.",
|
"settings.profile.description": "Modifica i dettagli del tuo profilo qui.",
|
||||||
"settings.profile.email": "Email",
|
"settings.profile.email": "Email",
|
||||||
"settings.profile.password": "Password",
|
"settings.profile.password": "Password",
|
||||||
"settings.profile.avatar": "Avatar",
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
"settings.appearance.title": "Aspetto",
|
"settings.appearance.title": "Aspetto",
|
||||||
"settings.appearance.description": "Personalizza il tema della tua dashboard.",
|
"settings.appearance.description": "Personalizza il tema della tua dashboard.",
|
||||||
"settings.appearance.theme": "Tema",
|
"settings.appearance.theme": "Tema",
|
||||||
"settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard",
|
"settings.appearance.themeDescription": "Seleziona un tema per la tua dashboard",
|
||||||
"settings.appearance.themes.light": "Chiaro",
|
"settings.appearance.themes.light": "Chiaro",
|
||||||
"settings.appearance.themes.dark": "Scuro",
|
"settings.appearance.themes.dark": "Scuro",
|
||||||
"settings.appearance.themes.system": "Sistema",
|
"settings.appearance.themes.system": "Sistema",
|
||||||
"settings.appearance.language": "Lingua",
|
"settings.appearance.language": "Lingua",
|
||||||
"settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard"
|
"settings.appearance.languageDescription": "Seleziona una lingua per la tua dashboard"
|
||||||
}
|
}
|
||||||
|
|||||||
1
apps/dokploy/public/locales/no/common.json
Normal file
1
apps/dokploy/public/locales/no/common.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
52
apps/dokploy/public/locales/no/settings.json
Normal file
52
apps/dokploy/public/locales/no/settings.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"settings.common.save": "Lagre",
|
||||||
|
"settings.server.domain.title": "Serverdomene",
|
||||||
|
"settings.server.domain.description": "Legg til et domene i serverapplikasjonen din.",
|
||||||
|
"settings.server.domain.form.domain": "Domene",
|
||||||
|
"settings.server.domain.form.letsEncryptEmail": "Let's Encrypt Epost",
|
||||||
|
"settings.server.domain.form.certificate.label": "Sertifikatleverandør",
|
||||||
|
"settings.server.domain.form.certificate.placeholder": "Velg et sertifikat",
|
||||||
|
"settings.server.domain.form.certificateOptions.none": "Ingen",
|
||||||
|
"settings.server.domain.form.certificateOptions.letsencrypt": "Let's Encrypt",
|
||||||
|
|
||||||
|
"settings.server.webServer.title": "Webserver",
|
||||||
|
"settings.server.webServer.description": "Last på nytt eller rens webserveren.",
|
||||||
|
"settings.server.webServer.actions": "Handlinger",
|
||||||
|
"settings.server.webServer.reload": "Last på nytt",
|
||||||
|
"settings.server.webServer.watchLogs": "Se logger",
|
||||||
|
"settings.server.webServer.updateServerIp": "Oppdater server-IP",
|
||||||
|
"settings.server.webServer.server.label": "Server",
|
||||||
|
"settings.server.webServer.traefik.label": "Traefik",
|
||||||
|
"settings.server.webServer.traefik.modifyEnv": "Endre miljø",
|
||||||
|
"settings.server.webServer.traefik.managePorts": "Ytterligere portkartlegginger",
|
||||||
|
"settings.server.webServer.traefik.managePortsDescription": "Legg til eller fjern flere porter for Traefik",
|
||||||
|
"settings.server.webServer.traefik.targetPort": "Målport",
|
||||||
|
"settings.server.webServer.traefik.publishedPort": "Publisert port",
|
||||||
|
"settings.server.webServer.traefik.addPort": "Legg til port",
|
||||||
|
"settings.server.webServer.traefik.portsUpdated": "Portene ble oppdatert",
|
||||||
|
"settings.server.webServer.traefik.portsUpdateError": "Kunne ikke oppdatere portene",
|
||||||
|
"settings.server.webServer.traefik.publishMode": "Publiseringsmodus",
|
||||||
|
"settings.server.webServer.storage.label": "Lagring",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedImages": "Rens ubrukte bilder",
|
||||||
|
"settings.server.webServer.storage.cleanUnusedVolumes": "Rens ubrukte volumer",
|
||||||
|
"settings.server.webServer.storage.cleanStoppedContainers": "Rens stoppete containere",
|
||||||
|
"settings.server.webServer.storage.cleanDockerBuilder": "Rens Docker Builder og System",
|
||||||
|
"settings.server.webServer.storage.cleanMonitoring": "Rens overvåking",
|
||||||
|
"settings.server.webServer.storage.cleanAll": "Rens alt",
|
||||||
|
|
||||||
|
"settings.profile.title": "Konto",
|
||||||
|
"settings.profile.description": "Endre detaljene for profilen din her.",
|
||||||
|
"settings.profile.email": "Epost",
|
||||||
|
"settings.profile.password": "Passord",
|
||||||
|
"settings.profile.avatar": "Avatar",
|
||||||
|
|
||||||
|
"settings.appearance.title": "Utseende",
|
||||||
|
"settings.appearance.description": "Tilpass temaet for dashbordet ditt.",
|
||||||
|
"settings.appearance.theme": "Tema",
|
||||||
|
"settings.appearance.themeDescription": "Velg et tema for dashbordet ditt",
|
||||||
|
"settings.appearance.themes.light": "Lys",
|
||||||
|
"settings.appearance.themes.dark": "Mørk",
|
||||||
|
"settings.appearance.themes.system": "System",
|
||||||
|
"settings.appearance.language": "Språk",
|
||||||
|
"settings.appearance.languageDescription": "Velg et språk for dashbordet ditt"
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import { mysqlRouter } from "./routers/mysql";
|
|||||||
import { notificationRouter } from "./routers/notification";
|
import { notificationRouter } from "./routers/notification";
|
||||||
import { portRouter } from "./routers/port";
|
import { portRouter } from "./routers/port";
|
||||||
import { postgresRouter } from "./routers/postgres";
|
import { postgresRouter } from "./routers/postgres";
|
||||||
|
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
||||||
import { projectRouter } from "./routers/project";
|
import { projectRouter } from "./routers/project";
|
||||||
import { redirectsRouter } from "./routers/redirects";
|
import { redirectsRouter } from "./routers/redirects";
|
||||||
import { redisRouter } from "./routers/redis";
|
import { redisRouter } from "./routers/redis";
|
||||||
@@ -30,8 +31,8 @@ import { serverRouter } from "./routers/server";
|
|||||||
import { settingsRouter } from "./routers/settings";
|
import { settingsRouter } from "./routers/settings";
|
||||||
import { sshRouter } from "./routers/ssh-key";
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
import { stripeRouter } from "./routers/stripe";
|
import { stripeRouter } from "./routers/stripe";
|
||||||
|
import { swarmRouter } from "./routers/swarm";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
import { previewDeploymentRouter } from "./routers/preview-deployment";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -73,6 +74,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
server: serverRouter,
|
server: serverRouter,
|
||||||
stripe: stripeRouter,
|
stripe: stripeRouter,
|
||||||
|
swarm: swarmRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -555,9 +555,9 @@ export const applicationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateApplication(input.applicationId as string, {
|
await updateApplication(input.applicationId as string, {
|
||||||
sourceType: "drop",
|
sourceType: "drop",
|
||||||
dropBuildPath: input.dropBuildPath,
|
dropBuildPath: input.dropBuildPath || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
await unzipDrop(zipFile, app);
|
await unzipDrop(zipFile, app);
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
<a href="${WEBSITE_URL}/reset-password?token=${token}">
|
<a href="${WEBSITE_URL}/reset-password?token=${token}">
|
||||||
Reset Password
|
Reset Password
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -505,7 +505,7 @@ export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
|
|||||||
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: " New User Registered",
|
title: "New User Registered",
|
||||||
color: 0x00ff00,
|
color: 0x00ff00,
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db } from "@/server/db";
|
|||||||
import {
|
import {
|
||||||
apiCreateCompose,
|
apiCreateCompose,
|
||||||
apiCreateComposeByTemplate,
|
apiCreateComposeByTemplate,
|
||||||
|
apiDeleteCompose,
|
||||||
apiFetchServices,
|
apiFetchServices,
|
||||||
apiFindCompose,
|
apiFindCompose,
|
||||||
apiRandomizeCompose,
|
apiRandomizeCompose,
|
||||||
@@ -117,7 +118,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
return updateCompose(input.composeId, input);
|
return updateCompose(input.composeId, input);
|
||||||
}),
|
}),
|
||||||
delete: protectedProcedure
|
delete: protectedProcedure
|
||||||
.input(apiFindCompose)
|
.input(apiDeleteCompose)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
await checkServiceAccess(ctx.user.authId, input.composeId, "delete");
|
||||||
@@ -138,7 +139,7 @@ export const composeRouter = createTRPCRouter({
|
|||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
const cleanupOperations = [
|
const cleanupOperations = [
|
||||||
async () => await removeCompose(composeResult),
|
async () => await removeCompose(composeResult, input.deleteVolumes),
|
||||||
async () => await removeDeploymentsByComposeId(composeResult),
|
async () => await removeDeploymentsByComposeId(composeResult),
|
||||||
async () => await removeComposeDirectory(composeResult.appName),
|
async () => await removeComposeDirectory(composeResult.appName),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
getContainers,
|
getContainers,
|
||||||
getContainersByAppLabel,
|
getContainersByAppLabel,
|
||||||
getContainersByAppNameMatch,
|
getContainersByAppNameMatch,
|
||||||
|
getServiceContainersByAppName,
|
||||||
|
getStackContainersByAppName,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
@@ -68,4 +70,26 @@ export const dockerRouter = createTRPCRouter({
|
|||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await getContainersByAppLabel(input.appName, input.serverId);
|
return await getContainersByAppLabel(input.appName, input.serverId);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
getStackContainersByAppName: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getStackContainersByAppName(input.appName, input.serverId);
|
||||||
|
}),
|
||||||
|
|
||||||
|
getServiceContainersByAppName: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
appName: z.string().min(1),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getServiceContainersByAppName(input.appName, input.serverId);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -187,11 +187,15 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
.input(apiTestDiscordConnection)
|
.input(apiTestDiscordConnection)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
try {
|
try {
|
||||||
|
const decorate = (decoration: string, text: string) =>
|
||||||
|
`${input.decoration ? decoration : ""} ${text}`.trim();
|
||||||
|
|
||||||
await sendDiscordNotification(input, {
|
await sendDiscordNotification(input, {
|
||||||
title: "> `🤚` - Test Notification",
|
title: decorate(">", "`🤚` - Test Notification"),
|
||||||
description: "> Hi, From Dokploy 👋",
|
description: decorate(">", "Hi, From Dokploy 👋"),
|
||||||
color: 0xf3f7f4,
|
color: 0xf3f7f4,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
44
apps/dokploy/server/api/routers/swarm.ts
Normal file
44
apps/dokploy/server/api/routers/swarm.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
getApplicationInfo,
|
||||||
|
getNodeApplications,
|
||||||
|
getNodeInfo,
|
||||||
|
getSwarmNodes,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
|
export const swarmRouter = createTRPCRouter({
|
||||||
|
getNodes: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getSwarmNodes(input.serverId);
|
||||||
|
}),
|
||||||
|
getNodeInfo: protectedProcedure
|
||||||
|
.input(z.object({ nodeId: z.string(), serverId: z.string().optional() }))
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getNodeInfo(input.nodeId, input.serverId);
|
||||||
|
}),
|
||||||
|
getNodeApps: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return getNodeApplications(input.serverId);
|
||||||
|
}),
|
||||||
|
getAppInfos: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
appName: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await getApplicationInfo(input.appName, input.serverId);
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -34,6 +34,7 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
const search = url.searchParams.get("search");
|
const search = url.searchParams.get("search");
|
||||||
const since = url.searchParams.get("since");
|
const since = url.searchParams.get("since");
|
||||||
const serverId = url.searchParams.get("serverId");
|
const serverId = url.searchParams.get("serverId");
|
||||||
|
const runType = url.searchParams.get("runType");
|
||||||
const { user, session } = await validateWebSocketRequest(req);
|
const { user, session } = await validateWebSocketRequest(req);
|
||||||
|
|
||||||
if (!containerId) {
|
if (!containerId) {
|
||||||
@@ -53,7 +54,9 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
const client = new Client();
|
const client = new Client();
|
||||||
client
|
client
|
||||||
.once("ready", () => {
|
.once("ready", () => {
|
||||||
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
|
const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${
|
||||||
|
runType === "swarm" ? "--raw" : ""
|
||||||
|
} --tail ${tail} ${
|
||||||
since === "all" ? "" : `--since ${since}`
|
since === "all" ? "" : `--since ${since}`
|
||||||
} --follow ${containerId}`;
|
} --follow ${containerId}`;
|
||||||
const escapedSearch = search ? search.replace(/'/g, "'\\''") : "";
|
const escapedSearch = search ? search.replace(/'/g, "'\\''") : "";
|
||||||
@@ -97,7 +100,9 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const shell = getShell();
|
const shell = getShell();
|
||||||
const baseCommand = `docker container logs --timestamps --tail ${tail} ${
|
const baseCommand = `docker ${runType === "swarm" ? "service" : "container"} logs --timestamps ${
|
||||||
|
runType === "swarm" ? "--raw" : ""
|
||||||
|
} --tail ${tail} ${
|
||||||
since === "all" ? "" : `--since ${since}`
|
since === "all" ? "" : `--since ${since}`
|
||||||
} --follow ${containerId}`;
|
} --follow ${containerId}`;
|
||||||
const command = search
|
const command = search
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require("tailwindcss-animate")],
|
plugins: [require("tailwindcss-animate"), require("fancy-ansi/plugin")],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
type DomainSchema,
|
type DomainSchema,
|
||||||
type Schema,
|
type Schema,
|
||||||
type Template,
|
type Template,
|
||||||
generateRandomDomain,
|
generateRandomDomain,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
export function generate(schema: Schema): Template {
|
export function generate(schema: Schema): Template {
|
||||||
const randomDomain = generateRandomDomain(schema);
|
const randomDomain = generateRandomDomain(schema);
|
||||||
|
|
||||||
const domains: DomainSchema[] = [
|
const domains: DomainSchema[] = [
|
||||||
{
|
{
|
||||||
host: randomDomain,
|
host: randomDomain,
|
||||||
port: 6610,
|
port: 6610,
|
||||||
serviceName: "onedev",
|
serviceName: "onedev",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domains,
|
domains,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
import {
|
import {
|
||||||
generateHash,
|
type DomainSchema,
|
||||||
generateRandomDomain,
|
type Schema,
|
||||||
generateBase64,
|
type Template,
|
||||||
type Template,
|
generateBase64,
|
||||||
type Schema,
|
generateHash,
|
||||||
type DomainSchema,
|
generateRandomDomain,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
export function generate(schema: Schema): Template {
|
export function generate(schema: Schema): Template {
|
||||||
const mainDomain = generateRandomDomain(schema);
|
const mainDomain = generateRandomDomain(schema);
|
||||||
const secretBase = generateBase64(64);
|
const secretBase = generateBase64(64);
|
||||||
|
|
||||||
const domains: DomainSchema[] = [
|
const domains: DomainSchema[] = [
|
||||||
{
|
{
|
||||||
host: mainDomain,
|
host: mainDomain,
|
||||||
port: 3000,
|
port: 3000,
|
||||||
serviceName: "unsend",
|
serviceName: "unsend",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const envs = [
|
const envs = [
|
||||||
"REDIS_URL=redis://unsend-redis-prod:6379",
|
"REDIS_URL=redis://unsend-redis-prod:6379",
|
||||||
"POSTGRES_USER=postgres",
|
"POSTGRES_USER=postgres",
|
||||||
"POSTGRES_PASSWORD=postgres",
|
"POSTGRES_PASSWORD=postgres",
|
||||||
"POSTGRES_DB=unsend",
|
"POSTGRES_DB=unsend",
|
||||||
"DATABASE_URL=postgresql://postgres:postgres@unsend-db-prod:5432/unsend",
|
"DATABASE_URL=postgresql://postgres:postgres@unsend-db-prod:5432/unsend",
|
||||||
"NEXTAUTH_URL=http://localhost:3000",
|
"NEXTAUTH_URL=http://localhost:3000",
|
||||||
`NEXTAUTH_SECRET=${secretBase}`,
|
`NEXTAUTH_SECRET=${secretBase}`,
|
||||||
"GITHUB_ID='Fill'",
|
"GITHUB_ID='Fill'",
|
||||||
"GITHUB_SECRET='Fill'",
|
"GITHUB_SECRET='Fill'",
|
||||||
"AWS_DEFAULT_REGION=us-east-1",
|
"AWS_DEFAULT_REGION=us-east-1",
|
||||||
"AWS_SECRET_KEY='Fill'",
|
"AWS_SECRET_KEY='Fill'",
|
||||||
"AWS_ACCESS_KEY='Fill'",
|
"AWS_ACCESS_KEY='Fill'",
|
||||||
"DOCKER_OUTPUT=1",
|
"DOCKER_OUTPUT=1",
|
||||||
"API_RATE_LIMIT=1",
|
"API_RATE_LIMIT=1",
|
||||||
"DISCORD_WEBHOOK_URL=",
|
"DISCORD_WEBHOOK_URL=",
|
||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
envs,
|
envs,
|
||||||
domains,
|
domains,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,22 @@ export const IS_CLOUD = process.env.IS_CLOUD === "true";
|
|||||||
export const docker = new Docker();
|
export const docker = new Docker();
|
||||||
|
|
||||||
export const paths = (isServer = false) => {
|
export const paths = (isServer = false) => {
|
||||||
if (isServer) {
|
|
||||||
const BASE_PATH = "/etc/dokploy";
|
|
||||||
return {
|
|
||||||
BASE_PATH,
|
|
||||||
MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
|
|
||||||
DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
|
|
||||||
LOGS_PATH: `${BASE_PATH}/logs`,
|
|
||||||
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
|
||||||
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
|
||||||
SSH_PATH: `${BASE_PATH}/ssh`,
|
|
||||||
CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
|
|
||||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
|
||||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const BASE_PATH =
|
const BASE_PATH =
|
||||||
process.env.NODE_ENV === "production"
|
isServer || process.env.NODE_ENV === "production"
|
||||||
? "/etc/dokploy"
|
? "/etc/dokploy"
|
||||||
: path.join(process.cwd(), ".docker");
|
: path.join(process.cwd(), ".docker");
|
||||||
|
const MAIN_TRAEFIK_PATH = `${BASE_PATH}/traefik`;
|
||||||
|
const DYNAMIC_TRAEFIK_PATH = `${MAIN_TRAEFIK_PATH}/dynamic`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
BASE_PATH,
|
BASE_PATH,
|
||||||
MAIN_TRAEFIK_PATH: `${BASE_PATH}/traefik`,
|
MAIN_TRAEFIK_PATH,
|
||||||
DYNAMIC_TRAEFIK_PATH: `${BASE_PATH}/traefik/dynamic`,
|
DYNAMIC_TRAEFIK_PATH,
|
||||||
LOGS_PATH: `${BASE_PATH}/logs`,
|
LOGS_PATH: `${BASE_PATH}/logs`,
|
||||||
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
APPLICATIONS_PATH: `${BASE_PATH}/applications`,
|
||||||
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
COMPOSE_PATH: `${BASE_PATH}/compose`,
|
||||||
SSH_PATH: `${BASE_PATH}/ssh`,
|
SSH_PATH: `${BASE_PATH}/ssh`,
|
||||||
CERTIFICATES_PATH: `${BASE_PATH}/certificates`,
|
CERTIFICATES_PATH: `${DYNAMIC_TRAEFIK_PATH}/certificates`,
|
||||||
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
MONITORING_PATH: `${BASE_PATH}/monitoring`,
|
||||||
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
REGISTRY_PATH: `${BASE_PATH}/registry`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { github } from "./github";
|
|||||||
import { gitlab } from "./gitlab";
|
import { gitlab } from "./gitlab";
|
||||||
import { mounts } from "./mount";
|
import { mounts } from "./mount";
|
||||||
import { ports } from "./port";
|
import { ports } from "./port";
|
||||||
|
import { previewDeployments } from "./preview-deployments";
|
||||||
import { projects } from "./project";
|
import { projects } from "./project";
|
||||||
import { redirects } from "./redirects";
|
import { redirects } from "./redirects";
|
||||||
import { registry } from "./registry";
|
import { registry } from "./registry";
|
||||||
@@ -25,7 +26,6 @@ import { server } from "./server";
|
|||||||
import { applicationStatus, certificateType } from "./shared";
|
import { applicationStatus, certificateType } from "./shared";
|
||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
import { previewDeployments } from "./preview-deployments";
|
|
||||||
|
|
||||||
export const sourceType = pgEnum("sourceType", [
|
export const sourceType = pgEnum("sourceType", [
|
||||||
"docker",
|
"docker",
|
||||||
|
|||||||
@@ -155,6 +155,11 @@ export const apiFindCompose = z.object({
|
|||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const apiDeleteCompose = z.object({
|
||||||
|
composeId: z.string().min(1),
|
||||||
|
deleteVolumes: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiFetchServices = z.object({
|
export const apiFetchServices = z.object({
|
||||||
composeId: z.string().min(1),
|
composeId: z.string().min(1),
|
||||||
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
type: z.enum(["fetch", "cache"]).optional().default("cache"),
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { nanoid } from "nanoid";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { server } from "./server";
|
|
||||||
import { previewDeployments } from "./preview-deployments";
|
import { previewDeployments } from "./preview-deployments";
|
||||||
|
import { server } from "./server";
|
||||||
|
|
||||||
export const deploymentStatus = pgEnum("deploymentStatus", [
|
export const deploymentStatus = pgEnum("deploymentStatus", [
|
||||||
"running",
|
"running",
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { z } from "zod";
|
|||||||
import { domain } from "../validations/domain";
|
import { domain } from "../validations/domain";
|
||||||
import { applications } from "./application";
|
import { applications } from "./application";
|
||||||
import { compose } from "./compose";
|
import { compose } from "./compose";
|
||||||
import { certificateType } from "./shared";
|
|
||||||
import { previewDeployments } from "./preview-deployments";
|
import { previewDeployments } from "./preview-deployments";
|
||||||
|
import { certificateType } from "./shared";
|
||||||
|
|
||||||
export const domainType = pgEnum("domainType", [
|
export const domainType = pgEnum("domainType", [
|
||||||
"compose",
|
"compose",
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ export * from "./github";
|
|||||||
export * from "./gitlab";
|
export * from "./gitlab";
|
||||||
export * from "./server";
|
export * from "./server";
|
||||||
export * from "./utils";
|
export * from "./utils";
|
||||||
export * from "./preview-deployments";
|
export * from "./preview-deployments";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { integer, pgTable, text } from "drizzle-orm/pg-core";
|
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -43,6 +43,7 @@ export const mongo = pgTable("mongo", {
|
|||||||
serverId: text("serverId").references(() => server.serverId, {
|
serverId: text("serverId").references(() => server.serverId, {
|
||||||
onDelete: "cascade",
|
onDelete: "cascade",
|
||||||
}),
|
}),
|
||||||
|
replicaSets: boolean("replicaSets").default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
export const mongoRelations = relations(mongo, ({ one, many }) => ({
|
||||||
@@ -77,6 +78,7 @@ const createSchema = createInsertSchema(mongo, {
|
|||||||
externalPort: z.number(),
|
externalPort: z.number(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
|
replicaSets: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiCreateMongo = createSchema
|
export const apiCreateMongo = createSchema
|
||||||
@@ -89,6 +91,7 @@ export const apiCreateMongo = createSchema
|
|||||||
databaseUser: true,
|
databaseUser: true,
|
||||||
databasePassword: true,
|
databasePassword: true,
|
||||||
serverId: true,
|
serverId: true,
|
||||||
|
replicaSets: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const discord = pgTable("discord", {
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => nanoid()),
|
.$defaultFn(() => nanoid()),
|
||||||
webhookUrl: text("webhookUrl").notNull(),
|
webhookUrl: text("webhookUrl").notNull(),
|
||||||
|
decoration: boolean("decoration"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const email = pgTable("email", {
|
export const email = pgTable("email", {
|
||||||
@@ -171,6 +172,7 @@ export const apiCreateDiscord = notificationsSchema
|
|||||||
})
|
})
|
||||||
.extend({
|
.extend({
|
||||||
webhookUrl: z.string().min(1),
|
webhookUrl: z.string().min(1),
|
||||||
|
decoration: z.boolean(),
|
||||||
})
|
})
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
@@ -180,9 +182,13 @@ export const apiUpdateDiscord = apiCreateDiscord.partial().extend({
|
|||||||
adminId: z.string().optional(),
|
adminId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const apiTestDiscordConnection = apiCreateDiscord.pick({
|
export const apiTestDiscordConnection = apiCreateDiscord
|
||||||
webhookUrl: true,
|
.pick({
|
||||||
});
|
webhookUrl: true,
|
||||||
|
})
|
||||||
|
.extend({
|
||||||
|
decoration: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiCreateEmail = notificationsSchema
|
export const apiCreateEmail = notificationsSchema
|
||||||
.pick({
|
.pick({
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { pgTable, text } from "drizzle-orm/pg-core";
|
import { pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { applications } from "./application";
|
|
||||||
import { domains } from "./domain";
|
|
||||||
import { deployments } from "./deployment";
|
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { generateAppName } from "./utils";
|
import { applications } from "./application";
|
||||||
|
import { deployments } from "./deployment";
|
||||||
|
import { domains } from "./domain";
|
||||||
import { applicationStatus } from "./shared";
|
import { applicationStatus } from "./shared";
|
||||||
|
import { generateAppName } from "./utils";
|
||||||
|
|
||||||
export const previewDeployments = pgTable("preview_deployments", {
|
export const previewDeployments = pgTable("preview_deployments", {
|
||||||
previewDeploymentId: text("previewDeploymentId")
|
previewDeploymentId: text("previewDeploymentId")
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { customAlphabet } from "nanoid";
|
import { customAlphabet } from "nanoid";
|
||||||
|
|
||||||
@@ -13,3 +14,17 @@ export const generateAppName = (type: string) => {
|
|||||||
const nanoidPart = customNanoid();
|
const nanoidPart = customNanoid();
|
||||||
return `${type}-${randomFakerElement}-${nanoidPart}`;
|
return `${type}-${randomFakerElement}-${nanoidPart}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const cleanAppName = (appName?: string) => {
|
||||||
|
if (!appName) {
|
||||||
|
return appName?.toLowerCase();
|
||||||
|
}
|
||||||
|
return appName.trim().replace(/ /g, "-").toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildAppName = (type: string, baseAppName?: string) => {
|
||||||
|
if (baseAppName) {
|
||||||
|
return `${cleanAppName(baseAppName)}-${generatePassword(6)}`;
|
||||||
|
}
|
||||||
|
return generateAppName(type);
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { db } from "@dokploy/server/db";
|
|||||||
import {
|
import {
|
||||||
type apiCreateApplication,
|
type apiCreateApplication,
|
||||||
applications,
|
applications,
|
||||||
|
buildAppName,
|
||||||
|
cleanAppName,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { generateAppName } from "@dokploy/server/db/schema";
|
|
||||||
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
|
import { getAdvancedStats } from "@dokploy/server/monitoring/utilts";
|
||||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
|
||||||
import {
|
import {
|
||||||
buildApplication,
|
buildApplication,
|
||||||
getBuildCommand,
|
getBuildCommand,
|
||||||
@@ -46,34 +46,31 @@ import {
|
|||||||
createDeploymentPreview,
|
createDeploymentPreview,
|
||||||
updateDeploymentStatus,
|
updateDeploymentStatus,
|
||||||
} from "./deployment";
|
} from "./deployment";
|
||||||
import { validUniqueServerAppName } from "./project";
|
import { type Domain, getDomainHost } from "./domain";
|
||||||
import {
|
|
||||||
findPreviewDeploymentById,
|
|
||||||
updatePreviewDeployment,
|
|
||||||
} from "./preview-deployment";
|
|
||||||
import {
|
import {
|
||||||
createPreviewDeploymentComment,
|
createPreviewDeploymentComment,
|
||||||
getIssueComment,
|
getIssueComment,
|
||||||
issueCommentExists,
|
issueCommentExists,
|
||||||
updateIssueComment,
|
updateIssueComment,
|
||||||
} from "./github";
|
} from "./github";
|
||||||
import { type Domain, getDomainHost } from "./domain";
|
import {
|
||||||
|
findPreviewDeploymentById,
|
||||||
|
updatePreviewDeployment,
|
||||||
|
} from "./preview-deployment";
|
||||||
|
import { validUniqueServerAppName } from "./project";
|
||||||
export type Application = typeof applications.$inferSelect;
|
export type Application = typeof applications.$inferSelect;
|
||||||
|
|
||||||
export const createApplication = async (
|
export const createApplication = async (
|
||||||
input: typeof apiCreateApplication._type,
|
input: typeof apiCreateApplication._type,
|
||||||
) => {
|
) => {
|
||||||
input.appName =
|
const appName = buildAppName("app", input.appName);
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("app");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
const valid = await validUniqueServerAppName(appName);
|
||||||
throw new TRPCError({
|
if (!valid) {
|
||||||
code: "CONFLICT",
|
throw new TRPCError({
|
||||||
message: "Application with this 'AppName' already exists",
|
code: "CONFLICT",
|
||||||
});
|
message: "Application with this 'AppName' already exists",
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
@@ -81,6 +78,7 @@ export const createApplication = async (
|
|||||||
.insert(applications)
|
.insert(applications)
|
||||||
.values({
|
.values({
|
||||||
...input,
|
...input,
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -140,10 +138,11 @@ export const updateApplication = async (
|
|||||||
applicationId: string,
|
applicationId: string,
|
||||||
applicationData: Partial<Application>,
|
applicationData: Partial<Application>,
|
||||||
) => {
|
) => {
|
||||||
|
const { appName, ...rest } = applicationData;
|
||||||
const application = await db
|
const application = await db
|
||||||
.update(applications)
|
.update(applications)
|
||||||
.set({
|
.set({
|
||||||
...applicationData,
|
...rest,
|
||||||
})
|
})
|
||||||
.where(eq(applications.applicationId, applicationId))
|
.where(eq(applications.applicationId, applicationId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { join } from "node:path";
|
|||||||
import { paths } from "@dokploy/server/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
|
import { type apiCreateCompose, compose } from "@dokploy/server/db/schema";
|
||||||
import { generateAppName } from "@dokploy/server/db/schema";
|
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||||
import {
|
import {
|
||||||
buildCompose,
|
buildCompose,
|
||||||
@@ -52,17 +52,14 @@ import { validUniqueServerAppName } from "./project";
|
|||||||
export type Compose = typeof compose.$inferSelect;
|
export type Compose = typeof compose.$inferSelect;
|
||||||
|
|
||||||
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
||||||
input.appName =
|
const appName = buildAppName("compose", input.appName);
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("compose");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
const valid = await validUniqueServerAppName(appName);
|
||||||
throw new TRPCError({
|
if (!valid) {
|
||||||
code: "CONFLICT",
|
throw new TRPCError({
|
||||||
message: "Service with this 'AppName' already exists",
|
code: "CONFLICT",
|
||||||
});
|
message: "Service with this 'AppName' already exists",
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDestination = await db
|
const newDestination = await db
|
||||||
@@ -70,6 +67,7 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|||||||
.values({
|
.values({
|
||||||
...input,
|
...input,
|
||||||
composeFile: "",
|
composeFile: "",
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -87,8 +85,9 @@ export const createCompose = async (input: typeof apiCreateCompose._type) => {
|
|||||||
export const createComposeByTemplate = async (
|
export const createComposeByTemplate = async (
|
||||||
input: typeof compose.$inferInsert,
|
input: typeof compose.$inferInsert,
|
||||||
) => {
|
) => {
|
||||||
if (input.appName) {
|
const appName = cleanAppName(input.appName);
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
if (appName) {
|
||||||
|
const valid = await validUniqueServerAppName(appName);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
@@ -101,6 +100,7 @@ export const createComposeByTemplate = async (
|
|||||||
.insert(compose)
|
.insert(compose)
|
||||||
.values({
|
.values({
|
||||||
...input,
|
...input,
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -184,10 +184,11 @@ export const updateCompose = async (
|
|||||||
composeId: string,
|
composeId: string,
|
||||||
composeData: Partial<Compose>,
|
composeData: Partial<Compose>,
|
||||||
) => {
|
) => {
|
||||||
|
const { appName, ...rest } = composeData;
|
||||||
const composeResult = await db
|
const composeResult = await db
|
||||||
.update(compose)
|
.update(compose)
|
||||||
.set({
|
.set({
|
||||||
...composeData,
|
...rest,
|
||||||
})
|
})
|
||||||
.where(eq(compose.composeId, composeId))
|
.where(eq(compose.composeId, composeId))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -205,7 +206,9 @@ export const deployCompose = async ({
|
|||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||||
|
compose.projectId
|
||||||
|
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||||
const deployment = await createDeploymentCompose({
|
const deployment = await createDeploymentCompose({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
title: titleLog,
|
title: titleLog,
|
||||||
@@ -307,7 +310,9 @@ export const deployRemoteCompose = async ({
|
|||||||
descriptionLog: string;
|
descriptionLog: string;
|
||||||
}) => {
|
}) => {
|
||||||
const compose = await findComposeById(composeId);
|
const compose = await findComposeById(composeId);
|
||||||
const buildLink = `${await getDokployUrl()}/dashboard/project/${compose.projectId}/services/compose/${compose.composeId}?tab=deployments`;
|
const buildLink = `${await getDokployUrl()}/dashboard/project/${
|
||||||
|
compose.projectId
|
||||||
|
}/services/compose/${compose.composeId}?tab=deployments`;
|
||||||
const deployment = await createDeploymentCompose({
|
const deployment = await createDeploymentCompose({
|
||||||
composeId: composeId,
|
composeId: composeId,
|
||||||
title: titleLog,
|
title: titleLog,
|
||||||
@@ -436,13 +441,17 @@ export const rebuildRemoteCompose = async ({
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeCompose = async (compose: Compose) => {
|
export const removeCompose = async (
|
||||||
|
compose: Compose,
|
||||||
|
deleteVolumes: boolean,
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
const { COMPOSE_PATH } = paths(!!compose.serverId);
|
||||||
const projectPath = join(COMPOSE_PATH, compose.appName);
|
const projectPath = join(COMPOSE_PATH, compose.appName);
|
||||||
|
|
||||||
if (compose.composeType === "stack") {
|
if (compose.composeType === "stack") {
|
||||||
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
const command = `cd ${projectPath} && docker stack rm ${compose.appName} && rm -rf ${projectPath}`;
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(compose.serverId, command);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
@@ -452,7 +461,13 @@ export const removeCompose = async (compose: Compose) => {
|
|||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
let command: string;
|
||||||
|
if (deleteVolumes) {
|
||||||
|
command = `cd ${projectPath} && docker compose -p ${compose.appName} down --volumes && rm -rf ${projectPath}`;
|
||||||
|
} else {
|
||||||
|
command = `cd ${projectPath} && docker compose -p ${compose.appName} down && rm -rf ${projectPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(compose.serverId, command);
|
await execAsyncRemote(compose.serverId, command);
|
||||||
} else {
|
} else {
|
||||||
@@ -476,7 +491,11 @@ export const startCompose = async (composeId: string) => {
|
|||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
compose.serverId,
|
compose.serverId,
|
||||||
`cd ${join(COMPOSE_PATH, compose.appName, "code")} && docker compose -p ${compose.appName} up -d`,
|
`cd ${join(
|
||||||
|
COMPOSE_PATH,
|
||||||
|
compose.appName,
|
||||||
|
"code",
|
||||||
|
)} && docker compose -p ${compose.appName} up -d`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
await execAsync(`docker compose -p ${compose.appName} up -d`, {
|
||||||
@@ -506,7 +525,9 @@ export const stopCompose = async (composeId: string) => {
|
|||||||
if (compose.serverId) {
|
if (compose.serverId) {
|
||||||
await execAsyncRemote(
|
await execAsyncRemote(
|
||||||
compose.serverId,
|
compose.serverId,
|
||||||
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${compose.appName} stop`,
|
`cd ${join(COMPOSE_PATH, compose.appName)} && docker compose -p ${
|
||||||
|
compose.appName
|
||||||
|
} stop`,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
await execAsync(`docker compose -p ${compose.appName} stop`, {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import { type Server, findServerById } from "./server";
|
|||||||
|
|
||||||
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
||||||
import {
|
import {
|
||||||
findPreviewDeploymentById,
|
|
||||||
type PreviewDeployment,
|
type PreviewDeployment,
|
||||||
|
findPreviewDeploymentById,
|
||||||
updatePreviewDeployment,
|
updatePreviewDeployment,
|
||||||
} from "./preview-deployment";
|
} from "./preview-deployment";
|
||||||
|
|
||||||
|
|||||||
@@ -157,6 +157,124 @@ export const getContainersByAppNameMatch = async (
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getStackContainersByAppName = async (
|
||||||
|
appName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let result: string[] = [];
|
||||||
|
|
||||||
|
const command = `docker stack ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stdout) return [];
|
||||||
|
result = stdout.trim().split("\n");
|
||||||
|
} else {
|
||||||
|
const { stdout, stderr } = await execAsync(command);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stdout) return [];
|
||||||
|
|
||||||
|
result = stdout.trim().split("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = result.map((line) => {
|
||||||
|
const parts = line.split(" | ");
|
||||||
|
const containerId = parts[0]
|
||||||
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
|
: "No container id";
|
||||||
|
const name = parts[1]
|
||||||
|
? parts[1].replace("Name: ", "").trim()
|
||||||
|
: "No container name";
|
||||||
|
|
||||||
|
const state = parts[2]
|
||||||
|
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||||
|
: "No state";
|
||||||
|
const node = parts[3]
|
||||||
|
? parts[3].replace("Node: ", "").trim()
|
||||||
|
: "No specific node";
|
||||||
|
return {
|
||||||
|
containerId,
|
||||||
|
name,
|
||||||
|
state,
|
||||||
|
node,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return containers || [];
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getServiceContainersByAppName = async (
|
||||||
|
appName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let result: string[] = [];
|
||||||
|
|
||||||
|
const command = `docker service ps ${appName} --format 'CONTAINER ID : {{.ID}} | Name: {{.Name}} | State: {{.DesiredState}} | Node: {{.Node}}'`;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const { stdout, stderr } = await execAsyncRemote(serverId, command);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stdout) return [];
|
||||||
|
result = stdout.trim().split("\n");
|
||||||
|
} else {
|
||||||
|
const { stdout, stderr } = await execAsync(command);
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stdout) return [];
|
||||||
|
|
||||||
|
result = stdout.trim().split("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = result.map((line) => {
|
||||||
|
const parts = line.split(" | ");
|
||||||
|
const containerId = parts[0]
|
||||||
|
? parts[0].replace("CONTAINER ID : ", "").trim()
|
||||||
|
: "No container id";
|
||||||
|
const name = parts[1]
|
||||||
|
? parts[1].replace("Name: ", "").trim()
|
||||||
|
: "No container name";
|
||||||
|
|
||||||
|
const state = parts[2]
|
||||||
|
? parts[2].replace("State: ", "").trim().toLowerCase()
|
||||||
|
: "No state";
|
||||||
|
|
||||||
|
const node = parts[3]
|
||||||
|
? parts[3].replace("Node: ", "").trim()
|
||||||
|
: "No specific node";
|
||||||
|
return {
|
||||||
|
containerId,
|
||||||
|
name,
|
||||||
|
state,
|
||||||
|
node,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return containers || [];
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
export const getContainersByAppLabel = async (
|
export const getContainersByAppLabel = async (
|
||||||
appName: string,
|
appName: string,
|
||||||
serverId?: string,
|
serverId?: string,
|
||||||
@@ -224,3 +342,123 @@ export const containerRestart = async (containerId: string) => {
|
|||||||
return config;
|
return config;
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSwarmNodes = async (serverId?: string) => {
|
||||||
|
try {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
const command = "docker node ls --format '{{json .}}'";
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodesArray = stdout
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => JSON.parse(line));
|
||||||
|
return nodesArray;
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNodeInfo = async (nodeId: string, serverId?: string) => {
|
||||||
|
try {
|
||||||
|
const command = `docker node inspect ${nodeId} --format '{{json .}}'`;
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeInfo = JSON.parse(stdout);
|
||||||
|
|
||||||
|
return nodeInfo;
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNodeApplications = async (serverId?: string) => {
|
||||||
|
try {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
const command = `docker service ls --format '{{json .}}'`;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appArray = stdout
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => JSON.parse(line))
|
||||||
|
.filter((service) => !service.Name.startsWith("dokploy-"));
|
||||||
|
|
||||||
|
return appArray;
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApplicationInfo = async (
|
||||||
|
appName: string,
|
||||||
|
serverId?: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
const command = `docker service ps ${appName} --format '{{json .}}' --no-trunc`;
|
||||||
|
|
||||||
|
if (serverId) {
|
||||||
|
const result = await execAsyncRemote(serverId, command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
} else {
|
||||||
|
const result = await execAsync(command);
|
||||||
|
stdout = result.stdout;
|
||||||
|
stderr = result.stderr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error(`Error: ${stderr}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appArray = stdout
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => JSON.parse(line));
|
||||||
|
|
||||||
|
return appArray;
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
backups,
|
backups,
|
||||||
mariadb,
|
mariadb,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { generateAppName } from "@dokploy/server/db/schema";
|
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||||
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
|
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
|
||||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||||
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
|||||||
export type Mariadb = typeof mariadb.$inferSelect;
|
export type Mariadb = typeof mariadb.$inferSelect;
|
||||||
|
|
||||||
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
||||||
input.appName =
|
const appName = buildAppName("mariadb", input.appName);
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mariadb");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
const valid = await validUniqueServerAppName(input.appName);
|
||||||
throw new TRPCError({
|
if (!valid) {
|
||||||
code: "CONFLICT",
|
throw new TRPCError({
|
||||||
message: "Service with this 'AppName' already exists",
|
code: "CONFLICT",
|
||||||
});
|
message: "Service with this 'AppName' already exists",
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMariadb = await db
|
const newMariadb = await db
|
||||||
@@ -40,6 +37,7 @@ export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
|
|||||||
databaseRootPassword: input.databaseRootPassword
|
databaseRootPassword: input.databaseRootPassword
|
||||||
? input.databaseRootPassword
|
? input.databaseRootPassword
|
||||||
: generatePassword(),
|
: generatePassword(),
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -82,10 +80,11 @@ export const updateMariadbById = async (
|
|||||||
mariadbId: string,
|
mariadbId: string,
|
||||||
mariadbData: Partial<Mariadb>,
|
mariadbData: Partial<Mariadb>,
|
||||||
) => {
|
) => {
|
||||||
|
const { appName, ...rest } = mariadbData;
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(mariadb)
|
.update(mariadb)
|
||||||
.set({
|
.set({
|
||||||
...mariadbData,
|
...rest,
|
||||||
})
|
})
|
||||||
.where(eq(mariadb.mariadbId, mariadbId))
|
.where(eq(mariadb.mariadbId, mariadbId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
|
import { type apiCreateMongo, backups, mongo } from "@dokploy/server/db/schema";
|
||||||
import { generateAppName } from "@dokploy/server/db/schema";
|
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||||
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
|
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
|
||||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||||
@@ -13,17 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
|||||||
export type Mongo = typeof mongo.$inferSelect;
|
export type Mongo = typeof mongo.$inferSelect;
|
||||||
|
|
||||||
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
||||||
input.appName =
|
const appName = buildAppName("mongo", input.appName);
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mongo");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
const valid = await validUniqueServerAppName(appName);
|
||||||
throw new TRPCError({
|
if (!valid) {
|
||||||
code: "CONFLICT",
|
throw new TRPCError({
|
||||||
message: "Service with this 'AppName' already exists",
|
code: "CONFLICT",
|
||||||
});
|
message: "Service with this 'AppName' already exists",
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMongo = await db
|
const newMongo = await db
|
||||||
@@ -33,6 +30,7 @@ export const createMongo = async (input: typeof apiCreateMongo._type) => {
|
|||||||
databasePassword: input.databasePassword
|
databasePassword: input.databasePassword
|
||||||
? input.databasePassword
|
? input.databasePassword
|
||||||
: generatePassword(),
|
: generatePassword(),
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -74,10 +72,11 @@ export const updateMongoById = async (
|
|||||||
mongoId: string,
|
mongoId: string,
|
||||||
mongoData: Partial<Mongo>,
|
mongoData: Partial<Mongo>,
|
||||||
) => {
|
) => {
|
||||||
|
const { appName, ...rest } = mongoData;
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(mongo)
|
.update(mongo)
|
||||||
.set({
|
.set({
|
||||||
...mongoData,
|
...rest,
|
||||||
})
|
})
|
||||||
.where(eq(mongo.mongoId, mongoId))
|
.where(eq(mongo.mongoId, mongoId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
|
import { type apiCreateMySql, backups, mysql } from "@dokploy/server/db/schema";
|
||||||
import { generateAppName } from "@dokploy/server/db/schema";
|
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||||
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
|
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
|
||||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||||
@@ -13,18 +13,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
|||||||
export type MySql = typeof mysql.$inferSelect;
|
export type MySql = typeof mysql.$inferSelect;
|
||||||
|
|
||||||
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
||||||
input.appName =
|
const appName = buildAppName("mysql", input.appName);
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("mysql");
|
|
||||||
|
|
||||||
if (input.appName) {
|
const valid = await validUniqueServerAppName(appName);
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
if (!valid) {
|
||||||
|
throw new TRPCError({
|
||||||
if (!valid) {
|
code: "CONFLICT",
|
||||||
throw new TRPCError({
|
message: "Service with this 'AppName' already exists",
|
||||||
code: "CONFLICT",
|
});
|
||||||
message: "Service with this 'AppName' already exists",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMysql = await db
|
const newMysql = await db
|
||||||
@@ -37,6 +33,7 @@ export const createMysql = async (input: typeof apiCreateMySql._type) => {
|
|||||||
databaseRootPassword: input.databaseRootPassword
|
databaseRootPassword: input.databaseRootPassword
|
||||||
? input.databaseRootPassword
|
? input.databaseRootPassword
|
||||||
: generatePassword(),
|
: generatePassword(),
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -79,10 +76,11 @@ export const updateMySqlById = async (
|
|||||||
mysqlId: string,
|
mysqlId: string,
|
||||||
mysqlData: Partial<MySql>,
|
mysqlData: Partial<MySql>,
|
||||||
) => {
|
) => {
|
||||||
|
const { appName, ...rest } = mysqlData;
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(mysql)
|
.update(mysql)
|
||||||
.set({
|
.set({
|
||||||
...mysqlData,
|
...rest,
|
||||||
})
|
})
|
||||||
.where(eq(mysql.mysqlId, mysqlId))
|
.where(eq(mysql.mysqlId, mysqlId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ export const createDiscordNotification = async (
|
|||||||
.insert(discord)
|
.insert(discord)
|
||||||
.values({
|
.values({
|
||||||
webhookUrl: input.webhookUrl,
|
webhookUrl: input.webhookUrl,
|
||||||
|
decoration: input.decoration,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -272,6 +273,7 @@ export const updateDiscordNotification = async (
|
|||||||
.update(discord)
|
.update(discord)
|
||||||
.set({
|
.set({
|
||||||
webhookUrl: input.webhookUrl,
|
webhookUrl: input.webhookUrl,
|
||||||
|
decoration: input.decoration,
|
||||||
})
|
})
|
||||||
.where(eq(discord.discordId, input.discordId))
|
.where(eq(discord.discordId, input.discordId))
|
||||||
.returning()
|
.returning()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
backups,
|
backups,
|
||||||
postgres,
|
postgres,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
import { generateAppName } from "@dokploy/server/db/schema";
|
import { buildAppName, cleanAppName } from "@dokploy/server/db/schema";
|
||||||
import { generatePassword } from "@dokploy/server/templates/utils";
|
import { generatePassword } from "@dokploy/server/templates/utils";
|
||||||
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
|
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
|
||||||
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
import { pullImage } from "@dokploy/server/utils/docker/utils";
|
||||||
@@ -17,17 +17,14 @@ import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
|
|||||||
export type Postgres = typeof postgres.$inferSelect;
|
export type Postgres = typeof postgres.$inferSelect;
|
||||||
|
|
||||||
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
||||||
input.appName =
|
const appName = buildAppName("postgres", input.appName);
|
||||||
`${input.appName}-${generatePassword(6)}` || generateAppName("postgres");
|
|
||||||
if (input.appName) {
|
|
||||||
const valid = await validUniqueServerAppName(input.appName);
|
|
||||||
|
|
||||||
if (!valid) {
|
const valid = await validUniqueServerAppName(appName);
|
||||||
throw new TRPCError({
|
if (!valid) {
|
||||||
code: "CONFLICT",
|
throw new TRPCError({
|
||||||
message: "Service with this 'AppName' already exists",
|
code: "CONFLICT",
|
||||||
});
|
message: "Service with this 'AppName' already exists",
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPostgres = await db
|
const newPostgres = await db
|
||||||
@@ -37,6 +34,7 @@ export const createPostgres = async (input: typeof apiCreatePostgres._type) => {
|
|||||||
databasePassword: input.databasePassword
|
databasePassword: input.databasePassword
|
||||||
? input.databasePassword
|
? input.databasePassword
|
||||||
: generatePassword(),
|
: generatePassword(),
|
||||||
|
appName,
|
||||||
})
|
})
|
||||||
.returning()
|
.returning()
|
||||||
.then((value) => value[0]);
|
.then((value) => value[0]);
|
||||||
@@ -96,10 +94,11 @@ export const updatePostgresById = async (
|
|||||||
postgresId: string,
|
postgresId: string,
|
||||||
postgresData: Partial<Postgres>,
|
postgresData: Partial<Postgres>,
|
||||||
) => {
|
) => {
|
||||||
|
const { appName, ...rest } = postgresData;
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(postgres)
|
.update(postgres)
|
||||||
.set({
|
.set({
|
||||||
...postgresData,
|
...rest,
|
||||||
})
|
})
|
||||||
.where(eq(postgres.postgresId, postgresId))
|
.where(eq(postgres.postgresId, postgresId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user