From 75e34285ef5eeadb8644956010920d48cf87b225 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 16 Dec 2024 11:51:17 -0500 Subject: [PATCH 01/62] feat(cluster): use code editor for node config --- .../dashboard/settings/cluster/nodes/show-node-data.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-node-data.tsx b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-node-data.tsx index c597b948..e2adbed7 100644 --- a/apps/dokploy/components/dashboard/settings/cluster/nodes/show-node-data.tsx +++ b/apps/dokploy/components/dashboard/settings/cluster/nodes/show-node-data.tsx @@ -1,3 +1,4 @@ +import { CodeEditor } from "@/components/shared/code-editor"; import { Dialog, DialogContent, @@ -33,7 +34,13 @@ export const ShowNodeData = ({ data }: Props) => {
-							{JSON.stringify(data, null, 2)}
+							
 						
From 7577e40b257b87cc55b300d5964fafe933830aed Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 16 Dec 2024 19:49:09 +0100 Subject: [PATCH 02/62] feat(logs): added filter log type component --- .../docker/logs/status-logs-filter.tsx | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx diff --git a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx new file mode 100644 index 00000000..d6bd81bf --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx @@ -0,0 +1,107 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { CheckIcon, PlusCircle } from "lucide-react"; +import React from "react"; + +interface StatusLogsFilterProps { + value?: string[]; + setValue?: (value: string[]) => void; + title?: string; + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function StatusLogsFilter({ + value = [], + setValue, + title, + options, +}: StatusLogsFilterProps) { + const selectedValues = new Set(value as string[]); + + return ( + + + + + + + + + No results found. + + {options.map((option) => { + const isSelected = selectedValues.has(option.value); + return ( + { + if (isSelected) { + selectedValues.delete(option.value); + } else { + selectedValues.add(option.value); + } + const filterValues = Array.from(selectedValues); + setValue?.(filterValues.length ? filterValues : []); + }} + > +
+ +
+ {option.icon && ( + + )} + {option.label} +
+ ); + })} +
+ +
+
+
+
+ ); +} From b03011a94ff556398b2b4e499b6b17ae8dd9a23d Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 16 Dec 2024 19:50:13 +0100 Subject: [PATCH 03/62] feat(logs): replaced the log type component with the new --- .../dashboard/docker/logs/docker-logs-id.tsx | 85 +++++++++++-------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index db1c774b..951f1ff7 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -9,18 +9,50 @@ import { SelectValue, } from "@/components/ui/select"; import { api } from "@/utils/api"; -import { Download as DownloadIcon, Loader2 } from "lucide-react"; +import { + CheckCircle2Icon, + Download as DownloadIcon, + Loader2, + Bug, + InfoIcon, + CircleX, + TriangleAlert, +} from "lucide-react"; import React, { useEffect, useRef } from "react"; import { TerminalLine } from "./terminal-line"; import { type LogLine, getLogType, parseLogs } from "./utils"; +import { StatusLogsFilter } from "./status-logs-filter"; interface Props { containerId: string; serverId?: string | null; } + type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; -type TypeFilter = "all" | "error" | "warning" | "success" | "info" | "debug"; + +export const priorities = [ + { + label: "Info", + value: "info", + }, + { + label: "Success", + value: "success", + }, + { + label: "Warning", + value: "warning", + }, + { + label: "Debug", + value: "debug", + }, + { + label: "Error", + value: "error", + }, +]; export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const { data } = api.docker.getConfig.useQuery( @@ -40,7 +72,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const [search, setSearch] = React.useState(""); const [since, setSince] = React.useState("all"); - const [typeFilter, setTypeFilter] = React.useState("all"); + const [typeFilter, setTypeFilter] = React.useState([]); const scrollRef = useRef(null); const [isLoading, setIsLoading] = React.useState(false); @@ -74,10 +106,6 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { setSince(value); }; - const handleTypeFilter = (value: TypeFilter) => { - setTypeFilter(value); - }; - useEffect(() => { if (!containerId) return; @@ -179,11 +207,13 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const handleFilter = (logs: LogLine[]) => { return logs.filter((log) => { const logType = getLogType(log.message).type; + + if (typeFilter.length === 0) { + return true; + } - const matchesType = typeFilter === "all" || logType === typeFilter; - - return matchesType; - }); + return typeFilter.includes(logType); + }); }; useEffect(() => { @@ -232,32 +262,13 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { All time - - + + Date: Mon, 16 Dec 2024 19:56:47 +0100 Subject: [PATCH 04/62] fix: fixed lint --- .../components/dashboard/docker/logs/docker-logs-id.tsx | 9 ++------- .../dashboard/docker/logs/status-logs-filter.tsx | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 951f1ff7..34f74e2a 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -10,13 +10,8 @@ import { } from "@/components/ui/select"; import { api } from "@/utils/api"; import { - CheckCircle2Icon, Download as DownloadIcon, - Loader2, - Bug, - InfoIcon, - CircleX, - TriangleAlert, + Loader2 } from "lucide-react"; import React, { useEffect, useRef } from "react"; import { TerminalLine } from "./terminal-line"; @@ -262,7 +257,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { All time - + Date: Mon, 16 Dec 2024 14:55:02 -0500 Subject: [PATCH 05/62] feat(logs): support ansi codes --- .../dashboard/docker/logs/terminal-line.tsx | 46 +++++--- .../components/dashboard/docker/logs/utils.ts | 106 +++++++++++++++++- 2 files changed, 135 insertions(+), 17 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index cdbbb2c8..2f247e25 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -9,7 +9,7 @@ import { import { cn } from "@/lib/utils"; import { escapeRegExp } from "lodash"; import React from "react"; -import { type LogLine, getLogType } from "./utils"; +import { type LogLine, getLogType, parseAnsi } from "./utils"; interface LogLineProps { log: LogLine; @@ -33,18 +33,38 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { : "--- No time found ---"; const highlightMessage = (text: string, term: string) => { - if (!term) return text; - - const parts = text.split(new RegExp(`(${escapeRegExp(term)})`, "gi")); - return parts.map((part, index) => - part.toLowerCase() === term.toLowerCase() ? ( - - {part} + if (!term) { + const segments = parseAnsi(text); + return segments.map((segment, index) => ( + + {segment.text} - ) : ( - part - ), - ); + )); + } + + // For search, we need to handle both ANSI and search highlighting + const segments = parseAnsi(text); + return segments.map((segment, index) => { + const parts = segment.text.split( + new RegExp(`(${escapeRegExp(term)})`, "gi"), + ); + return ( + + {parts.map((part, partIndex) => + part.toLowerCase() === term.toLowerCase() ? ( + + {part} + + ) : ( + part + ), + )} + + ); + }); }; const tooltip = (color: string, timestamp: string | null) => { @@ -104,7 +124,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { - {searchTerm ? highlightMessage(message, searchTerm) : message} + {highlightMessage(message, searchTerm || "")} ); diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 409c6989..48219428 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -1,5 +1,5 @@ -export type LogType = "error" | "warning" | "success" | "info" | "debug"; -export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; +export type LogType = "error" | "warning" | "success" | "info" | "debug"; +export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; export interface LogLine { rawTimestamp: string | null; @@ -12,6 +12,47 @@ interface LogStyle { variant: LogVariant; color: string; } +interface AnsiSegment { + text: string; + className: string; +} + +const ansiToTailwind: Record = { + // Reset + 0: "", + // Regular colors + 30: "text-black dark:text-gray-900", + 31: "text-red-600 dark:text-red-500", + 32: "text-green-600 dark:text-green-500", + 33: "text-yellow-600 dark:text-yellow-500", + 34: "text-blue-600 dark:text-blue-500", + 35: "text-purple-600 dark:text-purple-500", + 36: "text-cyan-600 dark:text-cyan-500", + 37: "text-gray-600 dark:text-gray-400", + // Bright colors + 90: "text-gray-500 dark:text-gray-600", + 91: "text-red-500 dark:text-red-600", + 92: "text-green-500 dark:text-green-600", + 93: "text-yellow-500 dark:text-yellow-600", + 94: "text-blue-500 dark:text-blue-600", + 95: "text-purple-500 dark:text-purple-600", + 96: "text-cyan-500 dark:text-cyan-600", + 97: "text-white dark:text-gray-300", + // Background colors + 40: "bg-black", + 41: "bg-red-600", + 42: "bg-green-600", + 43: "bg-yellow-600", + 44: "bg-blue-600", + 45: "bg-purple-600", + 46: "bg-cyan-600", + 47: "bg-white", + // Formatting + 1: "font-bold", + 2: "opacity-75", + 3: "italic", + 4: "underline", +}; const LOG_STYLES: Record = { error: { @@ -138,11 +179,68 @@ export const getLogType = (message: string): LogStyle => { if ( /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || - /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || - /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test( + lowerMessage, + ) || + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test( + lowerMessage, + ) ) { return LOG_STYLES.debug; } return LOG_STYLES.info; }; + +export function parseAnsi(text: string) { + const segments: { text: string; className: string }[] = []; + let currentIndex = 0; + let currentClasses: string[] = []; + + while (currentIndex < text.length) { + const escStart = text.indexOf("\x1b[", currentIndex); + + // No more escape sequences found + if (escStart === -1) { + if (currentIndex < text.length) { + segments.push({ + text: text.slice(currentIndex), + className: currentClasses.join(" "), + }); + } + break; + } + + // Add text before escape sequence + if (escStart > currentIndex) { + segments.push({ + text: text.slice(currentIndex, escStart), + className: currentClasses.join(" "), + }); + } + + const escEnd = text.indexOf("m", escStart); + if (escEnd === -1) break; + + // Handle multiple codes in one sequence (e.g., \x1b[1;31m) + const codesStr = text.slice(escStart + 2, escEnd); + const codes = codesStr.split(";").map((c) => Number.parseInt(c, 10)); + + if (codes.includes(0)) { + // Reset all formatting + currentClasses = []; + } else { + // Add new classes for each code + for (const code of codes) { + const className = ansiToTailwind[code]; + if (className && !currentClasses.includes(className)) { + currentClasses.push(className); + } + } + } + + currentIndex = escEnd + 1; + } + + return segments; +} \ No newline at end of file From 71fe6de9cbf40446028341ddddc8e05cf6d9d9e7 Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 16 Dec 2024 21:27:32 +0100 Subject: [PATCH 06/62] feat(logs): added show/hide timestamp option --- .../dashboard/docker/logs/docker-logs-id.tsx | 25 +++++++++++++------ .../dashboard/docker/logs/terminal-line.tsx | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 34f74e2a..477f2641 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -1,4 +1,3 @@ -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -7,6 +6,7 @@ import { SelectItem, SelectTrigger, SelectValue, + SelectSeparator } from "@/components/ui/select"; import { api } from "@/utils/api"; import { @@ -23,8 +23,7 @@ interface Props { serverId?: string | null; } - -type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h"; export const priorities = [ { @@ -65,7 +64,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { const [autoScroll, setAutoScroll] = React.useState(true); const [lines, setLines] = React.useState(100); const [search, setSearch] = React.useState(""); - + const [showTimestamp, setShowTimestamp] = React.useState(true); const [since, setSince] = React.useState("all"); const [typeFilter, setTypeFilter] = React.useState([]); const scrollRef = useRef(null); @@ -96,9 +95,13 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { }; const handleSince = (value: TimeFilter) => { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); + if (value === "timestamp") { + setShowTimestamp(!showTimestamp); + } else { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); + } }; useEffect(() => { @@ -255,6 +258,10 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { Last 7 days Last 30 days All time + + + {showTimestamp ? "Hide timestamp" : "Show timestamp"} + @@ -272,12 +279,13 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { onChange={handleSearch} className="inline-flex h-9 text-sm placeholder-gray-400 w-full sm:w-auto" /> + - -
- {filteredLogs.length > 0 ? ( - filteredLogs.map((filteredLog: LogLine, index: number) => ( - - )) - ) : isLoading ? ( -
- -
- ) : ( -
- No logs found -
- )} -
- - - - ); -}; \ No newline at end of file + + +
+ {filteredLogs.length > 0 ? ( + filteredLogs.map((filteredLog: LogLine, index: number) => ( + + )) + ) : isLoading ? ( +
+ +
+ ) : ( +
+ No logs found +
+ )} +
+ + + + ); +}; diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx new file mode 100644 index 00000000..6cc8785b --- /dev/null +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -0,0 +1,121 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandGroup, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { CheckIcon } from "lucide-react"; +import React from "react"; + +type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; + +const timeRanges = [ + { + label: "All time", + value: "all", + }, + { + label: "Last hour", + value: "1h", + }, + { + label: "Last 6 hours", + value: "6h", + }, + { + label: "Last 24 hours", + value: "24h", + }, + { + label: "Last 7 days", + value: "168h", + }, + { + label: "Last 30 days", + value: "720h", + }, +]; + +interface SinceLogsFilterProps { + value: string; + onValueChange: (value: TimeFilter) => void; + showTimestamp: boolean; + onTimestampChange: (show: boolean) => void; + title?: string; +} + +export function SinceLogsFilter({ + value, + onValueChange, + showTimestamp, + onTimestampChange, + title = "Time range", +}: SinceLogsFilterProps) { + const selectedLabel = + timeRanges.find((range) => range.value === value)?.label ?? + "Select time range"; + + return ( + + + + + + + + + {timeRanges.map((range) => { + const isSelected = value === range.value; + return ( + onValueChange(range.value)} + > +
+ +
+ {range.label} +
+ ); + })} +
+
+
+ +
+ Show timestamps + +
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx index 70a6dfc8..3ef11517 100644 --- a/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/status-logs-filter.tsx @@ -1,107 +1,170 @@ -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; -import { CheckIcon } from "lucide-react"; -import React from "react"; - -interface StatusLogsFilterProps { - value?: string[]; - setValue?: (value: string[]) => void; - title?: string; - options: { - label: string; - value: string; - icon?: React.ComponentType<{ className?: string }>; - }[]; -} - -export function StatusLogsFilter({ - value = [], - setValue, - title, - options, -}: StatusLogsFilterProps) { - const selectedValues = new Set(value as string[]); - - return ( - - - - - - - - - No results found. - - {options.map((option) => { - const isSelected = selectedValues.has(option.value); - return ( - { - if (isSelected) { - selectedValues.delete(option.value); - } else { - selectedValues.add(option.value); - } - const filterValues = Array.from(selectedValues); - setValue?.(filterValues.length ? filterValues : []); - }} - > -
- -
- {option.icon && ( - - )} - {option.label} -
- ); - })} -
- -
-
-
-
- ); -} +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandGroup, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { CheckIcon } from "lucide-react"; +import type React from "react"; + +interface StatusLogsFilterProps { + value?: string[]; + setValue?: (value: string[]) => void; + title?: string; + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; +} + +export function StatusLogsFilter({ + value = [], + setValue, + title, + options, +}: StatusLogsFilterProps) { + const selectedValues = new Set(value as string[]); + const allSelected = selectedValues.size === 0; + + const getSelectedBadges = () => { + if (allSelected) { + return ( + + All + + ); + } + + if (selectedValues.size >= 1) { + const selected = options.find((opt) => selectedValues.has(opt.value)); + return ( + <> + + {selected?.label} + + {selectedValues.size > 1 && ( + + +{selectedValues.size - 1} + + )} + + ); + } + + return null; + }; + + return ( + + + + + + + + + { + setValue?.([]); // Empty array means "All" + }} + > +
+ +
+ All +
+ {options.map((option) => { + const isSelected = selectedValues.has(option.value); + return ( + { + const newValues = new Set(selectedValues); + if (isSelected) { + newValues.delete(option.value); + } else { + newValues.add(option.value); + } + setValue?.(Array.from(newValues)); + }} + > +
+ +
+ {option.icon && ( + + )} + + {option.label} + +
+ ); + })} +
+
+
+
+
+ ); +} diff --git a/apps/dokploy/components/dashboard/docker/logs/utils.ts b/apps/dokploy/components/dashboard/docker/logs/utils.ts index 409c6989..cf0b30bb 100644 --- a/apps/dokploy/components/dashboard/docker/logs/utils.ts +++ b/apps/dokploy/components/dashboard/docker/logs/utils.ts @@ -1,5 +1,5 @@ -export type LogType = "error" | "warning" | "success" | "info" | "debug"; -export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; +export type LogType = "error" | "warning" | "success" | "info" | "debug"; +export type LogVariant = "red" | "yellow" | "green" | "blue" | "orange"; export interface LogLine { rawTimestamp: string | null; @@ -138,8 +138,12 @@ export const getLogType = (message: string): LogStyle => { if ( /(?:^|\s)(?:info|inf):?\s/i.test(lowerMessage) || - /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test(lowerMessage) || - /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test(lowerMessage) + /\[(info|log|debug|trace|server|db|api|http|request|response)\]/i.test( + lowerMessage, + ) || + /\b(?:version|config|import|load|get|HTTP|PATCH|POST|debug)\b:?/i.test( + lowerMessage, + ) ) { return LOG_STYLES.debug; } From 87a5ce2053457b7114ae60c3a091e39e8a3b3ea7 Mon Sep 17 00:00:00 2001 From: 190km Date: Mon, 16 Dec 2024 22:55:36 +0100 Subject: [PATCH 08/62] fix: timestamp width --- apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx index f93b0d2f..cdbbb2c8 100644 --- a/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/terminal-line.tsx @@ -91,7 +91,7 @@ export function TerminalLine({ log, noTimestamp, searchTerm }: LogLineProps) { {/* */} {tooltip(color, rawTimestamp)} {!noTimestamp && ( - + {formattedTime} )} From bd16e03602259782df630b4f1e8230e4a873931a Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 16 Dec 2024 17:01:44 -0500 Subject: [PATCH 09/62] chore: lint --- .../dashboard/docker/logs/docker-logs-id.tsx | 16 +++++----------- .../dashboard/docker/logs/since-logs-filter.tsx | 6 +++--- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 3e490a1f..110e0faa 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -3,7 +3,7 @@ import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { Download as DownloadIcon, Loader2 } from "lucide-react"; import React, { useEffect, useRef } from "react"; -import { SinceLogsFilter } from "./since-logs-filter"; +import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter"; import { StatusLogsFilter } from "./status-logs-filter"; import { TerminalLine } from "./terminal-line"; import { type LogLine, getLogType, parseLogs } from "./utils"; @@ -13,8 +13,6 @@ interface Props { serverId?: string | null; } -type TimeFilter = "all" | "timestamp" | "1h" | "6h" | "24h" | "168h" | "720h"; - export const priorities = [ { label: "Info", @@ -84,14 +82,10 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { setLines(Number(e.target.value) || 1); }; - const handleSince = (value: string) => { - if (value === "timestamp") { - setShowTimestamp(!showTimestamp); - } else { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); - } + const handleSince = (value: TimeFilter) => { + setRawLogs(""); + setFilteredLogs([]); + setSince(value); }; useEffect(() => { diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx index 6cc8785b..09dbaff8 100644 --- a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -17,9 +17,9 @@ import { cn } from "@/lib/utils"; import { CheckIcon } from "lucide-react"; import React from "react"; -type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; +export type TimeFilter = "all" | "1h" | "6h" | "24h" | "168h" | "720h"; -const timeRanges = [ +const timeRanges: Array<{ label: string; value: TimeFilter }> = [ { label: "All time", value: "all", @@ -44,7 +44,7 @@ const timeRanges = [ label: "Last 30 days", value: "720h", }, -]; +] as const; interface SinceLogsFilterProps { value: string; From 81c85ce15536d752f8bf34759d11c1cafec6cd15 Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 16 Dec 2024 17:09:54 -0500 Subject: [PATCH 10/62] fix: don't trigger if already selected --- .../components/dashboard/docker/logs/since-logs-filter.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx index 09dbaff8..61524ee9 100644 --- a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -91,7 +91,11 @@ export function SinceLogsFilter({ return ( onValueChange(range.value)} + onSelect={() => { + if (!isSelected) { + onValueChange(range.value); + } + }} >
Date: Mon, 16 Dec 2024 17:18:11 -0500 Subject: [PATCH 11/62] chore: lint --- .../components/dashboard/docker/logs/since-logs-filter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx index 61524ee9..b7caafe7 100644 --- a/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/since-logs-filter.tsx @@ -47,7 +47,7 @@ const timeRanges: Array<{ label: string; value: TimeFilter }> = [ ] as const; interface SinceLogsFilterProps { - value: string; + value: TimeFilter; onValueChange: (value: TimeFilter) => void; showTimestamp: boolean; onTimestampChange: (show: boolean) => void; From 6db9c99080438a10049cae3106c1fd3277442b4d Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Mon, 16 Dec 2024 19:12:33 -0500 Subject: [PATCH 12/62] feat(logs): add number of lines filter --- .../dashboard/docker/logs/docker-logs-id.tsx | 19 +- .../docker/logs/line-count-filter.tsx | 173 ++++++++++++++++++ 2 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 apps/dokploy/components/dashboard/docker/logs/line-count-filter.tsx diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 110e0faa..9c3e2dda 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -3,6 +3,7 @@ import { Input } from "@/components/ui/input"; import { api } from "@/utils/api"; import { Download as DownloadIcon, Loader2 } from "lucide-react"; import React, { useEffect, useRef } from "react"; +import { LineCountFilter } from "./line-count-filter"; import { SinceLogsFilter, type TimeFilter } from "./since-logs-filter"; import { StatusLogsFilter } from "./status-logs-filter"; import { TerminalLine } from "./terminal-line"; @@ -76,16 +77,16 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => { setSearch(e.target.value || ""); }; - const handleLines = (e: React.ChangeEvent) => { + const handleLines = (lines: number) => { setRawLogs(""); setFilteredLogs([]); - setLines(Number(e.target.value) || 1); + setLines(lines); }; const handleSince = (value: TimeFilter) => { - setRawLogs(""); - setFilteredLogs([]); - setSince(value); + setRawLogs(""); + setFilteredLogs([]); + setSince(value); }; useEffect(() => { @@ -223,13 +224,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => {
- + void; + title?: string; +} + +export function LineCountFilter({ + value, + onValueChange, + title = "Limit to", +}: LineCountFilterProps) { + const [open, setOpen] = React.useState(false); + const [inputValue, setInputValue] = React.useState(""); + const pendingValueRef = useRef(null); + + const isPresetValue = lineCountOptions.some( + (option) => option.value === value, + ); + + const debouncedValueChange = useCallback( + debounce((numValue: number) => { + if (numValue > 0 && numValue !== value) { + onValueChange(numValue); + pendingValueRef.current = null; + } + }, 500), + [onValueChange, value], + ); + + const handleInputChange = (input: string) => { + setInputValue(input); + + // Extract numbers from input and convert + const numValue = Number.parseInt(input.replace(/[^0-9]/g, "")); + if (!Number.isNaN(numValue)) { + pendingValueRef.current = numValue; + debouncedValueChange(numValue); + } + }; + + const handleSelect = (selectedValue: string) => { + const preset = lineCountOptions.find((opt) => opt.label === selectedValue); + if (preset) { + if (preset.value !== value) { + onValueChange(preset.value); + } + setInputValue(""); + setOpen(false); + return; + } + + const numValue = Number.parseInt(selectedValue); + if ( + !Number.isNaN(numValue) && + numValue > 0 && + numValue !== value && + numValue !== pendingValueRef.current + ) { + onValueChange(numValue); + setInputValue(""); + setOpen(false); + } + }; + + React.useEffect(() => { + return () => { + debouncedValueChange.cancel(); + }; + }, [debouncedValueChange]); + + const displayValue = isPresetValue + ? lineCountOptions.find((option) => option.value === value)?.label + : `${value} lines`; + + return ( + + + + + + +
+ + { + if (e.key === "Enter") { + e.preventDefault(); + const numValue = Number.parseInt( + inputValue.replace(/[^0-9]/g, ""), + ); + if ( + !Number.isNaN(numValue) && + numValue > 0 && + numValue !== value && + numValue !== pendingValueRef.current + ) { + handleSelect(inputValue); + } + } + }} + /> +
+ + + {lineCountOptions.map((option) => { + const isSelected = value === option.value; + return ( + handleSelect(option.label)} + className="relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground" + > +
+ +
+ {option.label} +
+ ); + })} +
+
+
+
+
+ ); +} + +export default LineCountFilter; From 536507377d8bc7821c3c5bfdfbaa79b1d57a1c60 Mon Sep 17 00:00:00 2001 From: Mohab Gabber Date: Tue, 17 Dec 2024 17:23:26 +0200 Subject: [PATCH 13/62] Added onedev docker compose --- .../templates/onedev/docker-compose.yml | 13 +++++++++++ apps/dokploy/templates/onedev/index.ts | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 apps/dokploy/templates/onedev/docker-compose.yml create mode 100644 apps/dokploy/templates/onedev/index.ts diff --git a/apps/dokploy/templates/onedev/docker-compose.yml b/apps/dokploy/templates/onedev/docker-compose.yml new file mode 100644 index 00000000..3676e02e --- /dev/null +++ b/apps/dokploy/templates/onedev/docker-compose.yml @@ -0,0 +1,13 @@ +--- +services: + onedev: + image: 1dev/server:11.6.6 + container_name: onedev + restart: always + + volumes: + - "/var/run/docker.sock:/var/run/docker.sock" + - "onedev-data:/opt/onedev" + +volumes: + onedev-data: \ No newline at end of file diff --git a/apps/dokploy/templates/onedev/index.ts b/apps/dokploy/templates/onedev/index.ts new file mode 100644 index 00000000..5dad1728 --- /dev/null +++ b/apps/dokploy/templates/onedev/index.ts @@ -0,0 +1,22 @@ +import { + type DomainSchema, + type Schema, + type Template, + generateRandomDomain, +} from "../utils"; + +export function generate(schema: Schema): Template { + const randomDomain = generateRandomDomain(schema); + + const domains: DomainSchema[] = [ + { + host: randomDomain, + port: 6610, + serviceName: "onedev", + }, + ]; + + return { + domains, + }; +} From 4e31d8ac02f162a1a41fd1870baa2cb73e7b2ff3 Mon Sep 17 00:00:00 2001 From: Mohab Gabber Date: Tue, 17 Dec 2024 18:51:38 +0200 Subject: [PATCH 14/62] Added onedev to templates.ts and onedev's icon --- apps/dokploy/public/templates/onedev.png | Bin 0 -> 23132 bytes apps/dokploy/templates/templates.ts | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 apps/dokploy/public/templates/onedev.png diff --git a/apps/dokploy/public/templates/onedev.png b/apps/dokploy/public/templates/onedev.png new file mode 100644 index 0000000000000000000000000000000000000000..6c39e6cf699ebb3f453edd19c9c30cab14118547 GIT binary patch literal 23132 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

aUg`DKo39oSL;?Y-UP`7rrVCWjf9p=uwihy$NAI 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 literal 0 HcmV?d00001 diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index 39243356..f0d926d8 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -1136,4 +1136,19 @@ export const templates: TemplateData[] = [ tags: ["search", "analytics"], load: () => import("./elastic-search/index").then((m) => m.generate), }, + { + id: "onedev", + name: "OneDev", + version: "11.6.6", + description: + "Git server with CI/CD, kanban, and packages. Seamless integration. Unparalleled experience.", + logo: "onedev.png", + links: { + github: "https://github.com/theonedev/onedev/", + website: "https://onedev.io/", + docs: "https://docs.onedev.io/", + }, + tags: ["self-hosted", "development"], + load: () => import("./onedev/index").then((m) => m.generate), + }, ]; From b3313cf975772a83e23a68ebcb68f4f9fbe01575 Mon Sep 17 00:00:00 2001 From: usopp Date: Tue, 17 Dec 2024 19:16:40 +0100 Subject: [PATCH 15/62] style: better white style --- .../application/deployments/show-deployment.tsx | 2 +- .../compose/deployments/show-deployment-compose.tsx | 2 +- .../dashboard/docker/logs/docker-logs-id.tsx | 2 +- apps/dokploy/components/ui/badge.tsx | 10 +++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx index 96e32d8c..380b22d9 100644 --- a/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx +++ b/apps/dokploy/components/dashboard/application/deployments/show-deployment.tsx @@ -111,7 +111,7 @@ export const ShowDeployment = ({ logPath, open, onClose, serverId }: Props) => {

{ filteredLogs.length > 0 ? filteredLogs.map((log: LogLine, index: number) => ( diff --git a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx index 9c3e2dda..3f30c292 100644 --- a/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx +++ b/apps/dokploy/components/dashboard/docker/logs/docker-logs-id.tsx @@ -263,7 +263,7 @@ export const DockerLogsId: React.FC = ({ containerId, serverId }) => {
{filteredLogs.length > 0 ? ( filteredLogs.map((filteredLog: LogLine, index: number) => ( diff --git a/apps/dokploy/components/ui/badge.tsx b/apps/dokploy/components/ui/badge.tsx index 911b0071..9c41234d 100644 --- a/apps/dokploy/components/ui/badge.tsx +++ b/apps/dokploy/components/ui/badge.tsx @@ -14,14 +14,14 @@ const badgeVariants = cva( "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - red: "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md", + red: "border-transparent select-none items-center whitespace-nowrap font-medium bg-red-600/20 dark:bg-red-500/15 text-destructive text-xs h-4 px-1 py-1 rounded-md", yellow: - "border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-500/15 text-yellow-500 text-xs h-4 px-1 py-1 rounded-md", + "border-transparent select-none items-center whitespace-nowrap font-medium bg-yellow-600/20 dark:bg-yellow-500/15 dark:text-yellow-500 text-yellow-600 text-xs h-4 px-1 py-1 rounded-md", orange: - "border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-500/15 text-orange-500 text-xs h-4 px-1 py-1 rounded-md", + "border-transparent select-none items-center whitespace-nowrap font-medium bg-orange-600/20 dark:bg-orange-500/15 dark:text-orange-500 text-orange-600 text-xs h-4 px-1 py-1 rounded-md", green: - "border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-500/15 text-emerald-500 text-xs h-4 px-1 py-1 rounded-md", - blue: "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-500/15 text-blue-500 text-xs h-4 px-1 py-1 rounded-md", + "border-transparent select-none items-center whitespace-nowrap font-medium bg-emerald-600/20 dark:bg-emerald-500/15 dark:text-emerald-500 text-emerald-600 text-xs h-4 px-1 py-1 rounded-md", + blue: "border-transparent select-none items-center whitespace-nowrap font-medium bg-blue-600/20 dark:bg-blue-500/15 dark:text-blue-500 text-blue-600 text-xs h-4 px-1 py-1 rounded-md", blank: "border-transparent select-none items-center whitespace-nowrap font-medium dark:bg-white/15 bg-black/15 text-foreground text-xs h-4 px-1 py-1 rounded-md", outline: "text-foreground", From fe2de6b89959117a750aede8e1c7111c8347fc3c Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Tue, 17 Dec 2024 22:03:58 -0500 Subject: [PATCH 16/62] feat(discord): remove dots --- .../src/utils/notifications/build-error.ts | 20 +++++++++---------- .../src/utils/notifications/build-success.ts | 18 ++++++++--------- .../utils/notifications/database-backup.ts | 20 +++++++++---------- .../src/utils/notifications/docker-cleanup.ts | 12 +++++------ .../utils/notifications/dokploy-restart.ts | 10 +++++----- 5 files changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/server/src/utils/notifications/build-error.ts b/packages/server/src/utils/notifications/build-error.ts index 5406698d..f9286fc7 100644 --- a/packages/server/src/utils/notifications/build-error.ts +++ b/packages/server/src/utils/notifications/build-error.ts @@ -28,7 +28,7 @@ export const sendBuildErrorNotifications = async ({ adminId, }: Props) => { const date = new Date(); - const unixDate = ~~((Number(date)) / 1000); + const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.appBuildError, true), @@ -60,45 +60,45 @@ export const sendBuildErrorNotifications = async ({ if (discord) { await sendDiscordNotification(discord, { - title: "> `⚠️` - Build Failed", + title: "> `⚠️` Build Failed", color: 0xed4245, fields: [ { - name: "`🛠️`・Project", + name: "`🛠️` Project", value: projectName, inline: true, }, { - name: "`⚙️`・Application", + name: "`⚙️` Application", value: applicationName, inline: true, }, { - name: "`❔`・Type", + name: "`❔` Type", value: applicationType, inline: true, }, { - name: "`📅`・Date", + name: "`📅` Date", value: ``, inline: true, }, { - name: "`⌚`・Time", + name: "`⌚` Time", value: ``, inline: true, }, { - name: "`❓`・Type", + name: "`❓`Type", value: "Failed", inline: true, }, { - name: "`⚠️`・Error Message", + name: "`⚠️` Error Message", value: `\`\`\`${errorMessage}\`\`\``, }, { - name: "`🧷`・Build Link", + name: "`🧷` Build Link", value: `[Click here to access build link](${buildLink})`, }, ], diff --git a/packages/server/src/utils/notifications/build-success.ts b/packages/server/src/utils/notifications/build-success.ts index 1c16b10d..97fe7e1c 100644 --- a/packages/server/src/utils/notifications/build-success.ts +++ b/packages/server/src/utils/notifications/build-success.ts @@ -26,7 +26,7 @@ export const sendBuildSuccessNotifications = async ({ adminId, }: Props) => { const date = new Date(); - const unixDate = ~~((Number(date)) / 1000); + const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.appDeploy, true), @@ -58,41 +58,41 @@ export const sendBuildSuccessNotifications = async ({ if (discord) { await sendDiscordNotification(discord, { - title: "> `✅` - Build Success", + title: "> `✅` Build Success", color: 0x57f287, fields: [ { - name: "`🛠️`・Project", + name: "`🛠️` Project", value: projectName, inline: true, }, { - name: "`⚙️`・Application", + name: "`⚙️` Application", value: applicationName, inline: true, }, { - name: "`❔`・Application Type", + name: "`❔` Application Type", value: applicationType, inline: true, }, { - name: "`📅`・Date", + name: "`📅` Date", value: ``, inline: true, }, { - name: "`⌚`・Time", + name: "`⌚` Time", value: ``, inline: true, }, { - name: "`❓`・Type", + name: "`❓` Type", value: "Successful", inline: true, }, { - name: "`🧷`・Build Link", + name: "`🧷` Build Link", value: `[Click here to access build link](${buildLink})`, }, ], diff --git a/packages/server/src/utils/notifications/database-backup.ts b/packages/server/src/utils/notifications/database-backup.ts index 6d3dcb28..d2dccdaf 100644 --- a/packages/server/src/utils/notifications/database-backup.ts +++ b/packages/server/src/utils/notifications/database-backup.ts @@ -26,7 +26,7 @@ export const sendDatabaseBackupNotifications = async ({ errorMessage?: string; }) => { const date = new Date(); - const unixDate = ~~((Number(date)) / 1000); + const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.databaseBackup, true), @@ -65,37 +65,37 @@ export const sendDatabaseBackupNotifications = async ({ await sendDiscordNotification(discord, { title: type === "success" - ? "> `✅` - Database Backup Successful" - : "> `❌` - Database Backup Failed", + ? "> `✅` Database Backup Successful" + : "> `❌` Database Backup Failed", color: type === "success" ? 0x57f287 : 0xed4245, fields: [ { - name: "`🛠️`・Project", + name: "`🛠️` Project", value: projectName, inline: true, }, { - name: "`⚙️`・Application", + name: "`⚙️` Application", value: applicationName, inline: true, }, { - name: "`❔`・Database", + name: "`❔` Database", value: databaseType, inline: true, }, { - name: "`📅`・Date", + name: "`📅` Date", value: ``, inline: true, }, { - name: "`⌚`・Time", + name: "`⌚` Time", value: ``, inline: true, }, { - name: "`❓`・Type", + name: "`❓` Type", value: type .replace("error", "Failed") .replace("success", "Successful"), @@ -104,7 +104,7 @@ export const sendDatabaseBackupNotifications = async ({ ...(type === "error" && errorMessage ? [ { - name: "`⚠️`・Error Message", + name: "`⚠️` Error Message", value: `\`\`\`${errorMessage}\`\`\``, }, ] diff --git a/packages/server/src/utils/notifications/docker-cleanup.ts b/packages/server/src/utils/notifications/docker-cleanup.ts index 7a836329..515d1ddc 100644 --- a/packages/server/src/utils/notifications/docker-cleanup.ts +++ b/packages/server/src/utils/notifications/docker-cleanup.ts @@ -15,7 +15,7 @@ export const sendDockerCleanupNotifications = async ( message = "Docker cleanup for dokploy", ) => { const date = new Date(); - const unixDate = ~~((Number(date)) / 1000); + const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ where: and( eq(notifications.dockerCleanup, true), @@ -46,26 +46,26 @@ export const sendDockerCleanupNotifications = async ( if (discord) { await sendDiscordNotification(discord, { - title: "> `✅` - Docker Cleanup", + title: "> `✅` Docker Cleanup", color: 0x57f287, fields: [ { - name: "`📅`・Date", + name: "`📅` Date", value: ``, inline: true, }, { - name: "`⌚`・Time", + name: "`⌚` Time", value: ``, inline: true, }, { - name: "`❓`・Type", + name: "`❓` Type", value: "Successful", inline: true, }, { - name: "`📜`・Message", + name: "`📜` Message", value: `\`\`\`${message}\`\`\``, }, ], diff --git a/packages/server/src/utils/notifications/dokploy-restart.ts b/packages/server/src/utils/notifications/dokploy-restart.ts index 86cd6f03..883a3d1f 100644 --- a/packages/server/src/utils/notifications/dokploy-restart.ts +++ b/packages/server/src/utils/notifications/dokploy-restart.ts @@ -12,7 +12,7 @@ import { export const sendDokployRestartNotifications = async () => { const date = new Date(); - const unixDate = ~~((Number(date)) / 1000); + const unixDate = ~~(Number(date) / 1000); const notificationList = await db.query.notifications.findMany({ where: eq(notifications.dokployRestart, true), with: { @@ -35,21 +35,21 @@ export const sendDokployRestartNotifications = async () => { if (discord) { await sendDiscordNotification(discord, { - title: "> `✅` - Dokploy Server Restarted", + title: "> `✅` Dokploy Server Restarted", color: 0x57f287, fields: [ { - name: "`📅`・Date", + name: "`📅` Date", value: ``, inline: true, }, { - name: "`⌚`・Time", + name: "`⌚` Time", value: ``, inline: true, }, { - name: "`❓`・Type", + name: "`❓` Type", value: "Successful", inline: true, }, From a941efb1ffb9e3137cf4a36071c8876d9e0911ba Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Tue, 17 Dec 2024 23:10:19 -0500 Subject: [PATCH 17/62] feat(certs): show expiration and chain details --- .../certificates/show-certificates.tsx | 195 ++++++++++++++++-- 1 file changed, 179 insertions(+), 16 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx index 69b1a332..ccc16f3e 100644 --- a/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx +++ b/apps/dokploy/components/dashboard/settings/certificates/show-certificates.tsx @@ -6,13 +6,144 @@ import { CardTitle, } from "@/components/ui/card"; import { api } from "@/utils/api"; -import { ShieldCheck } from "lucide-react"; +import { AlertCircle, Link, ShieldCheck } from "lucide-react"; import { AddCertificate } from "./add-certificate"; import { DeleteCertificate } from "./delete-certificate"; export const ShowCertificates = () => { const { data } = api.certificates.all.useQuery(); + const extractExpirationDate = (certData: string): Date | null => { + try { + const match = certData.match( + /-----BEGIN CERTIFICATE-----\s*([^-]+)\s*-----END CERTIFICATE-----/, + ); + if (!match?.[1]) return null; + + const base64Cert = match[1].replace(/\s/g, ""); + const binaryStr = window.atob(base64Cert); + const bytes = new Uint8Array(binaryStr.length); + + for (let i = 0; i < binaryStr.length; i++) { + bytes[i] = binaryStr.charCodeAt(i); + } + + let dateFound = 0; + for (let i = 0; i < bytes.length - 2; i++) { + if (bytes[i] === 0x17 || bytes[i] === 0x18) { + const dateType = bytes[i]; + const dateLength = bytes[i + 1]; + if (typeof dateLength === "undefined") continue; + + if (dateFound === 0) { + dateFound++; + i += dateLength + 1; + continue; + } + + let dateStr = ""; + for (let j = 0; j < dateLength; j++) { + const charCode = bytes[i + 2 + j]; + if (typeof charCode === "undefined") continue; + dateStr += String.fromCharCode(charCode); + } + + if (dateType === 0x17) { + // UTCTime (YYMMDDhhmmssZ) + const year = Number.parseInt(dateStr.slice(0, 2)); + const fullYear = year >= 50 ? 1900 + year : 2000 + year; + return new Date( + Date.UTC( + fullYear, + Number.parseInt(dateStr.slice(2, 4)) - 1, + Number.parseInt(dateStr.slice(4, 6)), + Number.parseInt(dateStr.slice(6, 8)), + Number.parseInt(dateStr.slice(8, 10)), + Number.parseInt(dateStr.slice(10, 12)), + ), + ); + } + + // GeneralizedTime (YYYYMMDDhhmmssZ) + return new Date( + Date.UTC( + Number.parseInt(dateStr.slice(0, 4)), + Number.parseInt(dateStr.slice(4, 6)) - 1, + Number.parseInt(dateStr.slice(6, 8)), + Number.parseInt(dateStr.slice(8, 10)), + Number.parseInt(dateStr.slice(10, 12)), + Number.parseInt(dateStr.slice(12, 14)), + ), + ); + } + } + return null; + } catch (error) { + console.error("Error parsing certificate:", error); + return null; + } + }; + + const getExpirationStatus = (certData: string) => { + const expirationDate = extractExpirationDate(certData); + + if (!expirationDate) + return { + status: "unknown" as const, + className: "text-muted-foreground", + message: "Could not determine expiration", + }; + + const now = new Date(); + const daysUntilExpiration = Math.ceil( + (expirationDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + + if (daysUntilExpiration < 0) { + return { + status: "expired" as const, + className: "text-red-500", + message: `Expired on ${expirationDate.toLocaleDateString([], { + year: "numeric", + month: "long", + day: "numeric", + })}`, + }; + } + + if (daysUntilExpiration <= 30) { + return { + status: "warning" as const, + className: "text-yellow-500", + message: `Expires in ${daysUntilExpiration} days`, + }; + } + + return { + status: "valid" as const, + className: "text-muted-foreground", + message: `Expires ${expirationDate.toLocaleDateString([], { + year: "numeric", + month: "long", + day: "numeric", + })}`, + }; + }; + + const getCertificateChainInfo = (certData: string) => { + const certCount = (certData.match(/-----BEGIN CERTIFICATE-----/g) || []) + .length; + return certCount > 1 + ? { + isChain: true, + count: certCount, + } + : { + isChain: false, + count: 1, + }; + }; + return (
@@ -23,7 +154,7 @@ export const ShowCertificates = () => { - {data?.length === 0 ? ( + {!data?.length ? (
@@ -35,21 +166,53 @@ export const ShowCertificates = () => { ) : (
- {data?.map((destination, index) => ( -
- - {index + 1}. {destination.name} - -
- + {data.map((certificate, index) => { + const expiration = getExpirationStatus( + certificate.certificateData, + ); + const chainInfo = getCertificateChainInfo( + certificate.certificateData, + ); + return ( +
+
+
+ + {index + 1}. {certificate.name} + + {chainInfo.isChain && ( +
+ + + Chain ({chainInfo.count}) + +
+ )} +
+ +
+
+ {expiration.status !== "valid" && ( + + )} + {expiration.message} + {certificate.autoRenew && + expiration.status !== "valid" && ( + + (Auto-renewal enabled) + + )} +
-
- ))} + ); + })}
From 5c8eda2405dada60624db3b292be3c8d1ca7dc39 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:47:48 -0600 Subject: [PATCH 18/62] Update apps/dokploy/templates/onedev/docker-compose.yml --- apps/dokploy/templates/onedev/docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/templates/onedev/docker-compose.yml b/apps/dokploy/templates/onedev/docker-compose.yml index 3676e02e..af4122cf 100644 --- a/apps/dokploy/templates/onedev/docker-compose.yml +++ b/apps/dokploy/templates/onedev/docker-compose.yml @@ -2,7 +2,6 @@ services: onedev: image: 1dev/server:11.6.6 - container_name: onedev restart: always volumes: From 1dece58cffc8818a2ff409337fb0389a09d954ce Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 18 Dec 2024 13:56:09 -0500 Subject: [PATCH 19/62] fix(term): fix light mode foreground color closes #907 --- .../components/dashboard/docker/terminal/docker-terminal.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx index 42683887..bf14680a 100644 --- a/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx +++ b/apps/dokploy/components/dashboard/docker/terminal/docker-terminal.tsx @@ -4,6 +4,7 @@ import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AttachAddon } from "@xterm/addon-attach"; +import { useTheme } from "next-themes"; interface Props { id: string; @@ -18,6 +19,7 @@ export const DockerTerminal: React.FC = ({ }) => { const termRef = useRef(null); const [activeWay, setActiveWay] = React.useState("bash"); + const { resolvedTheme } = useTheme(); useEffect(() => { const container = document.getElementById(id); if (container) { @@ -28,8 +30,9 @@ export const DockerTerminal: React.FC = ({ lineHeight: 1.4, convertEol: true, theme: { - cursor: "transparent", + cursor: resolvedTheme === "light" ? "#000000" : "transparent", background: "rgba(0, 0, 0, 0)", + foreground: "currentColor", }, }); const addonFit = new FitAddon(); From 0b5108848905b730ab2cd9b6ab4d9e36e275b947 Mon Sep 17 00:00:00 2001 From: 190km Date: Thu, 19 Dec 2024 02:10:07 +0100 Subject: [PATCH 20/62] fix: typo - Setup -> Login --- apps/dokploy/components/auth/login-2fa.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/auth/login-2fa.tsx b/apps/dokploy/components/auth/login-2fa.tsx index 7c4915fa..dcb004f1 100644 --- a/apps/dokploy/components/auth/login-2fa.tsx +++ b/apps/dokploy/components/auth/login-2fa.tsx @@ -87,7 +87,7 @@ export const Login2FA = ({ authId }: Props) => {
)} - 2FA Setup + 2FA Login Date: Wed, 18 Dec 2024 17:11:48 -0500 Subject: [PATCH 21/62] fix(docker): fix for custom registry login --- packages/server/src/utils/cluster/upload.ts | 19 +++++++++++-------- packages/server/src/utils/providers/docker.ts | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index 22c9881c..0ecbda4f 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -19,21 +19,25 @@ export const uploadImage = async ( const finalURL = registryUrl; - const registryTag = join(imagePrefix || "", imageName); + const registryTag = + `${registryUrl}/${join(imagePrefix || "", imageName)}`.replace(/\/+/g, "/"); try { writeStream.write( - `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${registryTag} | ${finalURL}\n`, + `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`, ); - await spawnAsync( + const loginCommand = spawnAsync( "docker", - ["login", finalURL, "-u", registry.username, "-p", registry.password], + ["login", finalURL, "-u", registry.username, "--password-stdin"], (data) => { if (writeStream.writable) { writeStream.write(data); } }, ); + loginCommand.child?.stdin?.write(registry.password); + loginCommand.child?.stdin?.end(); + await loginCommand; await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { if (writeStream.writable) { @@ -73,17 +77,16 @@ export const uploadImageRemoteCommand = ( try { const command = ` echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath}; - docker login ${finalURL} -u ${registry.username} -p ${registry.password} >> ${logPath} 2>> ${logPath} || { + echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || { echo "❌ DockerHub Failed" >> ${logPath}; exit 1; } - echo "✅ DockerHub Login Success" >> ${logPath}; + echo "✅ Registry Login Success" >> ${logPath}; docker tag ${imageName} ${registryTag} >> ${logPath} 2>> ${logPath} || { echo "❌ Error tagging image" >> ${logPath}; exit 1; } - echo "✅ Image Tagged" >> ${logPath}; - + echo "✅ Image Tagged" >> ${logPath}; docker push ${registryTag} 2>> ${logPath} || { echo "❌ Error pushing image" >> ${logPath}; exit 1; diff --git a/packages/server/src/utils/providers/docker.ts b/packages/server/src/utils/providers/docker.ts index 7245dc51..88c45776 100644 --- a/packages/server/src/utils/providers/docker.ts +++ b/packages/server/src/utils/providers/docker.ts @@ -53,7 +53,7 @@ export const buildRemoteDocker = async ( application: ApplicationNested, logPath: string, ) => { - const { sourceType, dockerImage, username, password } = application; + const { registryUrl, dockerImage, username, password } = application; try { if (!dockerImage) { @@ -65,7 +65,7 @@ echo "Pulling ${dockerImage}" >> ${logPath}; if (username && password) { command += ` -if ! docker login --username ${username} --password ${password} https://index.docker.io/v1/ >> ${logPath} 2>&1; then +if ! echo "${password}" | docker login --username "${username}" --password-stdin "${registryUrl || ""}" >> ${logPath} 2>&1; then echo "❌ Login failed" >> ${logPath}; exit 1; fi From b39c0ef9154999848986f1608471f748814dac5c Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Wed, 18 Dec 2024 23:00:50 -0500 Subject: [PATCH 22/62] fix(preview-deployments): wrap long envs --- apps/dokploy/components/ui/secrets.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/ui/secrets.tsx b/apps/dokploy/components/ui/secrets.tsx index 5669b051..1b06b86c 100644 --- a/apps/dokploy/components/ui/secrets.tsx +++ b/apps/dokploy/components/ui/secrets.tsx @@ -62,6 +62,7 @@ export const Secrets = (props: Props) => { } language="properties" disabled={isVisible} + lineWrapping placeholder={props.placeholder} className="h-96 font-mono" {...field} From c51b5021167c211db60643064a0701d2180ea219 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:05:30 -0600 Subject: [PATCH 23/62] refactor: add path join to prevent concatenate double slash and update the getImageName --- packages/server/src/utils/builders/index.ts | 374 ++++++++++---------- packages/server/src/utils/cluster/upload.ts | 124 +++---- 2 files changed, 250 insertions(+), 248 deletions(-) diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index 1cdc9787..fe2ca0eb 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -4,12 +4,12 @@ import type { InferResultType } from "@dokploy/server/types/with"; import type { CreateServiceOptions } from "dockerode"; import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; import { - calculateResources, - generateBindMounts, - generateConfigContainer, - generateFileMounts, - generateVolumeMounts, - prepareEnvironmentVariables, + calculateResources, + generateBindMounts, + generateConfigContainer, + generateFileMounts, + generateVolumeMounts, + prepareEnvironmentVariables, } from "../docker/utils"; import { getRemoteDocker } from "../servers/remote-docker"; import { buildCustomDocker, getDockerCommand } from "./docker-file"; @@ -24,217 +24,217 @@ import { nanoid } from "nanoid"; // PAKETO codeDirectory = where is the path of the code directory // DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile) export type ApplicationNested = InferResultType< - "applications", - { - mounts: true; - security: true; - redirects: true; - ports: true; - registry: true; - project: true; - } + "applications", + { + mounts: true; + security: true; + redirects: true; + ports: true; + registry: true; + project: true; + } >; export const buildApplication = async ( - application: ApplicationNested, - logPath: string, + application: ApplicationNested, + logPath: string ) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { buildType, sourceType } = application; - try { - writeStream.write( - `\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`, - ); - console.log(`Build ${buildType}: ✅`); - if (buildType === "nixpacks") { - await buildNixpacks(application, writeStream); - } else if (buildType === "heroku_buildpacks") { - await buildHeroku(application, writeStream); - } else if (buildType === "paketo_buildpacks") { - await buildPaketo(application, writeStream); - } else if (buildType === "dockerfile") { - await buildCustomDocker(application, writeStream); - } else if (buildType === "static") { - await buildStatic(application, writeStream); - } + const writeStream = createWriteStream(logPath, { flags: "a" }); + const { buildType, sourceType } = application; + try { + writeStream.write( + `\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n` + ); + console.log(`Build ${buildType}: ✅`); + if (buildType === "nixpacks") { + await buildNixpacks(application, writeStream); + } else if (buildType === "heroku_buildpacks") { + await buildHeroku(application, writeStream); + } else if (buildType === "paketo_buildpacks") { + await buildPaketo(application, writeStream); + } else if (buildType === "dockerfile") { + await buildCustomDocker(application, writeStream); + } else if (buildType === "static") { + await buildStatic(application, writeStream); + } - if (application.registryId) { - await uploadImage(application, writeStream); - } - await mechanizeDockerContainer(application); - writeStream.write("Docker Deployed: ✅"); - } catch (error) { - if (error instanceof Error) { - writeStream.write(`Error ❌\n${error?.message}`); - } else { - writeStream.write("Error ❌"); - } - throw error; - } finally { - writeStream.end(); - } + if (application.registryId) { + await uploadImage(application, writeStream); + } + await mechanizeDockerContainer(application); + writeStream.write("Docker Deployed: ✅"); + } catch (error) { + if (error instanceof Error) { + writeStream.write(`Error ❌\n${error?.message}`); + } else { + writeStream.write("Error ❌"); + } + throw error; + } finally { + writeStream.end(); + } }; export const getBuildCommand = ( - application: ApplicationNested, - logPath: string, + application: ApplicationNested, + logPath: string ) => { - let command = ""; - const { buildType, registry } = application; - switch (buildType) { - case "nixpacks": - command = getNixpacksCommand(application, logPath); - break; - case "heroku_buildpacks": - command = getHerokuCommand(application, logPath); - break; - case "paketo_buildpacks": - command = getPaketoCommand(application, logPath); - break; - case "static": - command = getStaticCommand(application, logPath); - break; - case "dockerfile": - command = getDockerCommand(application, logPath); - break; - } - if (registry) { - command += uploadImageRemoteCommand(application, logPath); - } + let command = ""; + const { buildType, registry } = application; + switch (buildType) { + case "nixpacks": + command = getNixpacksCommand(application, logPath); + break; + case "heroku_buildpacks": + command = getHerokuCommand(application, logPath); + break; + case "paketo_buildpacks": + command = getPaketoCommand(application, logPath); + break; + case "static": + command = getStaticCommand(application, logPath); + break; + case "dockerfile": + command = getDockerCommand(application, logPath); + break; + } + if (registry) { + command += uploadImageRemoteCommand(application, logPath); + } - return command; + return command; }; export const mechanizeDockerContainer = async ( - application: ApplicationNested, + application: ApplicationNested ) => { - const { - appName, - env, - mounts, - cpuLimit, - memoryLimit, - memoryReservation, - cpuReservation, - command, - ports, - } = application; + const { + appName, + env, + mounts, + cpuLimit, + memoryLimit, + memoryReservation, + cpuReservation, + command, + ports, + } = application; - const resources = calculateResources({ - memoryLimit, - memoryReservation, - cpuLimit, - cpuReservation, - }); + const resources = calculateResources({ + memoryLimit, + memoryReservation, + cpuLimit, + cpuReservation, + }); - const volumesMount = generateVolumeMounts(mounts); + const volumesMount = generateVolumeMounts(mounts); - const { - HealthCheck, - RestartPolicy, - Placement, - Labels, - Mode, - RollbackConfig, - UpdateConfig, - Networks, - } = generateConfigContainer(application); + const { + HealthCheck, + RestartPolicy, + Placement, + Labels, + Mode, + RollbackConfig, + UpdateConfig, + Networks, + } = generateConfigContainer(application); - const bindsMount = generateBindMounts(mounts); - const filesMount = generateFileMounts(appName, application); - const envVariables = prepareEnvironmentVariables( - env, - application.project.env, - ); + const bindsMount = generateBindMounts(mounts); + const filesMount = generateFileMounts(appName, application); + const envVariables = prepareEnvironmentVariables( + env, + application.project.env + ); - const image = getImageName(application); - const authConfig = getAuthConfig(application); - const docker = await getRemoteDocker(application.serverId); + const image = getImageName(application); + const authConfig = getAuthConfig(application); + const docker = await getRemoteDocker(application.serverId); - const settings: CreateServiceOptions = { - authconfig: authConfig, - Name: appName, - TaskTemplate: { - ContainerSpec: { - HealthCheck, - Image: image, - Env: envVariables, - Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(command - ? { - Command: ["/bin/sh"], - Args: ["-c", command], - } - : {}), - Labels, - }, - Networks, - RestartPolicy, - Placement, - Resources: { - ...resources, - }, - }, - Mode, - RollbackConfig, - EndpointSpec: { - Ports: ports.map((port) => ({ - Protocol: port.protocol, - TargetPort: port.targetPort, - PublishedPort: port.publishedPort, - })), - }, - UpdateConfig, - }; + const settings: CreateServiceOptions = { + authconfig: authConfig, + Name: appName, + TaskTemplate: { + ContainerSpec: { + HealthCheck, + Image: image, + Env: envVariables, + Mounts: [...volumesMount, ...bindsMount, ...filesMount], + ...(command + ? { + Command: ["/bin/sh"], + Args: ["-c", command], + } + : {}), + Labels, + }, + Networks, + RestartPolicy, + Placement, + Resources: { + ...resources, + }, + }, + Mode, + RollbackConfig, + EndpointSpec: { + Ports: ports.map((port) => ({ + Protocol: port.protocol, + TargetPort: port.targetPort, + PublishedPort: port.publishedPort, + })), + }, + UpdateConfig, + }; - try { - const service = docker.getService(appName); - const inspect = await service.inspect(); - await service.update({ - version: Number.parseInt(inspect.Version.Index), - ...settings, - TaskTemplate: { - ...settings.TaskTemplate, - ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, - }, - }); - } catch (error) { - await docker.createService(settings); - } + try { + const service = docker.getService(appName); + const inspect = await service.inspect(); + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...settings, + TaskTemplate: { + ...settings.TaskTemplate, + ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, + }, + }); + } catch (error) { + await docker.createService(settings); + } }; const getImageName = (application: ApplicationNested) => { - const { appName, sourceType, dockerImage, registry } = application; + const { appName, sourceType, dockerImage, registry } = application; - if (sourceType === "docker") { - return dockerImage || "ERROR-NO-IMAGE-PROVIDED"; - } + if (sourceType === "docker") { + return dockerImage || "ERROR-NO-IMAGE-PROVIDED"; + } - if (registry) { - return join(registry.imagePrefix || "", appName); - } + if (registry) { + return join(registry.registryUrl, registry.imagePrefix || "", appName); + } - return `${appName}:latest`; + return `${appName}:latest`; }; const getAuthConfig = (application: ApplicationNested) => { - const { registry, username, password, sourceType } = application; + const { registry, username, password, sourceType, registryUrl } = application; - if (sourceType === "docker") { - if (username && password) { - return { - password, - username, - serveraddress: "https://index.docker.io/v1/", - }; - } - } else if (registry) { - return { - password: registry.password, - username: registry.username, - serveraddress: registry.registryUrl, - }; - } + if (sourceType === "docker") { + if (username && password) { + return { + password, + username, + serveraddress: registryUrl || "", + }; + } + } else if (registry) { + return { + password: registry.password, + username: registry.username, + serveraddress: registry.registryUrl, + }; + } - return undefined; + return undefined; }; diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index 0ecbda4f..e4143bb0 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -1,81 +1,84 @@ import type { WriteStream } from "node:fs"; -import { join } from "node:path"; +import path, { join } from "node:path"; import type { ApplicationNested } from "../builders"; import { spawnAsync } from "../process/spawnAsync"; export const uploadImage = async ( - application: ApplicationNested, - writeStream: WriteStream, + application: ApplicationNested, + writeStream: WriteStream ) => { - const registry = application.registry; + const registry = application.registry; - if (!registry) { - throw new Error("Registry not found"); - } + if (!registry) { + throw new Error("Registry not found"); + } - const { registryUrl, imagePrefix, registryType } = registry; - const { appName } = application; - const imageName = `${appName}:latest`; + const { registryUrl, imagePrefix } = registry; + const { appName } = application; + const imageName = `${appName}:latest`; - const finalURL = registryUrl; + const finalURL = registryUrl; - const registryTag = - `${registryUrl}/${join(imagePrefix || "", imageName)}`.replace(/\/+/g, "/"); + const registryTag = path + .join(registryUrl, join(imagePrefix || "", imageName)) + .replace(/\/+/g, "/"); - try { - writeStream.write( - `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`, - ); - const loginCommand = spawnAsync( - "docker", - ["login", finalURL, "-u", registry.username, "--password-stdin"], - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }, - ); - loginCommand.child?.stdin?.write(registry.password); - loginCommand.child?.stdin?.end(); - await loginCommand; + try { + writeStream.write( + `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n` + ); + const loginCommand = spawnAsync( + "docker", + ["login", finalURL, "-u", registry.username, "--password-stdin"], + (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + } + ); + loginCommand.child?.stdin?.write(registry.password); + loginCommand.child?.stdin?.end(); + await loginCommand; - await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); + await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + }); - await spawnAsync("docker", ["push", registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - } catch (error) { - console.log(error); - throw error; - } + await spawnAsync("docker", ["push", registryTag], (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + }); + } catch (error) { + console.log(error); + throw error; + } }; export const uploadImageRemoteCommand = ( - application: ApplicationNested, - logPath: string, + application: ApplicationNested, + logPath: string ) => { - const registry = application.registry; + const registry = application.registry; - if (!registry) { - throw new Error("Registry not found"); - } + if (!registry) { + throw new Error("Registry not found"); + } - const { registryUrl, imagePrefix } = registry; - const { appName } = application; - const imageName = `${appName}:latest`; + const { registryUrl, imagePrefix } = registry; + const { appName } = application; + const imageName = `${appName}:latest`; - const finalURL = registryUrl; + const finalURL = registryUrl; - const registryTag = join(imagePrefix || "", imageName); + const registryTag = path + .join(registryUrl, join(imagePrefix || "", imageName)) + .replace(/\/+/g, "/"); - try { - const command = ` + try { + const command = ` echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath}; echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || { echo "❌ DockerHub Failed" >> ${logPath}; @@ -93,9 +96,8 @@ export const uploadImageRemoteCommand = ( } echo "✅ Image Pushed" >> ${logPath}; `; - return command; - } catch (error) { - console.log(error); - throw error; - } + return command; + } catch (error) { + throw error; + } }; From 1ae96297e8949332328e716f1aa7169142a669a3 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:07:08 -0600 Subject: [PATCH 24/62] refactor: update lint --- packages/server/src/utils/builders/index.ts | 374 ++++++++++---------- packages/server/src/utils/cluster/upload.ts | 124 +++---- 2 files changed, 249 insertions(+), 249 deletions(-) diff --git a/packages/server/src/utils/builders/index.ts b/packages/server/src/utils/builders/index.ts index fe2ca0eb..ce10413a 100644 --- a/packages/server/src/utils/builders/index.ts +++ b/packages/server/src/utils/builders/index.ts @@ -4,12 +4,12 @@ import type { InferResultType } from "@dokploy/server/types/with"; import type { CreateServiceOptions } from "dockerode"; import { uploadImage, uploadImageRemoteCommand } from "../cluster/upload"; import { - calculateResources, - generateBindMounts, - generateConfigContainer, - generateFileMounts, - generateVolumeMounts, - prepareEnvironmentVariables, + calculateResources, + generateBindMounts, + generateConfigContainer, + generateFileMounts, + generateVolumeMounts, + prepareEnvironmentVariables, } from "../docker/utils"; import { getRemoteDocker } from "../servers/remote-docker"; import { buildCustomDocker, getDockerCommand } from "./docker-file"; @@ -24,217 +24,217 @@ import { nanoid } from "nanoid"; // PAKETO codeDirectory = where is the path of the code directory // DOCKERFILE codeDirectory = where is the exact path of the (Dockerfile) export type ApplicationNested = InferResultType< - "applications", - { - mounts: true; - security: true; - redirects: true; - ports: true; - registry: true; - project: true; - } + "applications", + { + mounts: true; + security: true; + redirects: true; + ports: true; + registry: true; + project: true; + } >; export const buildApplication = async ( - application: ApplicationNested, - logPath: string + application: ApplicationNested, + logPath: string, ) => { - const writeStream = createWriteStream(logPath, { flags: "a" }); - const { buildType, sourceType } = application; - try { - writeStream.write( - `\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n` - ); - console.log(`Build ${buildType}: ✅`); - if (buildType === "nixpacks") { - await buildNixpacks(application, writeStream); - } else if (buildType === "heroku_buildpacks") { - await buildHeroku(application, writeStream); - } else if (buildType === "paketo_buildpacks") { - await buildPaketo(application, writeStream); - } else if (buildType === "dockerfile") { - await buildCustomDocker(application, writeStream); - } else if (buildType === "static") { - await buildStatic(application, writeStream); - } + const writeStream = createWriteStream(logPath, { flags: "a" }); + const { buildType, sourceType } = application; + try { + writeStream.write( + `\nBuild ${buildType}: ✅\nSource Type: ${sourceType}: ✅\n`, + ); + console.log(`Build ${buildType}: ✅`); + if (buildType === "nixpacks") { + await buildNixpacks(application, writeStream); + } else if (buildType === "heroku_buildpacks") { + await buildHeroku(application, writeStream); + } else if (buildType === "paketo_buildpacks") { + await buildPaketo(application, writeStream); + } else if (buildType === "dockerfile") { + await buildCustomDocker(application, writeStream); + } else if (buildType === "static") { + await buildStatic(application, writeStream); + } - if (application.registryId) { - await uploadImage(application, writeStream); - } - await mechanizeDockerContainer(application); - writeStream.write("Docker Deployed: ✅"); - } catch (error) { - if (error instanceof Error) { - writeStream.write(`Error ❌\n${error?.message}`); - } else { - writeStream.write("Error ❌"); - } - throw error; - } finally { - writeStream.end(); - } + if (application.registryId) { + await uploadImage(application, writeStream); + } + await mechanizeDockerContainer(application); + writeStream.write("Docker Deployed: ✅"); + } catch (error) { + if (error instanceof Error) { + writeStream.write(`Error ❌\n${error?.message}`); + } else { + writeStream.write("Error ❌"); + } + throw error; + } finally { + writeStream.end(); + } }; export const getBuildCommand = ( - application: ApplicationNested, - logPath: string + application: ApplicationNested, + logPath: string, ) => { - let command = ""; - const { buildType, registry } = application; - switch (buildType) { - case "nixpacks": - command = getNixpacksCommand(application, logPath); - break; - case "heroku_buildpacks": - command = getHerokuCommand(application, logPath); - break; - case "paketo_buildpacks": - command = getPaketoCommand(application, logPath); - break; - case "static": - command = getStaticCommand(application, logPath); - break; - case "dockerfile": - command = getDockerCommand(application, logPath); - break; - } - if (registry) { - command += uploadImageRemoteCommand(application, logPath); - } + let command = ""; + const { buildType, registry } = application; + switch (buildType) { + case "nixpacks": + command = getNixpacksCommand(application, logPath); + break; + case "heroku_buildpacks": + command = getHerokuCommand(application, logPath); + break; + case "paketo_buildpacks": + command = getPaketoCommand(application, logPath); + break; + case "static": + command = getStaticCommand(application, logPath); + break; + case "dockerfile": + command = getDockerCommand(application, logPath); + break; + } + if (registry) { + command += uploadImageRemoteCommand(application, logPath); + } - return command; + return command; }; export const mechanizeDockerContainer = async ( - application: ApplicationNested + application: ApplicationNested, ) => { - const { - appName, - env, - mounts, - cpuLimit, - memoryLimit, - memoryReservation, - cpuReservation, - command, - ports, - } = application; + const { + appName, + env, + mounts, + cpuLimit, + memoryLimit, + memoryReservation, + cpuReservation, + command, + ports, + } = application; - const resources = calculateResources({ - memoryLimit, - memoryReservation, - cpuLimit, - cpuReservation, - }); + const resources = calculateResources({ + memoryLimit, + memoryReservation, + cpuLimit, + cpuReservation, + }); - const volumesMount = generateVolumeMounts(mounts); + const volumesMount = generateVolumeMounts(mounts); - const { - HealthCheck, - RestartPolicy, - Placement, - Labels, - Mode, - RollbackConfig, - UpdateConfig, - Networks, - } = generateConfigContainer(application); + const { + HealthCheck, + RestartPolicy, + Placement, + Labels, + Mode, + RollbackConfig, + UpdateConfig, + Networks, + } = generateConfigContainer(application); - const bindsMount = generateBindMounts(mounts); - const filesMount = generateFileMounts(appName, application); - const envVariables = prepareEnvironmentVariables( - env, - application.project.env - ); + const bindsMount = generateBindMounts(mounts); + const filesMount = generateFileMounts(appName, application); + const envVariables = prepareEnvironmentVariables( + env, + application.project.env, + ); - const image = getImageName(application); - const authConfig = getAuthConfig(application); - const docker = await getRemoteDocker(application.serverId); + const image = getImageName(application); + const authConfig = getAuthConfig(application); + const docker = await getRemoteDocker(application.serverId); - const settings: CreateServiceOptions = { - authconfig: authConfig, - Name: appName, - TaskTemplate: { - ContainerSpec: { - HealthCheck, - Image: image, - Env: envVariables, - Mounts: [...volumesMount, ...bindsMount, ...filesMount], - ...(command - ? { - Command: ["/bin/sh"], - Args: ["-c", command], - } - : {}), - Labels, - }, - Networks, - RestartPolicy, - Placement, - Resources: { - ...resources, - }, - }, - Mode, - RollbackConfig, - EndpointSpec: { - Ports: ports.map((port) => ({ - Protocol: port.protocol, - TargetPort: port.targetPort, - PublishedPort: port.publishedPort, - })), - }, - UpdateConfig, - }; + const settings: CreateServiceOptions = { + authconfig: authConfig, + Name: appName, + TaskTemplate: { + ContainerSpec: { + HealthCheck, + Image: image, + Env: envVariables, + Mounts: [...volumesMount, ...bindsMount, ...filesMount], + ...(command + ? { + Command: ["/bin/sh"], + Args: ["-c", command], + } + : {}), + Labels, + }, + Networks, + RestartPolicy, + Placement, + Resources: { + ...resources, + }, + }, + Mode, + RollbackConfig, + EndpointSpec: { + Ports: ports.map((port) => ({ + Protocol: port.protocol, + TargetPort: port.targetPort, + PublishedPort: port.publishedPort, + })), + }, + UpdateConfig, + }; - try { - const service = docker.getService(appName); - const inspect = await service.inspect(); - await service.update({ - version: Number.parseInt(inspect.Version.Index), - ...settings, - TaskTemplate: { - ...settings.TaskTemplate, - ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, - }, - }); - } catch (error) { - await docker.createService(settings); - } + try { + const service = docker.getService(appName); + const inspect = await service.inspect(); + await service.update({ + version: Number.parseInt(inspect.Version.Index), + ...settings, + TaskTemplate: { + ...settings.TaskTemplate, + ForceUpdate: inspect.Spec.TaskTemplate.ForceUpdate + 1, + }, + }); + } catch (error) { + await docker.createService(settings); + } }; const getImageName = (application: ApplicationNested) => { - const { appName, sourceType, dockerImage, registry } = application; + const { appName, sourceType, dockerImage, registry } = application; - if (sourceType === "docker") { - return dockerImage || "ERROR-NO-IMAGE-PROVIDED"; - } + if (sourceType === "docker") { + return dockerImage || "ERROR-NO-IMAGE-PROVIDED"; + } - if (registry) { - return join(registry.registryUrl, registry.imagePrefix || "", appName); - } + if (registry) { + return join(registry.registryUrl, registry.imagePrefix || "", appName); + } - return `${appName}:latest`; + return `${appName}:latest`; }; const getAuthConfig = (application: ApplicationNested) => { - const { registry, username, password, sourceType, registryUrl } = application; + const { registry, username, password, sourceType, registryUrl } = application; - if (sourceType === "docker") { - if (username && password) { - return { - password, - username, - serveraddress: registryUrl || "", - }; - } - } else if (registry) { - return { - password: registry.password, - username: registry.username, - serveraddress: registry.registryUrl, - }; - } + if (sourceType === "docker") { + if (username && password) { + return { + password, + username, + serveraddress: registryUrl || "", + }; + } + } else if (registry) { + return { + password: registry.password, + username: registry.username, + serveraddress: registry.registryUrl, + }; + } - return undefined; + return undefined; }; diff --git a/packages/server/src/utils/cluster/upload.ts b/packages/server/src/utils/cluster/upload.ts index e4143bb0..980ace67 100644 --- a/packages/server/src/utils/cluster/upload.ts +++ b/packages/server/src/utils/cluster/upload.ts @@ -4,81 +4,81 @@ import type { ApplicationNested } from "../builders"; import { spawnAsync } from "../process/spawnAsync"; export const uploadImage = async ( - application: ApplicationNested, - writeStream: WriteStream + application: ApplicationNested, + writeStream: WriteStream, ) => { - const registry = application.registry; + const registry = application.registry; - if (!registry) { - throw new Error("Registry not found"); - } + if (!registry) { + throw new Error("Registry not found"); + } - const { registryUrl, imagePrefix } = registry; - const { appName } = application; - const imageName = `${appName}:latest`; + const { registryUrl, imagePrefix } = registry; + const { appName } = application; + const imageName = `${appName}:latest`; - const finalURL = registryUrl; + const finalURL = registryUrl; - const registryTag = path - .join(registryUrl, join(imagePrefix || "", imageName)) - .replace(/\/+/g, "/"); + const registryTag = path + .join(registryUrl, join(imagePrefix || "", imageName)) + .replace(/\/+/g, "/"); - try { - writeStream.write( - `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n` - ); - const loginCommand = spawnAsync( - "docker", - ["login", finalURL, "-u", registry.username, "--password-stdin"], - (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - } - ); - loginCommand.child?.stdin?.write(registry.password); - loginCommand.child?.stdin?.end(); - await loginCommand; + try { + writeStream.write( + `📦 [Enabled Registry] Uploading image to ${registry.registryType} | ${imageName} | ${finalURL}\n`, + ); + const loginCommand = spawnAsync( + "docker", + ["login", finalURL, "-u", registry.username, "--password-stdin"], + (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + }, + ); + loginCommand.child?.stdin?.write(registry.password); + loginCommand.child?.stdin?.end(); + await loginCommand; - await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); + await spawnAsync("docker", ["tag", imageName, registryTag], (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + }); - await spawnAsync("docker", ["push", registryTag], (data) => { - if (writeStream.writable) { - writeStream.write(data); - } - }); - } catch (error) { - console.log(error); - throw error; - } + await spawnAsync("docker", ["push", registryTag], (data) => { + if (writeStream.writable) { + writeStream.write(data); + } + }); + } catch (error) { + console.log(error); + throw error; + } }; export const uploadImageRemoteCommand = ( - application: ApplicationNested, - logPath: string + application: ApplicationNested, + logPath: string, ) => { - const registry = application.registry; + const registry = application.registry; - if (!registry) { - throw new Error("Registry not found"); - } + if (!registry) { + throw new Error("Registry not found"); + } - const { registryUrl, imagePrefix } = registry; - const { appName } = application; - const imageName = `${appName}:latest`; + const { registryUrl, imagePrefix } = registry; + const { appName } = application; + const imageName = `${appName}:latest`; - const finalURL = registryUrl; + const finalURL = registryUrl; - const registryTag = path - .join(registryUrl, join(imagePrefix || "", imageName)) - .replace(/\/+/g, "/"); + const registryTag = path + .join(registryUrl, join(imagePrefix || "", imageName)) + .replace(/\/+/g, "/"); - try { - const command = ` + try { + const command = ` echo "📦 [Enabled Registry] Uploading image to '${registry.registryType}' | '${registryTag}'" >> ${logPath}; echo "${registry.password}" | docker login ${finalURL} -u ${registry.username} --password-stdin >> ${logPath} 2>> ${logPath} || { echo "❌ DockerHub Failed" >> ${logPath}; @@ -96,8 +96,8 @@ export const uploadImageRemoteCommand = ( } echo "✅ Image Pushed" >> ${logPath}; `; - return command; - } catch (error) { - throw error; - } + return command; + } catch (error) { + throw error; + } }; From abdef13b93c7c767c56f5cc7cb3515460474d0c6 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Thu, 19 Dec 2024 02:14:06 -0600 Subject: [PATCH 25/62] refactor: set current color --- .../components/dashboard/settings/web-server/terminal.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx index 366784fc..d38f5d9e 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/terminal.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef } from "react"; import { FitAddon } from "xterm-addon-fit"; import "@xterm/xterm/css/xterm.css"; import { AttachAddon } from "@xterm/addon-attach"; +import { useTheme } from "next-themes"; interface Props { id: string; @@ -12,7 +13,7 @@ interface Props { export const Terminal: React.FC = ({ id, serverId }) => { const termRef = useRef(null); - + const { resolvedTheme } = useTheme(); useEffect(() => { const container = document.getElementById(id); if (container) { @@ -23,8 +24,9 @@ export const Terminal: React.FC = ({ id, serverId }) => { lineHeight: 1.4, convertEol: true, theme: { - cursor: "transparent", - background: "transparent", + cursor: resolvedTheme === "light" ? "#000000" : "transparent", + background: "rgba(0, 0, 0, 0)", + foreground: "currentColor", }, }); const addonFit = new FitAddon(); From 651e81ce6d4559a5fb07e385b0cae0a5b9a0098b Mon Sep 17 00:00:00 2001 From: Dominik Koch Date: Thu, 19 Dec 2024 11:22:58 +0100 Subject: [PATCH 26/62] feat(plausible): bump to 2.1.4 --- apps/dokploy/templates/plausible/docker-compose.yml | 2 +- apps/dokploy/templates/templates.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/templates/plausible/docker-compose.yml b/apps/dokploy/templates/plausible/docker-compose.yml index 62ce5ece..bb267f65 100644 --- a/apps/dokploy/templates/plausible/docker-compose.yml +++ b/apps/dokploy/templates/plausible/docker-compose.yml @@ -26,7 +26,7 @@ services: hard: 262144 plausible: - image: ghcr.io/plausible/community-edition:v2.1.0 + image: ghcr.io/plausible/community-edition:v2.1.4 restart: always command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run" depends_on: diff --git a/apps/dokploy/templates/templates.ts b/apps/dokploy/templates/templates.ts index f0d926d8..917184c5 100644 --- a/apps/dokploy/templates/templates.ts +++ b/apps/dokploy/templates/templates.ts @@ -34,7 +34,7 @@ export const templates: TemplateData[] = [ { id: "plausible", name: "Plausible", - version: "v2.1.0", + version: "v2.1.4", description: "Plausible is a open source, self-hosted web analytics platform that lets you track website traffic and user behavior.", logo: "plausible.svg", From e05d01788f19729ddca7d0ac40416ae0197625b3 Mon Sep 17 00:00:00 2001 From: usopp Date: Fri, 20 Dec 2024 00:11:48 +0100 Subject: [PATCH 27/62] style(notifications): better notification item style --- .../notifications/delete-notification.tsx | 11 ++- .../notifications/show-notifications.tsx | 90 ++++++++++--------- .../notifications/update-notification.tsx | 8 +- 3 files changed, 63 insertions(+), 46 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx index 468db851..4bb197b2 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/delete-notification.tsx @@ -11,7 +11,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; -import { TrashIcon } from "lucide-react"; +import { Trash2 } from "lucide-react"; import React from "react"; import { toast } from "sonner"; @@ -24,8 +24,13 @@ export const DeleteNotification = ({ notificationId }: Props) => { return ( - diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index c22f7b72..d742500f 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -40,48 +40,58 @@ export const ShowNotifications = () => {
) : (
-
- {data?.map((notification, index) => ( -
-
- {notification.notificationType === "slack" && ( - - )} - {notification.notificationType === "telegram" && ( - - )} - {notification.notificationType === "discord" && ( - - )} - {notification.notificationType === "email" && ( - - )} - - {notification.name} - -
- -
- - -
-
- ))} -
-
- +
+ {data?.map((notification, index) => ( +
+
+ {notification.notificationType === "slack" && ( +
+ +
+ )} + {notification.notificationType === "telegram" && ( +
+ +
+ )} + {notification.notificationType === "discord" && ( +
+ +
+ )} + {notification.notificationType === "email" && ( +
+ +
+ )} +
+ + {notification.name} + + + {notification.notificationType[0].toUpperCase() + notification.notificationType.slice(1)} notification + +
+
+
+ + +
+ ))}
+ +
+ +
+
)} diff --git a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx index 9bdf35f1..cfa2e0ba 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/update-notification.tsx @@ -26,7 +26,7 @@ import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { api } from "@/utils/api"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Mail, PenBoxIcon } from "lucide-react"; +import { Mail, Pen } from "lucide-react"; import { useEffect, useState } from "react"; import { FieldErrors, useFieldArray, useForm } from "react-hook-form"; import { toast } from "sonner"; @@ -218,8 +218,10 @@ export const UpdateNotification = ({ notificationId }: Props) => { return ( - From 77336a21f9889588b0b99f63c1b95afa4d726512 Mon Sep 17 00:00:00 2001 From: usopp Date: Fri, 20 Dec 2024 00:21:51 +0100 Subject: [PATCH 28/62] chore: lint --- .../dashboard/settings/notifications/show-notifications.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index d742500f..28aeb8c0 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -72,7 +72,7 @@ export const ShowNotifications = () => { {notification.name} - {notification.notificationType[0].toUpperCase() + notification.notificationType.slice(1)} notification + {notification.notificationType && notification.notificationType[0].toUpperCase() + notification.notificationType.slice(1)} notification
From ed8be62ff30dcaeeea34ec815e15f74e45fa46a4 Mon Sep 17 00:00:00 2001 From: usopp Date: Fri, 20 Dec 2024 00:24:44 +0100 Subject: [PATCH 29/62] chore: lint --- .../dashboard/settings/notifications/show-notifications.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx index 28aeb8c0..10ea7304 100644 --- a/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx +++ b/apps/dokploy/components/dashboard/settings/notifications/show-notifications.tsx @@ -72,7 +72,7 @@ export const ShowNotifications = () => { {notification.name}
- {notification.notificationType && notification.notificationType[0].toUpperCase() + notification.notificationType.slice(1)} notification + {notification.notificationType?.[0]?.toUpperCase() + notification.notificationType?.slice(1)} notification
From bf2551b0f613e1fe2186c88c011ec3da80eb29fc Mon Sep 17 00:00:00 2001 From: 190km Date: Fri, 20 Dec 2024 00:54:32 +0100 Subject: [PATCH 30/62] fix(settings/profile): fixed password changing --- apps/dokploy/server/api/routers/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index ef9db4da..cad77927 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -188,9 +188,9 @@ export const authRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const currentAuth = await findAuthByEmail(ctx.user.email); - if (input.password) { + if (input.currentPassword) { const correctPassword = bcrypt.compareSync( - input.password, + input.currentPassword, currentAuth?.password || "", ); if (!correctPassword) { From fdfa92753252af5b828d6785f9dd2fe382927599 Mon Sep 17 00:00:00 2001 From: 190km Date: Fri, 20 Dec 2024 01:00:16 +0100 Subject: [PATCH 31/62] feat(settings/profile): reset password form after validating password change --- .../components/dashboard/settings/profile/profile-form.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx index 65ccff0e..1141397f 100644 --- a/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx +++ b/apps/dokploy/components/dashboard/settings/profile/profile-form.tsx @@ -104,6 +104,7 @@ export const ProfileForm = () => { .then(async () => { await refetch(); toast.success("Profile Updated"); + form.reset(); }) .catch(() => { toast.error("Error to Update the profile"); From d9a1976cc0a3a1113232078c1c751c7097f8053e Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 14:01:55 +0100 Subject: [PATCH 32/62] fix: check updates message fixes --- .../dashboard/settings/web-server/update-server.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 48a61c7a..a2e4a9fa 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -76,18 +76,18 @@ export const UpdateServer = () => { className="w-full" onClick={async () => { await checkAndUpdateImage() - .then(async (e) => { - setIsUpdateAvailable(e); + .then(async (updateAvailable) => { + setIsUpdateAvailable(updateAvailable); + toast.info(updateAvailable ? "Update is available" : "No updates available"); }) .catch(() => { setIsUpdateAvailable(false); - toast.error("Error to check updates"); + toast.error("An error occurred while checking for updates, please try again."); }); - toast.success("Check updates"); }} isLoading={isLoading} > - Check Updates + {isLoading ? "Checking for updates..." : "Check for updates"} )}
From dd64b063408432f2bcd0a81824b0e0d5b8841d91 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 14:09:05 +0100 Subject: [PATCH 33/62] style: format with biome --- .../dashboard/settings/web-server/update-server.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index a2e4a9fa..06d4a3f1 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -78,11 +78,17 @@ export const UpdateServer = () => { await checkAndUpdateImage() .then(async (updateAvailable) => { setIsUpdateAvailable(updateAvailable); - toast.info(updateAvailable ? "Update is available" : "No updates available"); + toast.info( + updateAvailable + ? "Update is available" + : "No updates available", + ); }) .catch(() => { setIsUpdateAvailable(false); - toast.error("An error occurred while checking for updates, please try again."); + toast.error( + "An error occurred while checking for updates, please try again.", + ); }); }} isLoading={isLoading} From b842887bc388f32726c77ba2c4431a83c4720a77 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 16:43:05 +0100 Subject: [PATCH 34/62] feat: add toggle for auto updates checking --- .../web-server/toggle-auto-check-updates.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx diff --git a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx new file mode 100644 index 00000000..d115672a --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx @@ -0,0 +1,27 @@ +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { useState } from "react"; + +export const ToggleAutoCheckUpdates = () => { + const [enabled, setEnabled] = useState( + localStorage.getItem("enableAutoCheckUpdates") === "true", + ); + + const handleToggle = async (checked: boolean) => { + setEnabled(checked); + localStorage.setItem("enableAutoCheckUpdates", String(checked)); + }; + + return ( +
+ + +
+ ); +}; From a5cd8f18cdc9d3ff09f792721701e3e49487b271 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:23:02 +0100 Subject: [PATCH 35/62] feat: show auto check update toggle --- .../dashboard/settings/web-server/update-server.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 06d4a3f1..8d1ed2e0 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -14,13 +14,14 @@ import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; import { UpdateWebServer } from "./update-webserver"; +import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates"; export const UpdateServer = () => { const [isUpdateAvailable, setIsUpdateAvailable] = useState( null, ); - const { mutateAsync: checkAndUpdateImage, isLoading } = - api.settings.checkAndUpdateImage.useMutation(); + const { mutateAsync: checkServerUpdates, isLoading } = + api.settings.checkServerUpdates.useMutation(); const [isOpen, setIsOpen] = useState(false); return ( @@ -61,6 +62,7 @@ export const UpdateServer = () => {
+ {isUpdateAvailable === false && (
@@ -75,7 +77,7 @@ export const UpdateServer = () => { @@ -36,19 +63,12 @@ export const UpdateWebServer = () => { Are you absolutely sure? This action cannot be undone. This will update the web server to the - new version. + new version. The page will be reloaded once the update is finished. Cancel - { - await updateServer(); - toast.success("Please reload the browser to see the changes"); - }} - > - Confirm - + Confirm From 2804748118511dd0a46b48c202c94f6d419ed94c Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:27:51 +0100 Subject: [PATCH 38/62] refactor: rename action, move pull to updateServer --- apps/dokploy/server/api/routers/settings.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 7c777b17..001da65e 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -45,6 +45,7 @@ import { stopService, stopServiceRemote, updateAdmin, + checkIsUpdateAvailable, updateLetsEncryptEmail, updateServerById, updateServerTraefik, @@ -342,17 +343,20 @@ export const settingsRouter = createTRPCRouter({ writeConfig("middlewares", input.traefikConfig); return true; }), - - checkAndUpdateImage: adminProcedure.mutation(async () => { + checkForUpdate: adminProcedure.mutation(async () => { if (IS_CLOUD) { return true; } - return await pullLatestRelease(); + + return await checkIsUpdateAvailable(); }), updateServer: adminProcedure.mutation(async () => { if (IS_CLOUD) { return true; } + + await pullLatestRelease(); + await spawnAsync("docker", [ "service", "update", @@ -361,6 +365,7 @@ export const settingsRouter = createTRPCRouter({ getDokployImage(), "dokploy", ]); + return true; }), From 256534570b450c4dade3e27e2728a080c17ada32 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:29:01 +0100 Subject: [PATCH 39/62] refactor: add image tag helper, refactor update check logic, remove try/catch --- packages/server/src/services/settings.ts | 54 +++++++++++++++--------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 8261843a..fb8e6a65 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -3,37 +3,49 @@ import { join } from "node:path"; import { docker } from "@dokploy/server/constants"; import { getServiceContainer } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; +import { spawnAsync } from "../utils/process/spawnAsync"; // import packageInfo from "../../../package.json"; -const updateIsAvailable = async () => { - try { - const service = await getServiceContainer("dokploy"); +/** Returns current Dokploy docker image tag or `latest` by default. */ +export const getDokployImageTag = () => { + return process.env.RELEASE_TAG || "latest"; +}; - const localImage = await docker.getImage(getDokployImage()).inspect(); - return localImage.Id !== service?.ImageID; - } catch (error) { - return false; - } +/** Checks if server update is available by comparing current image's digest against digest for provided image tag via Docker hub API */ +export const checkIsUpdateAvailable = async () => { + const commandResult = await spawnAsync("docker", [ + "inspect", + "--format={{index .RepoDigests 0}}", + getDokployImage(), + ]); + + const currentDigest = commandResult.toString().trim().split("@")[1]; + + const url = `https://hub.docker.com/v2/repositories/dokploy/dokploy/tags/${getDokployImageTag()}`; + const response = await fetch(url, { + method: "GET", + headers: { "Content-Type": "application/json" }, + }); + + const data = (await response.json()) as { digest: string }; + const { digest } = data; + + return digest !== currentDigest; }; export const getDokployImage = () => { - return `dokploy/dokploy:${process.env.RELEASE_TAG || "latest"}`; + return `dokploy/dokploy:${getDokployImageTag()}`; }; export const pullLatestRelease = async () => { - try { - const stream = await docker.pull(getDokployImage(), {}); - await new Promise((resolve, reject) => { - docker.modem.followProgress(stream, (err, res) => - err ? reject(err) : resolve(res), - ); - }); - const newUpdateIsAvailable = await updateIsAvailable(); - return newUpdateIsAvailable; - } catch (error) {} - - return false; + const stream = await docker.pull(getDokployImage()); + await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err, res) => + err ? reject(err) : resolve(res), + ); + }); }; + export const getDokployVersion = () => { // return packageInfo.version; }; From a06dd17aa136bab2276b24c7e8ecdbff6ca160cc Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 17:30:14 +0100 Subject: [PATCH 40/62] feat(navbar): add automatic update checking interval, add update available button --- apps/dokploy/components/layouts/navbar.tsx | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index cead4683..b0836939 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -15,8 +15,13 @@ import { useRouter } from "next/router"; import { Logo } from "../shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { buttonVariants } from "../ui/button"; +import { useEffect, useRef, useState } from "react"; +import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; + +const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 15; export const Navbar = () => { + const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); const router = useRouter(); const { data } = api.auth.get.useQuery(); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -29,6 +34,55 @@ export const Navbar = () => { }, ); const { mutateAsync } = api.auth.logout.useMutation(); + const { mutateAsync: checkForUpdate } = + api.settings.checkForUpdate.useMutation(); + + const checkUpdatesIntervalRef = useRef(null); + + useEffect(() => { + // Handling of automatic check for server updates + if (!localStorage.getItem("enableAutoCheckUpdates")) { + // Enable auto update checking by default if user didn't change it + localStorage.setItem("enableAutoCheckUpdates", "true"); + } + + const clearUpdatesInterval = () => { + if (checkUpdatesIntervalRef.current) { + clearInterval(checkUpdatesIntervalRef.current); + } + }; + + const checkUpdates = async () => { + try { + if (localStorage.getItem("enableAutoCheckUpdates") !== "true") { + return; + } + + const updateAvailable = await checkForUpdate(); + + if (updateAvailable) { + // Stop interval when update is available + clearUpdatesInterval(); + setIsUpdateAvailable(true); + } + } catch (error) { + console.error("Error auto-checking for updates:", error); + } + }; + + checkUpdatesIntervalRef.current = setInterval( + checkUpdates, + AUTO_CHECK_UPDATES_INTERVAL_MINUTES * 60000, + ); + + // Also check for updates on initial page load + checkUpdates(); + + return () => { + clearUpdatesInterval(); + }; + }, []); + return (
+ {isUpdateAvailable && ( +
+ +
+ )} Date: Fri, 20 Dec 2024 17:32:10 +0100 Subject: [PATCH 41/62] refactor: remove unused async --- .../dashboard/settings/web-server/toggle-auto-check-updates.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx index d115672a..5c07d5df 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx @@ -7,7 +7,7 @@ export const ToggleAutoCheckUpdates = () => { localStorage.getItem("enableAutoCheckUpdates") === "true", ); - const handleToggle = async (checked: boolean) => { + const handleToggle = (checked: boolean) => { setEnabled(checked); localStorage.setItem("enableAutoCheckUpdates", String(checked)); }; From 4565b3d7a2af0681f7e8a419e12a0910c529b99c Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 18:26:54 +0100 Subject: [PATCH 42/62] refactor: add latestVersion information to update data --- .../settings/web-server/update-server.tsx | 8 ++--- apps/dokploy/components/layouts/navbar.tsx | 6 ++-- apps/dokploy/server/api/routers/settings.ts | 8 ++--- packages/server/src/services/settings.ts | 35 +++++++++++++++---- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 14a66749..1b8798e4 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -20,16 +20,16 @@ export const UpdateServer = () => { const [isUpdateAvailable, setIsUpdateAvailable] = useState( null, ); - const { mutateAsync: checkForUpdate, isLoading } = - api.settings.checkForUpdate.useMutation(); + const { mutateAsync: getUpdateData, isLoading } = + api.settings.getUpdateData.useMutation(); const [isOpen, setIsOpen] = useState(false); const handleCheckUpdates = async () => { try { - const updateAvailable = await checkForUpdate(); + const { updateAvailable, latestVersion } = await getUpdateData(); setIsUpdateAvailable(updateAvailable); if (updateAvailable) { - toast.success("Update is available!"); + toast.success(`${latestVersion} update is available!`); } else { toast.info("No updates available"); } diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index b0836939..a5a8b74c 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -34,8 +34,8 @@ export const Navbar = () => { }, ); const { mutateAsync } = api.auth.logout.useMutation(); - const { mutateAsync: checkForUpdate } = - api.settings.checkForUpdate.useMutation(); + const { mutateAsync: getUpdateData } = + api.settings.getUpdateData.useMutation(); const checkUpdatesIntervalRef = useRef(null); @@ -58,7 +58,7 @@ export const Navbar = () => { return; } - const updateAvailable = await checkForUpdate(); + const { updateAvailable } = await getUpdateData(); if (updateAvailable) { // Stop interval when update is available diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 001da65e..2861511e 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -45,7 +45,7 @@ import { stopService, stopServiceRemote, updateAdmin, - checkIsUpdateAvailable, + getUpdateData, updateLetsEncryptEmail, updateServerById, updateServerTraefik, @@ -343,12 +343,12 @@ export const settingsRouter = createTRPCRouter({ writeConfig("middlewares", input.traefikConfig); return true; }), - checkForUpdate: adminProcedure.mutation(async () => { + getUpdateData: adminProcedure.mutation(async () => { if (IS_CLOUD) { - return true; + return { latestVersion: null, updateAvailable: false }; } - return await checkIsUpdateAvailable(); + return await getUpdateData(); }), updateServer: adminProcedure.mutation(async () => { if (IS_CLOUD) { diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index fb8e6a65..3000f832 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -1,7 +1,6 @@ import { readdirSync } from "node:fs"; import { join } from "node:path"; import { docker } from "@dokploy/server/constants"; -import { getServiceContainer } from "@dokploy/server/utils/docker/utils"; import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; import { spawnAsync } from "../utils/process/spawnAsync"; // import packageInfo from "../../../package.json"; @@ -11,8 +10,11 @@ export const getDokployImageTag = () => { return process.env.RELEASE_TAG || "latest"; }; -/** Checks if server update is available by comparing current image's digest against digest for provided image tag via Docker hub API */ -export const checkIsUpdateAvailable = async () => { +/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ +export const getUpdateData = async (): Promise<{ + latestVersion: string | null; + updateAvailable: boolean; +}> => { const commandResult = await spawnAsync("docker", [ "inspect", "--format={{index .RepoDigests 0}}", @@ -21,16 +23,35 @@ export const checkIsUpdateAvailable = async () => { const currentDigest = commandResult.toString().trim().split("@")[1]; - const url = `https://hub.docker.com/v2/repositories/dokploy/dokploy/tags/${getDokployImageTag()}`; + const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { method: "GET", headers: { "Content-Type": "application/json" }, }); - const data = (await response.json()) as { digest: string }; - const { digest } = data; + const data = (await response.json()) as { + results: [{ digest: string; name: string }]; + }; + const { results } = data; + const latestTagDigest = results.find((t) => t.name === "latest")?.digest; - return digest !== currentDigest; + if (!latestTagDigest) { + return { latestVersion: null, updateAvailable: false }; + } + + const versionedTag = results.find( + (t) => t.digest === latestTagDigest && t.name.startsWith("v"), + ); + + if (!versionedTag) { + return { latestVersion: null, updateAvailable: false }; + } + + const { name: latestVersion, digest } = versionedTag; + + const updateAvailable = digest !== currentDigest; + + return { latestVersion, updateAvailable }; }; export const getDokployImage = () => { From ab9aa56c48ba7eb15c6a3c5f2bb05f3b83cf253d Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Fri, 20 Dec 2024 18:57:28 +0100 Subject: [PATCH 43/62] refactor: disable automatic updates for cloud version --- apps/dokploy/components/layouts/navbar.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index a5a8b74c..3d17b3e9 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -41,6 +41,10 @@ export const Navbar = () => { useEffect(() => { // Handling of automatic check for server updates + if (isCloud) { + return; + } + if (!localStorage.getItem("enableAutoCheckUpdates")) { // Enable auto update checking by default if user didn't change it localStorage.setItem("enableAutoCheckUpdates", "true"); From 788771c5eb74cda95d2e490dfe981ed56cd1e63c Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Fri, 20 Dec 2024 23:16:38 -0600 Subject: [PATCH 44/62] refactor: add password in validation --- apps/dokploy/server/api/routers/auth.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index cad77927..f0d3b495 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -188,9 +188,9 @@ export const authRouter = createTRPCRouter({ .mutation(async ({ ctx, input }) => { const currentAuth = await findAuthByEmail(ctx.user.email); - if (input.currentPassword) { + if (input.currentPassword || input.password) { const correctPassword = bcrypt.compareSync( - input.currentPassword, + input.currentPassword || "", currentAuth?.password || "", ); if (!correctPassword) { From f40e80233174ff4532da490bd1b17a79b00a60d3 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 08:47:21 +0100 Subject: [PATCH 45/62] fix: pull latest release in case of no image when checking update --- packages/server/src/services/settings.ts | 54 ++++++++++++++++-------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 3000f832..b77dbbc9 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -10,11 +10,21 @@ export const getDokployImageTag = () => { return process.env.RELEASE_TAG || "latest"; }; -/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ -export const getUpdateData = async (): Promise<{ - latestVersion: string | null; - updateAvailable: boolean; -}> => { +export const getDokployImage = () => { + return `dokploy/dokploy:${getDokployImageTag()}`; +}; + +export const pullLatestRelease = async () => { + const stream = await docker.pull(getDokployImage()); + await new Promise((resolve, reject) => { + docker.modem.followProgress(stream, (err, res) => + err ? reject(err) : resolve(res), + ); + }); +}; + +/** Returns current docker image digest */ +export const getCurrentImageDigest = async () => { const commandResult = await spawnAsync("docker", [ "inspect", "--format={{index .RepoDigests 0}}", @@ -23,6 +33,27 @@ export const getUpdateData = async (): Promise<{ const currentDigest = commandResult.toString().trim().split("@")[1]; + return currentDigest; +}; + +/** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ +export const getUpdateData = async (): Promise<{ + latestVersion: string | null; + updateAvailable: boolean; +}> => { + let currentDigest: string | undefined; + try { + currentDigest = await getCurrentImageDigest(); + } catch { + // In case image doesn't exist yet, pull latest release + await pullLatestRelease(); + currentDigest = await getCurrentImageDigest(); + } + + if (!currentDigest) { + throw new Error("Could not get current image digest"); + } + const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { method: "GET", @@ -54,19 +85,6 @@ export const getUpdateData = async (): Promise<{ return { latestVersion, updateAvailable }; }; -export const getDokployImage = () => { - return `dokploy/dokploy:${getDokployImageTag()}`; -}; - -export const pullLatestRelease = async () => { - const stream = await docker.pull(getDokployImage()); - await new Promise((resolve, reject) => { - docker.modem.followProgress(stream, (err, res) => - err ? reject(err) : resolve(res), - ); - }); -}; - export const getDokployVersion = () => { // return packageInfo.version; }; From 1aae523a0bf8eea4ce7ede4a31bb258801cf9dac Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 21 Dec 2024 01:53:39 -0600 Subject: [PATCH 46/62] refactor: add missing verifyToken --- apps/dokploy/server/api/routers/auth.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index f0d3b495..a1345cca 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -268,7 +268,9 @@ export const authRouter = createTRPCRouter({ return auth; }), - + verifyToken: protectedProcedure.mutation(async () => { + return true; + }), one: adminProcedure.input(apiFindOneAuth).query(async ({ input }) => { const auth = await findAuthById(input.id); return auth; From 18eae9f7d73f2ac15046861ae5aa1f7a4a838361 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 09:04:25 +0100 Subject: [PATCH 47/62] refactor: use service image sha instead of image itself for checking updates --- packages/server/src/services/settings.ts | 34 ++++++++---------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index b77dbbc9..a3bd3f09 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -1,8 +1,10 @@ import { readdirSync } from "node:fs"; import { join } from "node:path"; import { docker } from "@dokploy/server/constants"; -import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync"; -import { spawnAsync } from "../utils/process/spawnAsync"; +import { + execAsync, + execAsyncRemote, +} from "@dokploy/server/utils/process/execAsync"; // import packageInfo from "../../../package.json"; /** Returns current Dokploy docker image tag or `latest` by default. */ @@ -23,15 +25,12 @@ export const pullLatestRelease = async () => { }); }; -/** Returns current docker image digest */ -export const getCurrentImageDigest = async () => { - const commandResult = await spawnAsync("docker", [ - "inspect", - "--format={{index .RepoDigests 0}}", - getDokployImage(), - ]); - - const currentDigest = commandResult.toString().trim().split("@")[1]; +/** Returns Dokploy docker service image digest */ +export const getServiceImageDigest = async () => { + const { stdout } = await execAsync( + "docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'", + ); + const currentDigest = stdout.trim().split("@")[1]; return currentDigest; }; @@ -41,18 +40,7 @@ export const getUpdateData = async (): Promise<{ latestVersion: string | null; updateAvailable: boolean; }> => { - let currentDigest: string | undefined; - try { - currentDigest = await getCurrentImageDigest(); - } catch { - // In case image doesn't exist yet, pull latest release - await pullLatestRelease(); - currentDigest = await getCurrentImageDigest(); - } - - if (!currentDigest) { - throw new Error("Could not get current image digest"); - } + const currentDigest = await getServiceImageDigest(); const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { From 7a8bb8f71d9a1bf7f78c7041a0248c6e2a0ea382 Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 21 Dec 2024 02:45:58 -0600 Subject: [PATCH 48/62] fix: add missing notifications in cron jobs --- apps/dokploy/server/api/routers/settings.ts | 4 +- packages/server/src/utils/backups/index.ts | 140 ++++++++++++++++---- 2 files changed, 116 insertions(+), 28 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 7c777b17..e6469224 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -267,11 +267,11 @@ export const settingsRouter = createTRPCRouter({ message: "You are not authorized to access this admin", }); } - await updateAdmin(ctx.user.authId, { + const adminUpdated = await updateAdmin(ctx.user.authId, { enableDockerCleanup: input.enableDockerCleanup, }); - if (admin.enableDockerCleanup) { + if (adminUpdated?.enableDockerCleanup) { scheduleJob("docker-cleanup", "0 0 * * *", async () => { console.log( `Docker Cleanup ${new Date().toLocaleString()}] Running...`, diff --git a/packages/server/src/utils/backups/index.ts b/packages/server/src/utils/backups/index.ts index b1619253..797feb38 100644 --- a/packages/server/src/utils/backups/index.ts +++ b/packages/server/src/utils/backups/index.ts @@ -11,6 +11,8 @@ import { runMariadbBackup } from "./mariadb"; import { runMongoBackup } from "./mongo"; import { runMySqlBackup } from "./mysql"; import { runPostgresBackup } from "./postgres"; +import { sendDockerCleanupNotifications } from "../notifications/docker-cleanup"; +import { sendDatabaseBackupNotifications } from "../notifications/database-backup"; export const initCronJobs = async () => { console.log("Setting up cron jobs...."); @@ -25,14 +27,15 @@ export const initCronJobs = async () => { await cleanUpUnusedImages(); await cleanUpDockerBuilder(); await cleanUpSystemPrune(); + await sendDockerCleanupNotifications(admin.adminId); }); } const servers = await getAllServers(); for (const server of servers) { - const { appName, serverId } = server; - if (serverId) { + const { appName, serverId, enableDockerCleanup } = server; + if (enableDockerCleanup) { scheduleJob(serverId, "0 0 * * *", async () => { console.log( `SERVER-BACKUP[${new Date().toLocaleString()}] Running Cleanup ${appName}`, @@ -40,12 +43,17 @@ export const initCronJobs = async () => { await cleanUpUnusedImages(serverId); await cleanUpDockerBuilder(serverId); await cleanUpSystemPrune(serverId); + await sendDockerCleanupNotifications( + admin.adminId, + `Docker cleanup for Server ${appName}`, + ); }); } } const pgs = await db.query.postgres.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -61,18 +69,39 @@ export const initCronJobs = async () => { for (const backup of pg.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - runPostgresBackup(pg, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `PG-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + runPostgresBackup(pg, backup); + }); + + await sendDatabaseBackupNotifications({ + applicationName: pg.name, + projectName: pg.project.name, + databaseType: "postgres", + type: "success", + adminId: pg.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: pg.name, + projectName: pg.project.name, + databaseType: "postgres", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: pg.project.adminId, + }); + } } } } const mariadbs = await db.query.mariadb.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -89,18 +118,38 @@ export const initCronJobs = async () => { for (const backup of maria.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - await runMariadbBackup(maria, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `MARIADB-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + await runMariadbBackup(maria, backup); + }); + await sendDatabaseBackupNotifications({ + applicationName: maria.name, + projectName: maria.project.name, + databaseType: "mariadb", + type: "success", + adminId: maria.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: maria.name, + projectName: maria.project.name, + databaseType: "mariadb", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: maria.project.adminId, + }); + } } } } const mongodbs = await db.query.mongo.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -117,18 +166,38 @@ export const initCronJobs = async () => { for (const backup of mongo.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - await runMongoBackup(mongo, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `MONGO-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + await runMongoBackup(mongo, backup); + }); + await sendDatabaseBackupNotifications({ + applicationName: mongo.name, + projectName: mongo.project.name, + databaseType: "mongodb", + type: "success", + adminId: mongo.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: mongo.name, + projectName: mongo.project.name, + databaseType: "mongodb", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: mongo.project.adminId, + }); + } } } } const mysqls = await db.query.mysql.findMany({ with: { + project: true, backups: { with: { destination: true, @@ -145,12 +214,31 @@ export const initCronJobs = async () => { for (const backup of mysql.backups) { const { schedule, backupId, enabled } = backup; if (enabled) { - scheduleJob(backupId, schedule, async () => { - console.log( - `MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, - ); - await runMySqlBackup(mysql, backup); - }); + try { + scheduleJob(backupId, schedule, async () => { + console.log( + `MYSQL-SERVER[${new Date().toLocaleString()}] Running Backup ${backupId}`, + ); + await runMySqlBackup(mysql, backup); + }); + await sendDatabaseBackupNotifications({ + applicationName: mysql.name, + projectName: mysql.project.name, + databaseType: "mysql", + type: "success", + adminId: mysql.project.adminId, + }); + } catch (error) { + await sendDatabaseBackupNotifications({ + applicationName: mysql.name, + projectName: mysql.project.name, + databaseType: "mysql", + type: "error", + // @ts-ignore + errorMessage: error?.message || "Error message not provided", + adminId: mysql.project.adminId, + }); + } } } } From 8699e024ee1f80e53bd9495d7c8776aebfddaa5d Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 10:05:31 +0100 Subject: [PATCH 49/62] refactor: add try catch, add default update data --- apps/dokploy/server/api/routers/settings.ts | 3 +- packages/server/src/services/settings.ts | 34 ++++++++++++++++----- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 2861511e..937c93d3 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -52,6 +52,7 @@ import { writeConfig, writeMainConfig, writeTraefikConfigInPath, + DEFAULT_UPDATE_DATA, } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; @@ -345,7 +346,7 @@ export const settingsRouter = createTRPCRouter({ }), getUpdateData: adminProcedure.mutation(async () => { if (IS_CLOUD) { - return { latestVersion: null, updateAvailable: false }; + return DEFAULT_UPDATE_DATA; } return await getUpdateData(); diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index a3bd3f09..0d2ec967 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -7,6 +7,16 @@ import { } from "@dokploy/server/utils/process/execAsync"; // import packageInfo from "../../../package.json"; +export interface IUpdateData { + latestVersion: string | null; + updateAvailable: boolean; +} + +export const DEFAULT_UPDATE_DATA: IUpdateData = { + latestVersion: null, + updateAvailable: false, +}; + /** Returns current Dokploy docker image tag or `latest` by default. */ export const getDokployImageTag = () => { return process.env.RELEASE_TAG || "latest"; @@ -30,17 +40,27 @@ export const getServiceImageDigest = async () => { const { stdout } = await execAsync( "docker service inspect dokploy --format '{{.Spec.TaskTemplate.ContainerSpec.Image}}'", ); + const currentDigest = stdout.trim().split("@")[1]; + if (!currentDigest) { + throw new Error("Could not get current service image digest"); + } + return currentDigest; }; /** Returns latest version number and information whether server update is available by comparing current image's digest against digest for provided image tag via Docker hub API. */ -export const getUpdateData = async (): Promise<{ - latestVersion: string | null; - updateAvailable: boolean; -}> => { - const currentDigest = await getServiceImageDigest(); +export const getUpdateData = async (): Promise => { + let currentDigest: string; + try { + currentDigest = await getServiceImageDigest(); + } catch { + // Docker service might not exist locally + // You can run the # Installation command for docker service create mentioned in the below docs to test it locally: + // https://docs.dokploy.com/docs/core/manual-installation + return DEFAULT_UPDATE_DATA; + } const url = "https://hub.docker.com/v2/repositories/dokploy/dokploy/tags"; const response = await fetch(url, { @@ -55,7 +75,7 @@ export const getUpdateData = async (): Promise<{ const latestTagDigest = results.find((t) => t.name === "latest")?.digest; if (!latestTagDigest) { - return { latestVersion: null, updateAvailable: false }; + return DEFAULT_UPDATE_DATA; } const versionedTag = results.find( @@ -63,7 +83,7 @@ export const getUpdateData = async (): Promise<{ ); if (!versionedTag) { - return { latestVersion: null, updateAvailable: false }; + return DEFAULT_UPDATE_DATA; } const { name: latestVersion, digest } = versionedTag; From a8ff6c7b3f83c6c41a72462b065500f9116fedcf Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Sat, 21 Dec 2024 11:03:41 -0500 Subject: [PATCH 50/62] feat(updates): new update UI --- .../settings/web-server/update-server.tsx | 201 +++++++++++++----- .../settings/web-server/update-webserver.tsx | 4 +- apps/dokploy/components/layouts/navbar.tsx | 4 +- apps/dokploy/server/api/routers/settings.ts | 4 +- 4 files changed, 158 insertions(+), 55 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx index 1b8798e4..1bb240bb 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-server.tsx @@ -3,38 +3,49 @@ import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, - DialogDescription, - DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { api } from "@/utils/api"; -import { RefreshCcw } from "lucide-react"; +import { + Bug, + Download, + Info, + RefreshCcw, + Server, + Sparkles, + Stars, +} from "lucide-react"; import Link from "next/link"; import { useState } from "react"; import { toast } from "sonner"; -import { UpdateWebServer } from "./update-webserver"; import { ToggleAutoCheckUpdates } from "./toggle-auto-check-updates"; +import { UpdateWebServer } from "./update-webserver"; export const UpdateServer = () => { - const [isUpdateAvailable, setIsUpdateAvailable] = useState( - null, - ); + const [hasCheckedUpdate, setHasCheckedUpdate] = useState(false); + const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); const { mutateAsync: getUpdateData, isLoading } = api.settings.getUpdateData.useMutation(); + const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const [isOpen, setIsOpen] = useState(false); + const [latestVersion, setLatestVersion] = useState(""); const handleCheckUpdates = async () => { try { - const { updateAvailable, latestVersion } = await getUpdateData(); - setIsUpdateAvailable(updateAvailable); - if (updateAvailable) { - toast.success(`${latestVersion} update is available!`); + const updateData = await getUpdateData(); + setHasCheckedUpdate(true); + setIsUpdateAvailable(updateData.updateAvailable); + setLatestVersion(updateData.latestVersion || ""); + + if (updateData.updateAvailable) { + toast.success(`${updateData.latestVersion || ""} update is available!`); } else { toast.info("No updates available"); } } catch (error) { console.error("Error checking for updates:", error); + setHasCheckedUpdate(true); setIsUpdateAvailable(false); toast.error( "An error occurred while checking for updates, please try again.", @@ -45,59 +56,147 @@ export const UpdateServer = () => { return ( - - - - Web Server Update - - Check new releases and update your dokploy - - + +
+ + Web Server Update + + {dokployVersion && ( +
+ + {dokployVersion} +
+ )} +
-
- - We suggest to update your dokploy to the latest version only if you: - -
    -
  • Want to try the latest features
  • -
  • Some bug that is blocking to use some features
  • -
- - We recommend checking the latest version for any breaking changes - before updating. Go to{" "} - - Dokploy Releases - {" "} - to check the latest version. - + {/* Initial state */} + {!hasCheckedUpdate && ( +
+

+ Check for new releases and update Dokploy. +
+
+ We recommend checking for updates regularly to ensure you have the + latest features and security improvements. +

+
+ )} -
- - {isUpdateAvailable === false && ( -
- - - You are using the latest version + {/* Update available state */} + {isUpdateAvailable && latestVersion && ( +
+
+
+ + + + + + + New version available:
- )} + + {latestVersion} + +
+ +
+

+ A new version of the server software is available. Consider + updating if you: +

+
    +
  • + + + Want to access the latest features and improvements + +
  • +
  • + + + Are experiencing issues that may be resolved in the new + version + +
  • +
+
+
+ )} + + {/* Up to date state */} + {hasCheckedUpdate && !isUpdateAvailable && !isLoading && ( +
+
+
+ +
+
+

+ You are using the latest version +

+

+ Your server is up to date with all the latest features and + security improvements. +

+
+
+
+ )} + + {isUpdateAvailable && ( +
+
+ +
+ We recommend reviewing the{" "} + + release notes + {" "} + for any breaking changes before updating. +
+
+
+ )} + +
+ +
+ +
+
+ {isUpdateAvailable ? ( ) : ( )}
@@ -106,3 +205,5 @@ export const UpdateServer = () => {
); }; + +export default UpdateServer; diff --git a/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx b/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx index 9b3c89f6..c1e5de70 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/update-webserver.tsx @@ -11,6 +11,7 @@ import { } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { api } from "@/utils/api"; +import { HardDriveDownload } from "lucide-react"; import { toast } from "sonner"; interface Props { @@ -21,7 +22,7 @@ export const UpdateWebServer = ({ isNavbar }: Props) => { const { mutateAsync: updateServer, isLoading } = api.settings.updateServer.useMutation(); - const buttonLabel = isNavbar ? "Update available" : "Update server"; + const buttonLabel = isNavbar ? "Update available" : "Update Server"; const handleConfirm = async () => { try { @@ -49,6 +50,7 @@ export const UpdateWebServer = ({ isNavbar }: Props) => { variant={isNavbar ? "outline" : "secondary"} isLoading={isLoading} > + {!isLoading && } {!isLoading && ( diff --git a/apps/dokploy/components/layouts/navbar.tsx b/apps/dokploy/components/layouts/navbar.tsx index 3d17b3e9..1a7da0ea 100644 --- a/apps/dokploy/components/layouts/navbar.tsx +++ b/apps/dokploy/components/layouts/navbar.tsx @@ -12,11 +12,11 @@ import { api } from "@/utils/api"; import { HeartIcon } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/router"; +import { useEffect, useRef, useState } from "react"; +import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; import { Logo } from "../shared/logo"; import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"; import { buttonVariants } from "../ui/button"; -import { useEffect, useRef, useState } from "react"; -import { UpdateWebServer } from "../dashboard/settings/web-server/update-webserver"; const AUTO_CHECK_UPDATES_INTERVAL_MINUTES = 15; diff --git a/apps/dokploy/server/api/routers/settings.ts b/apps/dokploy/server/api/routers/settings.ts index 937c93d3..e30cee4a 100644 --- a/apps/dokploy/server/api/routers/settings.ts +++ b/apps/dokploy/server/api/routers/settings.ts @@ -12,6 +12,7 @@ import { } from "@/server/db/schema"; import { removeJob, schedule } from "@/server/utils/backup"; import { + DEFAULT_UPDATE_DATA, IS_CLOUD, canAccessToTraefikFiles, cleanStoppedContainers, @@ -25,6 +26,7 @@ import { findAdminById, findServerById, getDokployImage, + getUpdateData, initializeTraefik, logRotationManager, parseRawConfig, @@ -45,14 +47,12 @@ import { stopService, stopServiceRemote, updateAdmin, - getUpdateData, updateLetsEncryptEmail, updateServerById, updateServerTraefik, writeConfig, writeMainConfig, writeTraefikConfigInPath, - DEFAULT_UPDATE_DATA, } from "@dokploy/server"; import { checkGPUStatus, setupGPUSupport } from "@dokploy/server"; import { generateOpenApiDocument } from "@dokploy/trpc-openapi"; From 6c9b12cee904072e6a46d41b9fc3054ac4b79d21 Mon Sep 17 00:00:00 2001 From: UndefinedPony Date: Sat, 21 Dec 2024 18:33:22 +0100 Subject: [PATCH 51/62] refactor: use dynamic tag for comparing latest tag digest --- packages/server/src/services/settings.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/src/services/settings.ts b/packages/server/src/services/settings.ts index 0d2ec967..0ab9744e 100644 --- a/packages/server/src/services/settings.ts +++ b/packages/server/src/services/settings.ts @@ -72,7 +72,9 @@ export const getUpdateData = async (): Promise => { results: [{ digest: string; name: string }]; }; const { results } = data; - const latestTagDigest = results.find((t) => t.name === "latest")?.digest; + const latestTagDigest = results.find( + (t) => t.name === getDokployImageTag(), + )?.digest; if (!latestTagDigest) { return DEFAULT_UPDATE_DATA; From d08530d4516945a0bc77602fe9d213f8fcda08da Mon Sep 17 00:00:00 2001 From: Nicholas Penree Date: Sat, 21 Dec 2024 12:22:01 -0500 Subject: [PATCH 52/62] feat(updates): clean up light mode --- .../web-server/toggle-auto-check-updates.tsx | 3 +- .../settings/web-server/update-server.tsx | 59 +++++++++++++------ 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx index 5c07d5df..fb3776b1 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/toggle-auto-check-updates.tsx @@ -2,7 +2,7 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { useState } from "react"; -export const ToggleAutoCheckUpdates = () => { +export const ToggleAutoCheckUpdates = ({ disabled }: { disabled: boolean }) => { const [enabled, setEnabled] = useState( localStorage.getItem("enableAutoCheckUpdates") === "true", ); @@ -18,6 +18,7 @@ export const ToggleAutoCheckUpdates = () => { checked={enabled} onCheckedChange={handleToggle} id="autoCheckUpdatesToggle" + disabled={disabled} />