From e4d9fd37b9eaca2fade80910961d3e50f0cbf664 Mon Sep 17 00:00:00 2001 From: Mohammed Imran Date: Mon, 16 Feb 2026 11:57:28 +0530 Subject: [PATCH 01/11] chore: Format and lint codebase with format-and-lint:fix --- apps/dokploy/components/dashboard/project/add-template.tsx | 1 + .../components/dashboard/settings/certificates/utils.ts | 4 ++-- apps/dokploy/components/layouts/side.tsx | 1 + apps/dokploy/components/shared/logo.tsx | 1 + apps/dokploy/components/ui/sidebar.tsx | 2 +- 5 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/dashboard/project/add-template.tsx b/apps/dokploy/components/dashboard/project/add-template.tsx index 72c42da491..9b6613955f 100644 --- a/apps/dokploy/components/dashboard/project/add-template.tsx +++ b/apps/dokploy/components/dashboard/project/add-template.tsx @@ -332,6 +332,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => { viewMode === "detailed" && "border-b", )} > + {/** biome-ignore lint/performance/noImgElement: this is a valid use for img tag */} { // Helper: read ASN.1 length field function readLength(pos: number): { length: number; offset: number } { - // biome-ignore lint/style/noParameterAssign: + // biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation let len = der[pos++]; if (len & 0x80) { const bytes = len & 0x7f; len = 0; for (let i = 0; i < bytes; i++) { - // biome-ignore lint/style/noParameterAssign: + // biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation len = (len << 8) + der[pos++]; } } diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index 4b3354ed84..b236927d5e 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -905,6 +905,7 @@ export default function Page({ children }: Props) { onOpenChange={(open) => { setDefaultOpen(open); + // biome-ignore lint/suspicious/noDocumentCookie: this sets the cookie to keep the sidebar state. document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`; }} style={ diff --git a/apps/dokploy/components/shared/logo.tsx b/apps/dokploy/components/shared/logo.tsx index a1c3acb7e1..2c316e22f3 100644 --- a/apps/dokploy/components/shared/logo.tsx +++ b/apps/dokploy/components/shared/logo.tsx @@ -8,6 +8,7 @@ interface Props { export const Logo = ({ className = "size-14", logoUrl }: Props) => { if (logoUrl) { return ( + // biome-ignore lint/performance/noImgElement: this is for dynamic logo loading Organization Logo Date: Mon, 16 Feb 2026 12:42:35 +0530 Subject: [PATCH 02/11] Adjust version text size and layout in collapsed sidebar --- apps/dokploy/components/layouts/side.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index b236927d5e..41872ee01f 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -1141,7 +1141,7 @@ export default function Page({ children }: Props) {
Version {dokployVersion}
-
+
{dokployVersion}
From ebbbd390652227dd8c14bd439eb5c38ba960ab35 Mon Sep 17 00:00:00 2001 From: Mohammed Imran Date: Mon, 16 Feb 2026 12:59:55 +0530 Subject: [PATCH 03/11] feat(ui): add Vercel-style breadcrumb navigation with project/service switchers - Create AdvanceBreadcrumb component with searchable dropdowns - Add project selector with environment expansion support - Add service selector for quick switching between services - Add environment selector badge for multi-environment projects - Replace BreadcrumbSidebar with new component across all service pages - Update projects page, environment page, and all service detail pages (application, compose, postgres, mysql, mariadb, redis, mongo) --- .../components/dashboard/projects/show.tsx | 8 +- .../components/shared/advance-breadcrumb.tsx | 564 ++++++++++++++++++ .../environment/[environmentId].tsx | 15 +- .../services/application/[applicationId].tsx | 22 +- .../services/compose/[composeId].tsx | 22 +- .../services/mariadb/[mariadbId].tsx | 22 +- .../services/mongo/[mongoId].tsx | 22 +- .../services/mysql/[mysqlId].tsx | 22 +- .../services/postgres/[postgresId].tsx | 22 +- .../services/redis/[redisId].tsx | 22 +- 10 files changed, 611 insertions(+), 130 deletions(-) create mode 100644 apps/dokploy/components/shared/advance-breadcrumb.tsx diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 07ad68144d..a5ac722912 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -13,7 +13,7 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; import { StatusTooltip } from "@/components/shared/status-tooltip"; @@ -169,9 +169,7 @@ export const ShowProjects = () => { return ( <> - + {!isCloud && (
@@ -564,7 +562,7 @@ export const ShowProjects = () => { -
+
Created diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx new file mode 100644 index 0000000000..bdb65aa364 --- /dev/null +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -0,0 +1,564 @@ +import type { ServiceType } from "@dokploy/server/db/schema"; +import { + Check, + ChevronDown, + ChevronRight, + CircuitBoard, + FolderInput, + GlobeIcon, + X, +} from "lucide-react"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { + MariadbIcon, + MongodbIcon, + MysqlIcon, + PostgresqlIcon, + RedisIcon, +} from "@/components/icons/data-tools-icons"; +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 { ScrollArea } from "@/components/ui/scroll-area"; +import { Separator } from "@/components/ui/separator"; +import { SidebarTrigger } from "@/components/ui/sidebar"; +import { api } from "@/utils/api"; + +interface AdvanceBreadcrumbProps { + projectId?: string; + environmentId?: string; + serviceId?: string; + serviceType?: ServiceType; +} + +const getServiceIcon = (type: ServiceType, className = "size-4") => { + const icons: Record = { + application: , + compose: , + postgres: , + mysql: , + mariadb: , + redis: , + mongo: , + }; + + return icons[type]; +}; + +interface ServiceItem { + id: string; + name: string; + type: ServiceType; + appName?: string; +} + +export const AdvanceBreadcrumb = ({ + projectId, + environmentId, + serviceId, +}: AdvanceBreadcrumbProps) => { + const router = useRouter(); + const [projectOpen, setProjectOpen] = useState(false); + const [serviceOpen, setServiceOpen] = useState(false); + const [environmentOpen, setEnvironmentOpen] = useState(false); + const [projectSearch, setProjectSearch] = useState(""); + const [serviceSearch, setServiceSearch] = useState(""); + const [expandedProjectId, setExpandedProjectId] = useState( + null, + ); + + // Fetch all projects + const { data: allProjects } = api.project.all.useQuery(); + + // Fetch current project data + const { data: currentProject } = api.project.one.useQuery( + { projectId: projectId || "" }, + { enabled: !!projectId }, + ); + + // Fetch current environment + const { data: currentEnvironment } = api.environment.one.useQuery( + { environmentId: environmentId || "" }, + { enabled: !!environmentId }, + ); + + // Fetch environments for current project + const { data: projectEnvironments } = api.environment.byProjectId.useQuery( + { projectId: projectId || "" }, + { enabled: !!projectId }, + ); + + // Close dropdowns on escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + setProjectOpen(false); + setServiceOpen(false); + setEnvironmentOpen(false); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + // Extract services from current environment + const services: ServiceItem[] = []; + if (currentEnvironment) { + currentEnvironment.applications?.forEach( + (app: { applicationId: string; name: string; appName: string }) => { + services.push({ + id: app.applicationId, + name: app.name, + type: "application", + appName: app.appName, + }); + }, + ); + currentEnvironment.compose?.forEach( + (comp: { composeId: string; name: string; appName: string }) => { + services.push({ + id: comp.composeId, + name: comp.name, + type: "compose", + appName: comp.appName, + }); + }, + ); + currentEnvironment.postgres?.forEach( + (pg: { postgresId: string; name: string; appName: string }) => { + services.push({ + id: pg.postgresId, + name: pg.name, + type: "postgres", + appName: pg.appName, + }); + }, + ); + currentEnvironment.mysql?.forEach( + (my: { mysqlId: string; name: string; appName: string }) => { + services.push({ + id: my.mysqlId, + name: my.name, + type: "mysql", + appName: my.appName, + }); + }, + ); + currentEnvironment.mariadb?.forEach( + (maria: { mariadbId: string; name: string; appName: string }) => { + services.push({ + id: maria.mariadbId, + name: maria.name, + type: "mariadb", + appName: maria.appName, + }); + }, + ); + currentEnvironment.redis?.forEach( + (red: { redisId: string; name: string; appName: string }) => { + services.push({ + id: red.redisId, + name: red.name, + type: "redis", + appName: red.appName, + }); + }, + ); + currentEnvironment.mongo?.forEach( + (mon: { mongoId: string; name: string; appName: string }) => { + services.push({ + id: mon.mongoId, + name: mon.name, + type: "mongo", + appName: mon.appName, + }); + }, + ); + } + + // Get current service + const currentService = services.find((s) => s.id === serviceId); + + // Navigate to project's default environment + const handleProjectSelect = ( + selectedProjectId: string, + selectedEnvironmentId?: string, + ) => { + const project = allProjects?.find((p) => p.projectId === selectedProjectId); + if (project && project.environments.length > 0) { + // Use provided environment or find production environment or use the first one + const targetEnvId = + selectedEnvironmentId || + project.environments.find((e) => e.name === "production") + ?.environmentId || + project.environments[0]?.environmentId; + + router.push( + `/dashboard/project/${selectedProjectId}/environment/${targetEnvId}`, + ); + } + setProjectOpen(false); + setExpandedProjectId(null); + }; + + // Navigate to environment + const handleEnvironmentSelect = (envId: string) => { + router.push(`/dashboard/project/${projectId}/environment/${envId}`); + setEnvironmentOpen(false); + }; + + // Navigate to service + const handleServiceSelect = (service: ServiceItem) => { + const serviceTypePath = + service.type === "application" ? "application" : service.type; + router.push( + `/dashboard/project/${projectId}/environment/${environmentId}/services/${serviceTypePath}/${service.id}`, + ); + setServiceOpen(false); + }; + + // Filter projects based on search + const filteredProjects = + allProjects?.filter( + (p) => + p.name.toLowerCase().includes(projectSearch.toLowerCase()) || + p.description?.toLowerCase().includes(projectSearch.toLowerCase()), + ) || []; + + // Filter services based on search + const filteredServices = services.filter( + (s) => + s.name.toLowerCase().includes(serviceSearch.toLowerCase()) || + s.appName?.toLowerCase().includes(serviceSearch.toLowerCase()), + ); + + // If we're just on the projects page, show simple breadcrumb + if (!projectId) { + return ( +
+
+ + +
+ + Projects +
+
+
+ ); + } + + return ( +
+
+ + + +
+ {/* Project Selector */} + + + + + + +
+ + + Esc + +
+ + No projects found. + + + {filteredProjects.map((project) => { + const totalServices = project.environments.reduce( + (total, env) => + total + + (env.applications?.length || 0) + + (env.compose?.length || 0) + + (env.postgres?.length || 0) + + (env.mysql?.length || 0) + + (env.mariadb?.length || 0) + + (env.redis?.length || 0) + + (env.mongo?.length || 0), + 0, + ); + const isSelected = project.projectId === projectId; + const isExpanded = + expandedProjectId === project.projectId; + + return ( +
+ { + if (project.environments.length > 1) { + setExpandedProjectId( + isExpanded ? null : project.projectId, + ); + } else { + handleProjectSelect(project.projectId); + } + }} + className="flex items-center justify-between py-3 px-2 cursor-pointer" + > +
+
+ {project.name.slice(0, 2)} +
+
+ + {project.name} + + + {project.environments.length} env + {project.environments.length !== 1 + ? "s" + : ""}{" "} + · {totalServices} service + {totalServices !== 1 ? "s" : ""} + +
+
+
+ {isSelected && ( + + )} + {project.environments.length > 1 && ( + + )} +
+
+ + {/* Expanded environments */} + {isExpanded && ( +
+ {project.environments.map((env) => { + const envServices = + (env.applications?.length || 0) + + (env.compose?.length || 0) + + (env.postgres?.length || 0) + + (env.mysql?.length || 0) + + (env.mariadb?.length || 0) + + (env.redis?.length || 0) + + (env.mongo?.length || 0); + const isEnvSelected = + env.environmentId === environmentId; + + return ( + + handleProjectSelect( + project.projectId, + env.environmentId, + ) + } + className="flex items-center justify-between py-2 px-2 cursor-pointer text-sm" + > +
+

{env.name}

+ + {envServices} service + {envServices !== 1 ? "s" : ""} + +
+ {isEnvSelected && ( + + )} +
+ ); + })} +
+ )} +
+ ); + })} +
+
+
+
+
+
+ + {/* Environment Selector */} + {projectEnvironments && projectEnvironments.length > 1 && ( + + + + + +
+ {projectEnvironments.map((env) => { + const isSelected = env.environmentId === environmentId; + return ( + + ); + })} +
+
+
+ )} + + {projectEnvironments && projectEnvironments.length === 1 && ( +

+ {currentEnvironment?.name || "production"} +

+ )} + + {/* Service Selector - only show when viewing a service */} + {serviceId && currentService && ( + <> + + + + + + + + +
+ + + Esc + +
+ + No services found. + + + {filteredServices.map((service) => { + const isSelected = service.id === serviceId; + return ( + handleServiceSelect(service)} + className="flex items-center justify-between py-3 px-2 cursor-pointer" + > +
+
+ {getServiceIcon(service.type)} +
+
+ + {service.name} + + + {service.type} + +
+
+ {isSelected && ( + + )} +
+ ); + })} +
+
+
+
+
+
+ + {/* Close button to go back to environment */} + + + )} +
+
+
+ ); +}; diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index af901311e2..3fe6b7845c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -44,8 +44,8 @@ import { RedisIcon, } from "@/components/icons/data-tools-icons"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { AlertBlock } from "@/components/shared/alert-block"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { DialogAction } from "@/components/shared/dialog-action"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input"; @@ -865,18 +865,7 @@ const EnvironmentPage = ( return (
- + Environment: {currentEnvironment.name} | {projectData?.name} | Dokploy diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx index 7917bd97c7..2aa3994744 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx @@ -34,7 +34,7 @@ import { DeleteService } from "@/components/dashboard/compose/delete-service"; import { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring"; import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -103,21 +103,11 @@ const Service = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="application" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={applicationId} + serviceType="application" /> <Head> <title> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx index 1d6902c59f..9b479773a8 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx @@ -30,7 +30,7 @@ import { ShowBackups } from "@/components/dashboard/database/backups/show-backup import { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring"; import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -92,21 +92,11 @@ const Service = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="compose" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={composeId} + serviceType="compose" /> <Head> <title> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx index 0a1e8501de..2f45ad0cfd 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx @@ -23,7 +23,7 @@ import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/ import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { MariadbIcon } from "@/components/icons/data-tools-icons"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -74,21 +74,11 @@ const Mariadb = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="mariadb" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={mariadbId} + serviceType="mariadb" /> <div className="flex flex-col gap-4"> <Head> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx index bae83cb2b2..a810ca6076 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx @@ -23,7 +23,7 @@ import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/ import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { MongodbIcon } from "@/components/icons/data-tools-icons"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -73,21 +73,11 @@ const Mongo = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="mongodb" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={mongoId} + serviceType="mongo" /> <Head> <title> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx index ba2b9d8a03..bf7dae0e6c 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx @@ -23,7 +23,7 @@ import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { MysqlIcon } from "@/components/icons/data-tools-icons"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -72,21 +72,11 @@ const MySql = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="mysql" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={mysqlId} + serviceType="mysql" /> <div className="flex flex-col gap-4"> <Head> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx index 1d90e3e133..68356c5dfb 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx @@ -23,7 +23,7 @@ import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres" import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { PostgresqlIcon } from "@/components/icons/data-tools-icons"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -72,21 +72,11 @@ const Postgresql = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="postgres" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={postgresId} + serviceType="postgres" /> <Head> <title> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx index 47eb82a742..c40e76bd3a 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx @@ -22,7 +22,7 @@ import { UpdateRedis } from "@/components/dashboard/redis/update-redis"; import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings"; import { RedisIcon } from "@/components/icons/data-tools-icons"; import { DashboardLayout } from "@/components/layouts/dashboard-layout"; -import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; +import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { StatusTooltip } from "@/components/shared/status-tooltip"; import { Badge } from "@/components/ui/badge"; import { @@ -72,21 +72,11 @@ const Redis = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="redis" /> - <BreadcrumbSidebar - list={[ - { name: "Projects", href: "/dashboard/projects" }, - { - name: data?.environment?.project?.name || "", - href: `/dashboard/project/${projectId}/environment/${environmentId}`, - }, - { - name: data?.environment?.name || "", - dropdownItems: environmentDropdownItems, - }, - { - name: data?.name || "", - }, - ]} + <AdvanceBreadcrumb + projectId={projectId as string} + environmentId={environmentId as string} + serviceId={redisId} + serviceType="redis" /> <Head> <title> From 938b0b4ed397fc047c98ce0648d73eec738efd09 Mon Sep 17 00:00:00 2001 From: Mohammed Imran <mohammedimran86992@gmail.com> Date: Mon, 16 Feb 2026 14:01:00 +0530 Subject: [PATCH 04/11] chore: Reorder and clean up imports, update openapi schema, and improve cache invalidation logic --- apps/dokploy/__test__/setup.ts | 1 + .../traefik/update-traefik-config.tsx | 2 +- .../dashboard/compose/logs/show.tsx | 4 +- .../dashboard/project/add-compose.tsx | 4 +- .../project/advanced-environment-selector.tsx | 2 + .../settings/git/gitea/add-gitea-provider.tsx | 2 +- .../git/gitea/edit-gitea-provider.tsx | 2 +- .../git/gitlab/add-gitlab-provider.tsx | 2 +- .../git/gitlab/edit-gitlab-provider.tsx | 2 +- .../settings/web-server/edit-traefik-env.tsx | 2 +- .../web-server/manage-traefik-ports.tsx | 2 +- .../proprietary/auth/sign-in-with-github.tsx | 2 +- .../proprietary/auth/sign-in-with-google.tsx | 2 +- apps/dokploy/server/api/root.ts | 4 +- apps/dokploy/setup.ts | 3 +- openapi.json | 3003 ++++++++++++++--- packages/server/src/services/ai.ts | 26 +- packages/server/src/services/domain.ts | 2 +- .../server/src/services/preview-deployment.ts | 2 +- 19 files changed, 2589 insertions(+), 480 deletions(-) diff --git a/apps/dokploy/__test__/setup.ts b/apps/dokploy/__test__/setup.ts index 5af01d147d..41510fe720 100644 --- a/apps/dokploy/__test__/setup.ts +++ b/apps/dokploy/__test__/setup.ts @@ -12,6 +12,7 @@ vi.mock("@dokploy/server/db", () => { chain.where = () => chain; chain.values = () => chain; chain.returning = () => Promise.resolve([{}]); + // biome-ignore lint/suspicious/noThenProperty: this is for testing chain.then = undefined; const tableMock = { diff --git a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx index 928949d9f2..21893802bc 100644 --- a/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx +++ b/apps/dokploy/components/dashboard/application/advanced/traefik/update-traefik-config.tsx @@ -7,6 +7,7 @@ import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; import { CodeEditor } from "@/components/shared/code-editor"; import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, @@ -24,7 +25,6 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; -import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { api } from "@/utils/api"; diff --git a/apps/dokploy/components/dashboard/compose/logs/show.tsx b/apps/dokploy/components/dashboard/compose/logs/show.tsx index fbcdf7292f..20c70e7394 100644 --- a/apps/dokploy/components/dashboard/compose/logs/show.tsx +++ b/apps/dokploy/components/dashboard/compose/logs/show.tsx @@ -1,8 +1,8 @@ import { Loader2 } from "lucide-react"; -import { badgeStateColor } from "@/components/dashboard/application/logs/show"; -import { Badge } from "@/components/ui/badge"; import dynamic from "next/dynamic"; import { useEffect, useState } from "react"; +import { badgeStateColor } from "@/components/dashboard/application/logs/show"; +import { Badge } from "@/components/ui/badge"; import { Card, CardContent, diff --git a/apps/dokploy/components/dashboard/project/add-compose.tsx b/apps/dokploy/components/dashboard/project/add-compose.tsx index bb911373fe..0d7e85b51a 100644 --- a/apps/dokploy/components/dashboard/project/add-compose.tsx +++ b/apps/dokploy/components/dashboard/project/add-compose.tsx @@ -79,7 +79,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => { api.compose.create.useMutation(); // Get environment data to extract projectId - const { data: environment } = api.environment.one.useQuery({ environmentId }); + // const { data: environment } = api.environment.one.useQuery({ environmentId }); const hasServers = servers && servers.length > 0; // Show dropdown logic based on cloud environment @@ -117,6 +117,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => { await utils.environment.one.invalidate({ environmentId, }); + // Invalidate the project query to refresh the project data for the advance-breadcrumb + await utils.project.all.invalidate(); }) .catch(() => { toast.error("Error creating the compose"); diff --git a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx index 678928990b..42c49675f5 100644 --- a/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx +++ b/apps/dokploy/components/dashboard/project/advanced-environment-selector.tsx @@ -98,6 +98,8 @@ export const AdvancedEnvironmentSelector = ({ toast.success("Environment created successfully"); utils.environment.byProjectId.invalidate({ projectId }); + // Invalidate the project query to refresh the project data for the advance-breadcrumb + utils.project.all.invalidate(); setIsCreateDialogOpen(false); setName(""); setDescription(""); diff --git a/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx index f474c376d0..aa6b379c81 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitea/add-gitea-provider.tsx @@ -19,9 +19,9 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, - FormDescription, FormLabel, FormMessage, } from "@/components/ui/form"; diff --git a/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx index fe578acced..cc9261a5af 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitea/edit-gitea-provider.tsx @@ -17,9 +17,9 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, - FormDescription, FormLabel, FormMessage, } from "@/components/ui/form"; diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx index 69d9261947..eb805743a8 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/add-gitlab-provider.tsx @@ -19,9 +19,9 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, - FormDescription, FormLabel, FormMessage, } from "@/components/ui/form"; diff --git a/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx b/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx index 394e25281e..3c4bad1129 100644 --- a/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx +++ b/apps/dokploy/components/dashboard/settings/git/gitlab/edit-gitlab-provider.tsx @@ -18,9 +18,9 @@ import { import { Form, FormControl, + FormDescription, FormField, FormItem, - FormDescription, FormLabel, FormMessage, } from "@/components/ui/form"; diff --git a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx index 482b985794..e0a0bca006 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/edit-traefik-env.tsx @@ -1,7 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; -import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation"; import { toast } from "sonner"; import { z } from "zod"; import { AlertBlock } from "@/components/shared/alert-block"; @@ -24,6 +23,7 @@ import { FormLabel, FormMessage, } from "@/components/ui/form"; +import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation"; import { api } from "@/utils/api"; const schema = z.object({ diff --git a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx index 73973ef06a..a30cefb943 100644 --- a/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx +++ b/apps/dokploy/components/dashboard/settings/web-server/manage-traefik-ports.tsx @@ -1,7 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { ArrowRightLeft, Plus, Trash2 } from "lucide-react"; import { useTranslation } from "next-i18next"; -import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation"; import type React from "react"; import { useEffect, useState } from "react"; import { useFieldArray, useForm } from "react-hook-form"; @@ -36,6 +35,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { useHealthCheckAfterMutation } from "@/hooks/use-health-check-after-mutation"; import { api } from "@/utils/api"; interface Props { diff --git a/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx b/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx index 988eeae050..22b0aef811 100644 --- a/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx +++ b/apps/dokploy/components/proprietary/auth/sign-in-with-github.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { toast } from "sonner"; -import { authClient } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; +import { authClient } from "@/lib/auth-client"; export function SignInWithGithub() { const [isLoading, setIsLoading] = useState(false); diff --git a/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx b/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx index bff0e69ab8..e40d8d9b5b 100644 --- a/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx +++ b/apps/dokploy/components/proprietary/auth/sign-in-with-google.tsx @@ -2,8 +2,8 @@ import { useState } from "react"; import { toast } from "sonner"; -import { authClient } from "@/lib/auth-client"; import { Button } from "@/components/ui/button"; +import { authClient } from "@/lib/auth-client"; export function SignInWithGoogle() { const [isLoading, setIsLoading] = useState(false); diff --git a/apps/dokploy/server/api/root.ts b/apps/dokploy/server/api/root.ts index c8b4295fe8..5792e8a276 100644 --- a/apps/dokploy/server/api/root.ts +++ b/apps/dokploy/server/api/root.ts @@ -22,12 +22,12 @@ import { mountRouter } from "./routers/mount"; import { mysqlRouter } from "./routers/mysql"; import { notificationRouter } from "./routers/notification"; import { organizationRouter } from "./routers/organization"; -import { licenseKeyRouter } from "./routers/proprietary/license-key"; -import { ssoRouter } from "./routers/proprietary/sso"; import { portRouter } from "./routers/port"; import { postgresRouter } from "./routers/postgres"; import { previewDeploymentRouter } from "./routers/preview-deployment"; import { projectRouter } from "./routers/project"; +import { licenseKeyRouter } from "./routers/proprietary/license-key"; +import { ssoRouter } from "./routers/proprietary/sso"; import { redirectsRouter } from "./routers/redirects"; import { redisRouter } from "./routers/redis"; import { registryRouter } from "./routers/registry"; diff --git a/apps/dokploy/setup.ts b/apps/dokploy/setup.ts index dc0c0847f0..0f993e14a9 100644 --- a/apps/dokploy/setup.ts +++ b/apps/dokploy/setup.ts @@ -1,8 +1,9 @@ -import { exit } from "node:process"; import { exec } from "node:child_process"; +import { exit } from "node:process"; import { promisify } from "node:util"; const execAsync = promisify(exec); + import { setupDirectories } from "@dokploy/server/setup/config-paths"; import { initializePostgres } from "@dokploy/server/setup/postgres-setup"; import { initializeRedis } from "@dokploy/server/setup/redis-setup"; diff --git a/openapi.json b/openapi.json index 76366bdb2c..d71ac6255d 100644 --- a/openapi.json +++ b/openapi.json @@ -758,7 +758,10 @@ "minLength": 1 }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -841,7 +844,10 @@ "type": "object", "properties": { "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "applicationId": { "type": "string" @@ -1064,9 +1070,12 @@ "buildSecrets": { "type": "string", "nullable": true + }, + "createEnvFile": { + "type": "boolean" } }, - "required": ["applicationId"], + "required": ["applicationId", "createEnvFile"], "additionalProperties": false } } @@ -1370,6 +1379,10 @@ "type": "string", "nullable": true }, + "bitbucketRepositorySlug": { + "type": "string", + "nullable": true + }, "bitbucketId": { "type": "string", "nullable": true @@ -1393,6 +1406,7 @@ "bitbucketBuildPath", "bitbucketOwner", "bitbucketRepository", + "bitbucketRepositorySlug", "bitbucketId", "applicationId", "enableSubmodules" @@ -1721,7 +1735,10 @@ "minLength": 1 }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -1836,6 +1853,13 @@ "type": "string", "nullable": true }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "refreshToken": { "type": "string", "nullable": true @@ -1925,6 +1949,10 @@ "type": "string", "nullable": true }, + "bitbucketRepositorySlug": { + "type": "string", + "nullable": true + }, "bitbucketOwner": { "type": "string", "nullable": true @@ -2197,8 +2225,9 @@ }, "DriverOpts": { "type": "object", - "properties": {}, - "additionalProperties": false + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -2240,6 +2269,29 @@ "additionalProperties": false, "nullable": true }, + "ulimitsSwarm": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 1 + }, + "Soft": { + "type": "integer", + "minimum": -1 + }, + "Hard": { + "type": "integer", + "minimum": -1 + } + }, + "required": ["Name", "Soft", "Hard"], + "additionalProperties": false + }, + "nullable": true + }, "replicas": { "type": "number" }, @@ -2274,6 +2326,9 @@ "type": "boolean", "nullable": true }, + "createEnvFile": { + "type": "boolean" + }, "createdAt": { "type": "string" }, @@ -2281,6 +2336,10 @@ "type": "string", "nullable": true }, + "rollbackRegistryId": { + "type": "string", + "nullable": true + }, "environmentId": { "type": "string" }, @@ -2586,7 +2645,10 @@ "in": "query", "required": true, "schema": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" } } ], @@ -2708,7 +2770,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "dockerImage": { "type": "string", @@ -2744,12 +2808,10 @@ }, "required": [ "name", - "appName", "environmentId", "databaseName", "databaseUser", - "databasePassword", - "databaseRootPassword" + "databasePassword" ], "additionalProperties": false } @@ -3031,7 +3093,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" } }, "required": ["mysqlId", "appName"], @@ -3164,7 +3228,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -3194,6 +3260,13 @@ "type": "string", "nullable": true }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "env": { "type": "string", "nullable": true @@ -3431,8 +3504,9 @@ }, "DriverOpts": { "type": "object", - "properties": {}, - "additionalProperties": false + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -3474,6 +3548,29 @@ "additionalProperties": false, "nullable": true }, + "ulimitsSwarm": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 1 + }, + "Soft": { + "type": "integer", + "minimum": -1 + }, + "Hard": { + "type": "integer", + "minimum": -1 + } + }, + "required": ["Name", "Soft", "Hard"], + "additionalProperties": false + }, + "nullable": true + }, "replicas": { "type": "number" }, @@ -3608,7 +3705,10 @@ "minLength": 1 }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "databaseName": { "type": "string", @@ -3624,7 +3724,7 @@ }, "dockerImage": { "type": "string", - "default": "postgres:15" + "default": "postgres:18" }, "environmentId": { "type": "string" @@ -3640,7 +3740,6 @@ }, "required": [ "name", - "appName", "databaseName", "databaseUser", "databasePassword", @@ -4009,7 +4108,10 @@ "type": "string" }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" } }, "required": ["postgresId", "appName"], @@ -4057,7 +4159,10 @@ "minLength": 1 }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "databaseName": { "type": "string", @@ -4077,12 +4182,19 @@ }, "dockerImage": { "type": "string", - "default": "postgres:15" + "default": "postgres:18" }, "command": { "type": "string", "nullable": true }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "env": { "type": "string", "nullable": true @@ -4320,8 +4432,9 @@ }, "DriverOpts": { "type": "object", - "properties": {}, - "additionalProperties": false + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -4363,6 +4476,29 @@ "additionalProperties": false, "nullable": true }, + "ulimitsSwarm": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 1 + }, + "Soft": { + "type": "integer", + "minimum": -1 + }, + "Hard": { + "type": "integer", + "minimum": -1 + } + }, + "required": ["Name", "Soft", "Hard"], + "additionalProperties": false + }, + "nullable": true + }, "replicas": { "type": "number" }, @@ -4498,7 +4634,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "databasePassword": { "type": "string" @@ -4519,12 +4657,7 @@ "nullable": true } }, - "required": [ - "name", - "appName", - "databasePassword", - "environmentId" - ], + "required": ["name", "databasePassword", "environmentId"], "additionalProperties": false } } @@ -4637,7 +4770,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" } }, "required": ["redisId", "appName"], @@ -4938,7 +5073,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -4955,6 +5092,13 @@ "type": "string", "nullable": true }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "env": { "type": "string", "nullable": true @@ -5195,8 +5339,9 @@ }, "DriverOpts": { "type": "object", - "properties": {}, - "additionalProperties": false + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -5238,6 +5383,29 @@ "additionalProperties": false, "nullable": true }, + "ulimitsSwarm": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 1 + }, + "Soft": { + "type": "integer", + "minimum": -1 + }, + "Hard": { + "type": "integer", + "minimum": -1 + } + }, + "required": ["Name", "Soft", "Hard"], + "additionalProperties": false + }, + "nullable": true + }, "replicas": { "type": "number" }, @@ -5370,7 +5538,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "dockerImage": { "type": "string", @@ -5403,7 +5573,6 @@ }, "required": [ "name", - "appName", "environmentId", "databaseUser", "databasePassword" @@ -5688,7 +5857,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" } }, "required": ["mongoId", "appName"], @@ -5821,7 +5992,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -5843,6 +6016,13 @@ "type": "string", "nullable": true }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "env": { "type": "string", "nullable": true @@ -6080,8 +6260,9 @@ }, "DriverOpts": { "type": "object", - "properties": {}, - "additionalProperties": false + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -6123,6 +6304,29 @@ "additionalProperties": false, "nullable": true }, + "ulimitsSwarm": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 1 + }, + "Soft": { + "type": "integer", + "minimum": -1 + }, + "Hard": { + "type": "integer", + "minimum": -1 + } + }, + "required": ["Name", "Soft", "Hard"], + "additionalProperties": false + }, + "nullable": true + }, "replicas": { "type": "number" }, @@ -6263,7 +6467,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "dockerImage": { "type": "string", @@ -6299,8 +6505,6 @@ }, "required": [ "name", - "appName", - "databaseRootPassword", "environmentId", "databaseName", "databaseUser", @@ -6670,7 +6874,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" } }, "required": ["mariadbId", "appName"], @@ -6719,7 +6925,9 @@ }, "appName": { "type": "string", - "minLength": 1 + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -6749,6 +6957,13 @@ "type": "string", "nullable": true }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true + }, "env": { "type": "string", "nullable": true @@ -6986,8 +7201,9 @@ }, "DriverOpts": { "type": "object", - "properties": {}, - "additionalProperties": false + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -7029,6 +7245,29 @@ "additionalProperties": false, "nullable": true }, + "ulimitsSwarm": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Name": { + "type": "string", + "minLength": 1 + }, + "Soft": { + "type": "integer", + "minimum": -1 + }, + "Hard": { + "type": "integer", + "minimum": -1 + } + }, + "required": ["Name", "Soft", "Hard"], + "additionalProperties": false + }, + "nullable": true + }, "replicas": { "type": "number" }, @@ -7174,7 +7413,10 @@ "enum": ["docker-compose", "stack"] }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "serverId": { "type": "string", @@ -7261,7 +7503,10 @@ "minLength": 1 }, "appName": { - "type": "string" + "type": "string", + "minLength": 1, + "maxLength": 63, + "pattern": "^[a-zA-Z0-9._-]+$" }, "description": { "type": "string", @@ -7333,6 +7578,10 @@ "type": "string", "nullable": true }, + "bitbucketRepositorySlug": { + "type": "string", + "nullable": true + }, "bitbucketOwner": { "type": "string", "nullable": true @@ -8569,7 +8818,10 @@ "type": "string", "minLength": 1 }, - "name": { + "firstName": { + "type": "string" + }, + "lastName": { "type": "string" }, "isRegistered": { @@ -8619,151 +8871,32 @@ "type": "string", "format": "date-time" }, - "serverIp": { - "type": "string", - "nullable": true - }, - "certificateType": { - "type": "string", - "enum": ["letsencrypt", "none", "custom"] + "enablePaidFeatures": { + "type": "boolean" }, - "https": { + "allowImpersonation": { "type": "boolean" }, - "host": { - "type": "string", - "nullable": true + "enableEnterpriseFeatures": { + "type": "boolean" }, - "letsEncryptEmail": { + "licenseKey": { "type": "string", "nullable": true }, - "sshPrivateKey": { + "stripeCustomerId": { "type": "string", "nullable": true }, - "enableDockerCleanup": { - "type": "boolean" - }, - "logCleanupCron": { + "stripeSubscriptionId": { "type": "string", "nullable": true }, - "enablePaidFeatures": { - "type": "boolean" + "serversQuantity": { + "type": "number" }, - "allowImpersonation": { - "type": "boolean" - }, - "metricsConfig": { - "type": "object", - "properties": { - "server": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["Dokploy", "Remote"] - }, - "refreshRate": { - "type": "number" - }, - "port": { - "type": "number" - }, - "token": { - "type": "string" - }, - "urlCallback": { - "type": "string" - }, - "retentionDays": { - "type": "number" - }, - "cronJob": { - "type": "string" - }, - "thresholds": { - "type": "object", - "properties": { - "cpu": { - "type": "number" - }, - "memory": { - "type": "number" - } - }, - "required": ["cpu", "memory"], - "additionalProperties": false - } - }, - "required": [ - "type", - "refreshRate", - "port", - "token", - "urlCallback", - "retentionDays", - "cronJob", - "thresholds" - ], - "additionalProperties": false - }, - "containers": { - "type": "object", - "properties": { - "refreshRate": { - "type": "number" - }, - "services": { - "type": "object", - "properties": { - "include": { - "type": "array", - "items": { - "type": "string" - } - }, - "exclude": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["include", "exclude"], - "additionalProperties": false - } - }, - "required": ["refreshRate", "services"], - "additionalProperties": false - } - }, - "required": ["server", "containers"], - "additionalProperties": false - }, - "cleanupCacheApplications": { - "type": "boolean" - }, - "cleanupCacheOnPreviews": { - "type": "boolean" - }, - "cleanupCacheOnCompose": { - "type": "boolean" - }, - "stripeCustomerId": { - "type": "string", - "nullable": true - }, - "stripeSubscriptionId": { - "type": "string", - "nullable": true - }, - "serversQuantity": { - "type": "number" - }, - "password": { - "type": "string" + "password": { + "type": "string" }, "currentPassword": { "type": "string" @@ -10878,6 +11011,52 @@ } } }, + "/previewDeployment.redeploy": { + "post": { + "operationId": "previewDeployment-redeploy", + "tags": ["previewDeployment"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "previewDeploymentId": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["previewDeploymentId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/mounts.create": { "post": { "operationId": "mounts-create", @@ -11335,6 +11514,29 @@ } } }, + "/settings.getWebServerSettings": { + "get": { + "operationId": "settings-getWebServerSettings", + "tags": ["settings"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/settings.reloadServer": { "post": { "operationId": "settings-reloadServer", @@ -11404,6 +11606,29 @@ } } }, + "/settings.cleanAllDeploymentQueue": { + "post": { + "operationId": "settings-cleanAllDeploymentQueue", + "tags": ["settings"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/settings.reloadTraefik": { "post": { "operationId": "settings-reloadTraefik", @@ -11759,8 +11984,7 @@ "type": "object", "properties": { "sshPrivateKey": { - "type": "string", - "nullable": true + "type": "string" } }, "required": ["sshPrivateKey"], @@ -11800,15 +12024,23 @@ "type": "object", "properties": { "host": { - "type": "string", - "nullable": true + "type": "string" }, "certificateType": { "type": "string", "enum": ["letsencrypt", "none", "custom"] }, "letsEncryptEmail": { - "type": "string", + "anyOf": [ + { + "type": "string", + "format": "email" + }, + { + "type": "string", + "enum": [""] + } + ], "nullable": true }, "https": { @@ -12329,6 +12561,46 @@ } } }, + "/settings.updateServerIp": { + "post": { + "operationId": "settings-updateServerIp", + "tags": ["settings"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "serverIp": { + "type": "string" + } + }, + "required": ["serverIp"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/settings.getOpenApiDocument": { "get": { "operationId": "settings-getOpenApiDocument", @@ -13690,26 +13962,69 @@ } } }, - "/cluster.getNodes": { - "get": { - "operationId": "cluster-getNodes", - "tags": ["cluster"], + "/registry.testRegistryById": { + "post": { + "operationId": "registry-testRegistryById", + "tags": ["registry"], "security": [ { "Authorization": [] } ], - "parameters": [ - { - "name": "serverId", - "in": "query", - "required": false, - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "registryId": { + "type": "string", + "minLength": 1 + }, + "serverId": { + "type": "string" + } + }, + "additionalProperties": false + } } } - ], - "responses": { + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/cluster.getNodes": { + "get": { + "operationId": "cluster-getNodes", + "tags": ["cluster"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "serverId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { "200": { "description": "Successful response", "content": { @@ -13851,6 +14166,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -13877,6 +14195,7 @@ "required": [ "appBuildError", "databaseBackup", + "volumeBackup", "dokployRestart", "name", "appDeploy", @@ -13926,6 +14245,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14045,6 +14367,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14075,6 +14400,7 @@ "required": [ "appBuildError", "databaseBackup", + "volumeBackup", "dokployRestart", "name", "appDeploy", @@ -14125,6 +14451,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14253,6 +14582,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14279,6 +14611,7 @@ "required": [ "appBuildError", "databaseBackup", + "volumeBackup", "dokployRestart", "name", "appDeploy", @@ -14328,6 +14661,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14448,6 +14784,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14494,6 +14833,7 @@ "required": [ "appBuildError", "databaseBackup", + "volumeBackup", "dokployRestart", "name", "appDeploy", @@ -14547,6 +14887,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14692,9 +15035,9 @@ } } }, - "/notification.remove": { + "/notification.createResend": { "post": { - "operationId": "notification-remove", + "operationId": "notification-createResend", "tags": ["notification"], "security": [ { @@ -14708,11 +15051,59 @@ "schema": { "type": "object", "properties": { - "notificationId": { + "appBuildError": { + "type": "boolean" + }, + "databaseBackup": { + "type": "boolean" + }, + "volumeBackup": { + "type": "boolean" + }, + "dokployRestart": { + "type": "boolean" + }, + "name": { "type": "string" + }, + "appDeploy": { + "type": "boolean" + }, + "dockerCleanup": { + "type": "boolean" + }, + "serverThreshold": { + "type": "boolean" + }, + "apiKey": { + "type": "string", + "minLength": 1 + }, + "fromAddress": { + "type": "string", + "minLength": 1 + }, + "toAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 } }, - "required": ["notificationId"], + "required": [ + "appBuildError", + "databaseBackup", + "volumeBackup", + "dokployRestart", + "name", + "appDeploy", + "dockerCleanup", + "serverThreshold", + "apiKey", + "fromAddress", + "toAddresses" + ], "additionalProperties": false } } @@ -14732,25 +15123,80 @@ } } }, - "/notification.one": { - "get": { - "operationId": "notification-one", + "/notification.updateResend": { + "post": { + "operationId": "notification-updateResend", "tags": ["notification"], "security": [ { "Authorization": [] } ], - "parameters": [ - { - "name": "notificationId", - "in": "query", - "required": true, - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appBuildError": { + "type": "boolean" + }, + "databaseBackup": { + "type": "boolean" + }, + "volumeBackup": { + "type": "boolean" + }, + "dokployRestart": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "appDeploy": { + "type": "boolean" + }, + "dockerCleanup": { + "type": "boolean" + }, + "serverThreshold": { + "type": "boolean" + }, + "apiKey": { + "type": "string", + "minLength": 1 + }, + "fromAddress": { + "type": "string", + "minLength": 1 + }, + "toAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "notificationId": { + "type": "string", + "minLength": 1 + }, + "resendId": { + "type": "string", + "minLength": 1 + }, + "organizationId": { + "type": "string" + } + }, + "required": ["notificationId", "resendId"], + "additionalProperties": false + } } } - ], + }, + "parameters": [], "responses": { "200": { "description": "Successful response", @@ -14764,15 +15210,44 @@ } } }, - "/notification.all": { - "get": { - "operationId": "notification-all", + "/notification.testResendConnection": { + "post": { + "operationId": "notification-testResendConnection", "tags": ["notification"], "security": [ { "Authorization": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "apiKey": { + "type": "string", + "minLength": 1 + }, + "fromAddress": { + "type": "string", + "minLength": 1 + }, + "toAddresses": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + } + }, + "required": ["apiKey", "fromAddress", "toAddresses"], + "additionalProperties": false + } + } + } + }, "parameters": [], "responses": { "200": { @@ -14787,9 +15262,9 @@ } } }, - "/notification.receiveNotification": { + "/notification.remove": { "post": { - "operationId": "notification-receiveNotification", + "operationId": "notification-remove", "tags": ["notification"], "security": [ { @@ -14803,39 +15278,11 @@ "schema": { "type": "object", "properties": { - "ServerType": { - "type": "string", - "enum": ["Dokploy", "Remote"], - "default": "Dokploy" - }, - "Type": { - "type": "string", - "enum": ["Memory", "CPU"] - }, - "Value": { - "type": "number" - }, - "Threshold": { - "type": "number" - }, - "Message": { - "type": "string" - }, - "Timestamp": { - "type": "string" - }, - "Token": { + "notificationId": { "type": "string" } }, - "required": [ - "Type", - "Value", - "Threshold", - "Message", - "Timestamp", - "Token" - ], + "required": ["notificationId"], "additionalProperties": false } } @@ -14855,28 +15302,154 @@ } } }, - "/notification.createGotify": { - "post": { - "operationId": "notification-createGotify", + "/notification.one": { + "get": { + "operationId": "notification-one", "tags": ["notification"], "security": [ { "Authorization": [] } ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "appBuildError": { + "parameters": [ + { + "name": "notificationId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/notification.all": { + "get": { + "operationId": "notification-all", + "tags": ["notification"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/notification.receiveNotification": { + "post": { + "operationId": "notification-receiveNotification", + "tags": ["notification"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ServerType": { + "type": "string", + "enum": ["Dokploy", "Remote"], + "default": "Dokploy" + }, + "Type": { + "type": "string", + "enum": ["Memory", "CPU"] + }, + "Value": { + "type": "number" + }, + "Threshold": { + "type": "number" + }, + "Message": { + "type": "string" + }, + "Timestamp": { + "type": "string" + }, + "Token": { + "type": "string" + } + }, + "required": [ + "Type", + "Value", + "Threshold", + "Message", + "Timestamp", + "Token" + ], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/notification.createGotify": { + "post": { + "operationId": "notification-createGotify", + "tags": ["notification"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appBuildError": { "type": "boolean" }, "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -14908,6 +15481,7 @@ "required": [ "appBuildError", "databaseBackup", + "volumeBackup", "dokployRestart", "name", "appDeploy", @@ -14958,6 +15532,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -15091,6 +15668,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -15112,8 +15692,7 @@ "minLength": 1 }, "accessToken": { - "type": "string", - "minLength": 1 + "type": "string" }, "priority": { "type": "number", @@ -15123,6 +15702,7 @@ "required": [ "appBuildError", "databaseBackup", + "volumeBackup", "dokployRestart", "name", "appDeploy", @@ -15173,6 +15753,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -15194,8 +15777,7 @@ "minLength": 1 }, "accessToken": { - "type": "string", - "minLength": 1 + "type": "string" }, "priority": { "type": "number", @@ -15258,8 +15840,7 @@ "minLength": 1 }, "accessToken": { - "type": "string", - "minLength": 1 + "type": "string" }, "priority": { "type": "number", @@ -15286,9 +15867,9 @@ } } }, - "/notification.createLark": { + "/notification.createCustom": { "post": { - "operationId": "notification-createLark", + "operationId": "notification-createCustom", "tags": ["notification"], "security": [ { @@ -15308,6 +15889,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -15323,21 +15907,18 @@ "serverThreshold": { "type": "boolean" }, - "webhookUrl": { + "endpoint": { "type": "string", "minLength": 1 + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, - "required": [ - "appBuildError", - "databaseBackup", - "dokployRestart", - "name", - "appDeploy", - "dockerCleanup", - "serverThreshold", - "webhookUrl" - ], + "required": ["name", "endpoint"], "additionalProperties": false } } @@ -15357,9 +15938,9 @@ } } }, - "/notification.updateLark": { + "/notification.updateCustom": { "post": { - "operationId": "notification-updateLark", + "operationId": "notification-updateCustom", "tags": ["notification"], "security": [ { @@ -15379,6 +15960,9 @@ "databaseBackup": { "type": "boolean" }, + "volumeBackup": { + "type": "boolean" + }, "dokployRestart": { "type": "boolean" }, @@ -15394,15 +15978,21 @@ "serverThreshold": { "type": "boolean" }, - "webhookUrl": { + "endpoint": { "type": "string", "minLength": 1 }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "notificationId": { "type": "string", "minLength": 1 }, - "larkId": { + "customId": { "type": "string", "minLength": 1 }, @@ -15410,7 +16000,7 @@ "type": "string" } }, - "required": ["notificationId", "larkId"], + "required": ["notificationId", "customId"], "additionalProperties": false } } @@ -15430,9 +16020,9 @@ } } }, - "/notification.testLarkConnection": { + "/notification.testCustomConnection": { "post": { - "operationId": "notification-testLarkConnection", + "operationId": "notification-testCustomConnection", "tags": ["notification"], "security": [ { @@ -15446,12 +16036,18 @@ "schema": { "type": "object", "properties": { - "webhookUrl": { + "endpoint": { "type": "string", "minLength": 1 + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + } } }, - "required": ["webhookUrl"], + "required": ["endpoint"], "additionalProperties": false } } @@ -15471,15 +16067,67 @@ } } }, - "/notification.getEmailProviders": { - "get": { - "operationId": "notification-getEmailProviders", + "/notification.createLark": { + "post": { + "operationId": "notification-createLark", "tags": ["notification"], "security": [ { "Authorization": [] } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appBuildError": { + "type": "boolean" + }, + "databaseBackup": { + "type": "boolean" + }, + "volumeBackup": { + "type": "boolean" + }, + "dokployRestart": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "appDeploy": { + "type": "boolean" + }, + "dockerCleanup": { + "type": "boolean" + }, + "serverThreshold": { + "type": "boolean" + }, + "webhookUrl": { + "type": "string", + "minLength": 1 + } + }, + "required": [ + "appBuildError", + "databaseBackup", + "volumeBackup", + "dokployRestart", + "name", + "appDeploy", + "dockerCleanup", + "serverThreshold", + "webhookUrl" + ], + "additionalProperties": false + } + } + } + }, "parameters": [], "responses": { "200": { @@ -15494,10 +16142,10 @@ } } }, - "/sshKey.create": { + "/notification.updateLark": { "post": { - "operationId": "sshKey-create", - "tags": ["sshKey"], + "operationId": "notification-updateLark", + "tags": ["notification"], "security": [ { "Authorization": [] @@ -15510,30 +16158,47 @@ "schema": { "type": "object", "properties": { - "name": { - "type": "string", - "minLength": 1 + "appBuildError": { + "type": "boolean" }, - "description": { - "type": "string", - "nullable": true + "databaseBackup": { + "type": "boolean" }, - "privateKey": { - "type": "string" + "volumeBackup": { + "type": "boolean" }, - "publicKey": { + "dokployRestart": { + "type": "boolean" + }, + "name": { "type": "string" }, + "appDeploy": { + "type": "boolean" + }, + "dockerCleanup": { + "type": "boolean" + }, + "serverThreshold": { + "type": "boolean" + }, + "webhookUrl": { + "type": "string", + "minLength": 1 + }, + "notificationId": { + "type": "string", + "minLength": 1 + }, + "larkId": { + "type": "string", + "minLength": 1 + }, "organizationId": { "type": "string" } }, - "required": [ - "name", - "privateKey", - "publicKey", - "organizationId" - ], + "required": ["notificationId", "larkId"], "additionalProperties": false } } @@ -15553,10 +16218,10 @@ } } }, - "/sshKey.remove": { + "/notification.testLarkConnection": { "post": { - "operationId": "sshKey-remove", - "tags": ["sshKey"], + "operationId": "notification-testLarkConnection", + "tags": ["notification"], "security": [ { "Authorization": [] @@ -15569,11 +16234,12 @@ "schema": { "type": "object", "properties": { - "sshKeyId": { - "type": "string" + "webhookUrl": { + "type": "string", + "minLength": 1 } }, - "required": ["sshKeyId"], + "required": ["webhookUrl"], "additionalProperties": false } } @@ -15593,47 +16259,78 @@ } } }, - "/sshKey.one": { - "get": { - "operationId": "sshKey-one", - "tags": ["sshKey"], + "/notification.createPushover": { + "post": { + "operationId": "notification-createPushover", + "tags": ["notification"], "security": [ { "Authorization": [] } ], - "parameters": [ - { - "name": "sshKeyId", - "in": "query", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": {} + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appBuildError": { + "type": "boolean" + }, + "databaseBackup": { + "type": "boolean" + }, + "volumeBackup": { + "type": "boolean" + }, + "dokployRestart": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "appDeploy": { + "type": "boolean" + }, + "dockerCleanup": { + "type": "boolean" + }, + "serverThreshold": { + "type": "boolean" + }, + "userKey": { + "type": "string", + "minLength": 1 + }, + "apiToken": { + "type": "string", + "minLength": 1 + }, + "priority": { + "type": "number", + "minimum": -2, + "maximum": 2, + "default": 0 + }, + "retry": { + "type": "number", + "minimum": 30, + "nullable": true + }, + "expire": { + "type": "number", + "minimum": 1, + "maximum": 10800, + "nullable": true + } + }, + "required": ["name", "userKey", "apiToken"], + "additionalProperties": false + } } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } - }, - "/sshKey.all": { - "get": { - "operationId": "sshKey-all", - "tags": ["sshKey"], - "security": [ - { - "Authorization": [] } - ], + }, "parameters": [], "responses": { "200": { @@ -15648,10 +16345,10 @@ } } }, - "/sshKey.generate": { + "/notification.updatePushover": { "post": { - "operationId": "sshKey-generate", - "tags": ["sshKey"], + "operationId": "notification-updatePushover", + "tags": ["notification"], "security": [ { "Authorization": [] @@ -15664,11 +16361,67 @@ "schema": { "type": "object", "properties": { - "type": { + "notificationId": { "type": "string", - "enum": ["rsa", "ed25519"] + "minLength": 1 + }, + "pushoverId": { + "type": "string", + "minLength": 1 + }, + "organizationId": { + "type": "string" + }, + "userKey": { + "type": "string", + "minLength": 1 + }, + "apiToken": { + "type": "string", + "minLength": 1 + }, + "priority": { + "type": "number", + "minimum": -2, + "maximum": 2 + }, + "retry": { + "type": "number", + "minimum": 30, + "nullable": true + }, + "expire": { + "type": "number", + "minimum": 1, + "maximum": 10800, + "nullable": true + }, + "appBuildError": { + "type": "boolean" + }, + "databaseBackup": { + "type": "boolean" + }, + "volumeBackup": { + "type": "boolean" + }, + "dokployRestart": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "appDeploy": { + "type": "boolean" + }, + "dockerCleanup": { + "type": "boolean" + }, + "serverThreshold": { + "type": "boolean" } }, + "required": ["notificationId", "pushoverId"], "additionalProperties": false } } @@ -15688,10 +16441,10 @@ } } }, - "/sshKey.update": { + "/notification.testPushoverConnection": { "post": { - "operationId": "sshKey-update", - "tags": ["sshKey"], + "operationId": "notification-testPushoverConnection", + "tags": ["notification"], "security": [ { "Authorization": [] @@ -15704,23 +16457,32 @@ "schema": { "type": "object", "properties": { - "name": { + "userKey": { "type": "string", "minLength": 1 }, - "description": { + "apiToken": { "type": "string", - "nullable": true + "minLength": 1 }, - "lastUsedAt": { - "type": "string", + "priority": { + "type": "number", + "minimum": -2, + "maximum": 2 + }, + "retry": { + "type": "number", + "minimum": 30, "nullable": true }, - "sshKeyId": { - "type": "string" + "expire": { + "type": "number", + "minimum": 1, + "maximum": 10800, + "nullable": true } }, - "required": ["sshKeyId"], + "required": ["userKey", "apiToken", "priority"], "additionalProperties": false } } @@ -15740,10 +16502,10 @@ } } }, - "/gitProvider.getAll": { + "/notification.getEmailProviders": { "get": { - "operationId": "gitProvider-getAll", - "tags": ["gitProvider"], + "operationId": "notification-getEmailProviders", + "tags": ["notification"], "security": [ { "Authorization": [] @@ -15763,10 +16525,10 @@ } } }, - "/gitProvider.remove": { + "/sshKey.create": { "post": { - "operationId": "gitProvider-remove", - "tags": ["gitProvider"], + "operationId": "sshKey-create", + "tags": ["sshKey"], "security": [ { "Authorization": [] @@ -15779,12 +16541,30 @@ "schema": { "type": "object", "properties": { - "gitProviderId": { + "name": { "type": "string", "minLength": 1 + }, + "description": { + "type": "string", + "nullable": true + }, + "privateKey": { + "type": "string" + }, + "publicKey": { + "type": "string" + }, + "organizationId": { + "type": "string" } }, - "required": ["gitProviderId"], + "required": [ + "name", + "privateKey", + "publicKey", + "organizationId" + ], "additionalProperties": false } } @@ -15804,9 +16584,260 @@ } } }, - "/gitea.create": { + "/sshKey.remove": { "post": { - "operationId": "gitea-create", + "operationId": "sshKey-remove", + "tags": ["sshKey"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sshKeyId": { + "type": "string" + } + }, + "required": ["sshKeyId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sshKey.one": { + "get": { + "operationId": "sshKey-one", + "tags": ["sshKey"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "sshKeyId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sshKey.all": { + "get": { + "operationId": "sshKey-all", + "tags": ["sshKey"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sshKey.generate": { + "post": { + "operationId": "sshKey-generate", + "tags": ["sshKey"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["rsa", "ed25519"] + } + }, + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sshKey.update": { + "post": { + "operationId": "sshKey-update", + "tags": ["sshKey"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "nullable": true + }, + "lastUsedAt": { + "type": "string", + "nullable": true + }, + "sshKeyId": { + "type": "string" + } + }, + "required": ["sshKeyId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/gitProvider.getAll": { + "get": { + "operationId": "gitProvider-getAll", + "tags": ["gitProvider"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/gitProvider.remove": { + "post": { + "operationId": "gitProvider-remove", + "tags": ["gitProvider"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "gitProviderId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["gitProviderId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/gitea.create": { + "post": { + "operationId": "gitea-create", "tags": ["gitea"], "security": [ { @@ -15827,6 +16858,10 @@ "type": "string", "minLength": 1 }, + "giteaInternalUrl": { + "type": "string", + "nullable": true + }, "redirectUri": { "type": "string" }, @@ -16090,7 +17125,11 @@ "type": "string", "minLength": 1 }, - "redirectUri": { + "giteaInternalUrl": { + "type": "string", + "nullable": true + }, + "redirectUri": { "type": "string" }, "clientId": { @@ -16532,6 +17571,10 @@ "type": "string", "minLength": 1 }, + "gitlabInternalUrl": { + "type": "string", + "nullable": true + }, "applicationId": { "type": "string" }, @@ -16799,6 +17842,10 @@ "type": "string", "minLength": 1 }, + "gitlabInternalUrl": { + "type": "string", + "nullable": true + }, "applicationId": { "type": "string" }, @@ -17905,6 +18952,29 @@ } } }, + "/stripe.getInvoices": { + "get": { + "operationId": "stripe-getInvoices", + "tags": ["stripe"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, "/swarm.getNodes": { "get": { "operationId": "swarm-getNodes", @@ -18485,22 +19555,820 @@ "content": { "application/json": {} } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } - }, - "/organization.all": { - "get": { - "operationId": "organization-all", - "tags": ["organization"], - "security": [ - { - "Authorization": [] + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.all": { + "get": { + "operationId": "organization-all", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.one": { + "get": { + "operationId": "organization-one", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "organizationId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.update": { + "post": { + "operationId": "organization-update", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "logo": { + "type": "string" + } + }, + "required": ["organizationId", "name"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.delete": { + "post": { + "operationId": "organization-delete", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { + "type": "string" + } + }, + "required": ["organizationId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.allInvitations": { + "get": { + "operationId": "organization-allInvitations", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.removeInvitation": { + "post": { + "operationId": "organization-removeInvitation", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "invitationId": { + "type": "string" + } + }, + "required": ["invitationId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.updateMemberRole": { + "post": { + "operationId": "organization-updateMemberRole", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "memberId": { + "type": "string" + }, + "role": { + "type": "string", + "enum": ["admin", "member"] + } + }, + "required": ["memberId", "role"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/organization.setDefault": { + "post": { + "operationId": "organization-setDefault", + "tags": ["organization"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["organizationId"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/licenseKey.activate": { + "post": { + "operationId": "licenseKey-activate", + "tags": ["licenseKey"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "licenseKey": { + "type": "string", + "minLength": 1 + } + }, + "required": ["licenseKey"], + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/licenseKey.validate": { + "post": { + "operationId": "licenseKey-validate", + "tags": ["licenseKey"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/licenseKey.deactivate": { + "post": { + "operationId": "licenseKey-deactivate", + "tags": ["licenseKey"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/licenseKey.getEnterpriseSettings": { + "get": { + "operationId": "licenseKey-getEnterpriseSettings", + "tags": ["licenseKey"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/licenseKey.haveValidLicenseKey": { + "get": { + "operationId": "licenseKey-haveValidLicenseKey", + "tags": ["licenseKey"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/licenseKey.updateEnterpriseSettings": { + "post": { + "operationId": "licenseKey-updateEnterpriseSettings", + "tags": ["licenseKey"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "enableEnterpriseFeatures": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + }, + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sso.showSignInWithSSO": { + "get": { + "operationId": "sso-showSignInWithSSO", + "tags": ["sso"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sso.listProviders": { + "get": { + "operationId": "sso-listProviders", + "tags": ["sso"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sso.one": { + "get": { + "operationId": "sso-one", + "tags": ["sso"], + "security": [ + { + "Authorization": [] + } + ], + "parameters": [ + { + "name": "providerId", + "in": "query", + "required": true, + "schema": { + "type": "string", + "minLength": 1 + } + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": {} + } + }, + "default": { + "$ref": "#/components/responses/error" + } + } + } + }, + "/sso.update": { + "post": { + "operationId": "sso-update", + "tags": ["sso"], + "security": [ + { + "Authorization": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providerId": { + "type": "string" + }, + "issuer": { + "type": "string" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "oidcConfig": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "authorizationEndpoint": { + "type": "string" + }, + "tokenEndpoint": { + "type": "string" + }, + "userInfoEndpoint": { + "type": "string" + }, + "tokenEndpointAuthentication": { + "type": "string", + "enum": ["client_secret_post", "client_secret_basic"] + }, + "jwksEndpoint": { + "type": "string" + }, + "discoveryEndpoint": { + "type": "string" + }, + "skipDiscovery": { + "type": "boolean" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "pkce": { + "type": "boolean", + "default": true + }, + "mapping": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "string" + }, + "name": { + "type": "string" + }, + "image": { + "type": "string" + }, + "extraFields": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["id", "email", "name"], + "additionalProperties": false + } + }, + "required": ["clientId", "clientSecret"], + "additionalProperties": false + }, + "samlConfig": { + "type": "object", + "properties": { + "entryPoint": { + "type": "string" + }, + "cert": { + "type": "string" + }, + "callbackUrl": { + "type": "string" + }, + "audience": { + "type": "string" + }, + "idpMetadata": { + "type": "object", + "properties": { + "metadata": { + "type": "string" + }, + "entityID": { + "type": "string" + }, + "cert": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPass": { + "type": "string" + }, + "isAssertionEncrypted": { + "type": "boolean" + }, + "encPrivateKey": { + "type": "string" + }, + "encPrivateKeyPass": { + "type": "string" + }, + "singleSignOnService": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Binding": { + "type": "string" + }, + "Location": { + "type": "string" + } + }, + "required": ["Binding", "Location"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "spMetadata": { + "type": "object", + "properties": { + "metadata": { + "type": "string" + }, + "entityID": { + "type": "string" + }, + "binding": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPass": { + "type": "string" + }, + "isAssertionEncrypted": { + "type": "boolean" + }, + "encPrivateKey": { + "type": "string" + }, + "encPrivateKeyPass": { + "type": "string" + } + }, + "additionalProperties": false + }, + "wantAssertionsSigned": { + "type": "boolean" + }, + "authnRequestsSigned": { + "type": "boolean" + }, + "signatureAlgorithm": { + "type": "string" + }, + "digestAlgorithm": { + "type": "string" + }, + "identifierFormat": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "decryptionPvk": { + "type": "string" + }, + "additionalParams": { + "type": "object", + "additionalProperties": {} + }, + "mapping": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "string" + }, + "name": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "extraFields": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["id", "email", "name"], + "additionalProperties": false + } + }, + "required": [ + "entryPoint", + "cert", + "callbackUrl", + "spMetadata" + ], + "additionalProperties": false + }, + "organizationId": { + "type": "string" + }, + "overrideUserInfo": { + "type": "boolean", + "default": false + } + }, + "required": ["providerId", "issuer", "domains"], + "additionalProperties": false + } + } } - ], + }, "parameters": [], "responses": { "200": { @@ -18515,25 +20383,34 @@ } } }, - "/organization.one": { - "get": { - "operationId": "organization-one", - "tags": ["organization"], + "/sso.deleteProvider": { + "post": { + "operationId": "sso-deleteProvider", + "tags": ["sso"], "security": [ { "Authorization": [] } ], - "parameters": [ - { - "name": "organizationId", - "in": "query", - "required": true, - "schema": { - "type": "string" + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providerId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["providerId"], + "additionalProperties": false + } } } - ], + }, + "parameters": [], "responses": { "200": { "description": "Successful response", @@ -18547,10 +20424,10 @@ } } }, - "/organization.update": { + "/sso.register": { "post": { - "operationId": "organization-update", - "tags": ["organization"], + "operationId": "sso-register", + "tags": ["sso"], "security": [ { "Authorization": [] @@ -18563,17 +20440,252 @@ "schema": { "type": "object", "properties": { - "organizationId": { + "providerId": { "type": "string" }, - "name": { + "issuer": { "type": "string" }, - "logo": { + "domains": { + "type": "array", + "items": { + "type": "string" + } + }, + "oidcConfig": { + "type": "object", + "properties": { + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "authorizationEndpoint": { + "type": "string" + }, + "tokenEndpoint": { + "type": "string" + }, + "userInfoEndpoint": { + "type": "string" + }, + "tokenEndpointAuthentication": { + "type": "string", + "enum": ["client_secret_post", "client_secret_basic"] + }, + "jwksEndpoint": { + "type": "string" + }, + "discoveryEndpoint": { + "type": "string" + }, + "skipDiscovery": { + "type": "boolean" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "pkce": { + "type": "boolean", + "default": true + }, + "mapping": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "string" + }, + "name": { + "type": "string" + }, + "image": { + "type": "string" + }, + "extraFields": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["id", "email", "name"], + "additionalProperties": false + } + }, + "required": ["clientId", "clientSecret"], + "additionalProperties": false + }, + "samlConfig": { + "type": "object", + "properties": { + "entryPoint": { + "type": "string" + }, + "cert": { + "type": "string" + }, + "callbackUrl": { + "type": "string" + }, + "audience": { + "type": "string" + }, + "idpMetadata": { + "type": "object", + "properties": { + "metadata": { + "type": "string" + }, + "entityID": { + "type": "string" + }, + "cert": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPass": { + "type": "string" + }, + "isAssertionEncrypted": { + "type": "boolean" + }, + "encPrivateKey": { + "type": "string" + }, + "encPrivateKeyPass": { + "type": "string" + }, + "singleSignOnService": { + "type": "array", + "items": { + "type": "object", + "properties": { + "Binding": { + "type": "string" + }, + "Location": { + "type": "string" + } + }, + "required": ["Binding", "Location"], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "spMetadata": { + "type": "object", + "properties": { + "metadata": { + "type": "string" + }, + "entityID": { + "type": "string" + }, + "binding": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "privateKeyPass": { + "type": "string" + }, + "isAssertionEncrypted": { + "type": "boolean" + }, + "encPrivateKey": { + "type": "string" + }, + "encPrivateKeyPass": { + "type": "string" + } + }, + "additionalProperties": false + }, + "wantAssertionsSigned": { + "type": "boolean" + }, + "authnRequestsSigned": { + "type": "boolean" + }, + "signatureAlgorithm": { + "type": "string" + }, + "digestAlgorithm": { + "type": "string" + }, + "identifierFormat": { + "type": "string" + }, + "privateKey": { + "type": "string" + }, + "decryptionPvk": { + "type": "string" + }, + "additionalParams": { + "type": "object", + "additionalProperties": {} + }, + "mapping": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "emailVerified": { + "type": "string" + }, + "name": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "extraFields": { + "type": "object", + "additionalProperties": {} + } + }, + "required": ["id", "email", "name"], + "additionalProperties": false + } + }, + "required": [ + "entryPoint", + "cert", + "callbackUrl", + "spMetadata" + ], + "additionalProperties": false + }, + "organizationId": { "type": "string" + }, + "overrideUserInfo": { + "type": "boolean", + "default": false } }, - "required": ["organizationId", "name"], + "required": ["providerId", "issuer", "domains"], "additionalProperties": false } } @@ -18593,10 +20705,10 @@ } } }, - "/organization.delete": { + "/sso.addTrustedOrigin": { "post": { - "operationId": "organization-delete", - "tags": ["organization"], + "operationId": "sso-addTrustedOrigin", + "tags": ["sso"], "security": [ { "Authorization": [] @@ -18609,11 +20721,12 @@ "schema": { "type": "object", "properties": { - "organizationId": { - "type": "string" + "origin": { + "type": "string", + "minLength": 1 } }, - "required": ["organizationId"], + "required": ["origin"], "additionalProperties": false } } @@ -18633,33 +20746,10 @@ } } }, - "/organization.allInvitations": { - "get": { - "operationId": "organization-allInvitations", - "tags": ["organization"], - "security": [ - { - "Authorization": [] - } - ], - "parameters": [], - "responses": { - "200": { - "description": "Successful response", - "content": { - "application/json": {} - } - }, - "default": { - "$ref": "#/components/responses/error" - } - } - } - }, - "/organization.removeInvitation": { + "/sso.removeTrustedOrigin": { "post": { - "operationId": "organization-removeInvitation", - "tags": ["organization"], + "operationId": "sso-removeTrustedOrigin", + "tags": ["sso"], "security": [ { "Authorization": [] @@ -18672,11 +20762,12 @@ "schema": { "type": "object", "properties": { - "invitationId": { - "type": "string" + "origin": { + "type": "string", + "minLength": 1 } }, - "required": ["invitationId"], + "required": ["origin"], "additionalProperties": false } } @@ -18696,10 +20787,10 @@ } } }, - "/organization.setDefault": { + "/sso.updateTrustedOrigin": { "post": { - "operationId": "organization-setDefault", - "tags": ["organization"], + "operationId": "sso-updateTrustedOrigin", + "tags": ["sso"], "security": [ { "Authorization": [] @@ -18712,12 +20803,16 @@ "schema": { "type": "object", "properties": { - "organizationId": { + "oldOrigin": { + "type": "string", + "minLength": 1 + }, + "newOrigin": { "type": "string", "minLength": 1 } }, - "required": ["organizationId"], + "required": ["oldOrigin", "newOrigin"], "additionalProperties": false } } @@ -18808,6 +20903,10 @@ "enabled": { "type": "boolean" }, + "timezone": { + "type": "string", + "nullable": true + }, "createdAt": { "type": "string" } @@ -18904,6 +21003,10 @@ "enabled": { "type": "boolean" }, + "timezone": { + "type": "string", + "nullable": true + }, "createdAt": { "type": "string" } diff --git a/packages/server/src/services/ai.ts b/packages/server/src/services/ai.ts index fefc82eaed..875fe06ed4 100644 --- a/packages/server/src/services/ai.ts +++ b/packages/server/src/services/ai.ts @@ -105,21 +105,21 @@ export const suggestVariants = async ({ }), prompt: ` Act as advanced DevOps engineer and analyze the user's request to determine the appropriate suggestions (up to 3 items). - + CRITICAL - Read the user's request carefully and follow the appropriate strategy: - + Strategy A - If the user specifies a PARTICULAR APPLICATION/SERVICE (e.g., "deploy Chatwoot", "install sendingtk/chatwoot:develop", "setup Bitwarden"): - Generate different deployment VARIANTS of that SAME application - Each variant should be a different configuration (minimal, full stack, with different databases, development vs production, etc.) - Example: For "Chatwoot" → "Chatwoot with PostgreSQL", "Chatwoot Development", "Chatwoot Full Stack" - The name MUST include the specific application name the user mentioned - + Strategy B - If the user describes a GENERAL NEED or USE CASE (e.g., "personal blog", "project management tool", "chat application"): - Suggest different open source projects that fulfill that need - Each suggestion should be a different tool/platform that solves the same problem - Example: For "personal blog" → "WordPress", "Ghost", "Hugo with Nginx" - The name should be the actual project name - + Return your response as a JSON object with the following structure: { "suggestions": [ @@ -131,7 +131,7 @@ export const suggestVariants = async ({ } ] } - + Important rules for the response: 1. Use slug format for the id field (lowercase, hyphenated) 2. Determine which strategy to use based on whether the user specified a particular application or described a general need @@ -142,9 +142,9 @@ export const suggestVariants = async ({ 7. The shortDescription should be a single-line summary focusing on key technologies or differentiators 8. All suggestions should be installable in docker and have docker compose support 9. Provide variety in your suggestions - different complexity levels, tech stacks, or approaches - + User wants to create a new project with the following details: - + ${input} `, }); @@ -182,7 +182,7 @@ export const suggestVariants = async ({ }), prompt: ` Act as advanced DevOps engineer and generate docker compose with environment variables and domain configurations needed to install the following project. - + Return your response as a JSON object with this structure: { "dockerCompose": "yaml string here", @@ -190,9 +190,9 @@ export const suggestVariants = async ({ "domains": [{"host": "domain.com", "port": 3000, "serviceName": "service"}], "configFiles": [{"content": "file content", "filePath": "path/to/file"}] } - + Note: configFiles is optional - only include it if configuration files are absolutely required. - + Follow these rules: Docker Compose Rules: @@ -242,16 +242,16 @@ export const suggestVariants = async ({ 4. ONLY include environment variables that are actually used in the docker-compose 5. Every environment variable referenced in the docker-compose MUST have a corresponding entry in envVariables 6. Do not include environment variables for services that don't exist in the docker-compose - + For each service that needs to be exposed to the internet: 1. Define a domain configuration with: - host: the domain name for the service in format: {service-name}-{random-3-chars-hex}-${ip ? ip.replaceAll(".", "-") : ""}.traefik.me - port: the internal port the service runs on - serviceName: the name of the service in the docker-compose 2. Make sure the service is properly configured to work with the specified port - + User's original request: ${input} - + Project details: ${suggestion?.description} `, diff --git a/packages/server/src/services/domain.ts b/packages/server/src/services/domain.ts index b2e15ed914..232fbd55c5 100644 --- a/packages/server/src/services/domain.ts +++ b/packages/server/src/services/domain.ts @@ -44,7 +44,7 @@ export const createDomain = async (input: typeof apiCreateDomain._type) => { export const generateTraefikMeDomain = async ( appName: string, - userId: string, + _userId: string, serverId?: string, ) => { if (serverId) { diff --git a/packages/server/src/services/preview-deployment.ts b/packages/server/src/services/preview-deployment.ts index 1ece3bc539..bc9bd0f5a4 100644 --- a/packages/server/src/services/preview-deployment.ts +++ b/packages/server/src/services/preview-deployment.ts @@ -235,7 +235,7 @@ const generateWildcardDomain = async ( baseDomain: string, appName: string, serverIp: string, - userId: string, + _userId: string, ): Promise<string> => { if (!baseDomain.startsWith("*.")) { throw new Error('The base domain must start with "*."'); From 355d46948b5818e4e6755f567f99f5796df5eafa Mon Sep 17 00:00:00 2001 From: Mohammed Imran <mohammedimran86992@gmail.com> Date: Mon, 16 Feb 2026 14:10:38 +0530 Subject: [PATCH 05/11] chore: resolved greptile review comments --- .../components/shared/advance-breadcrumb.tsx | 204 ++++++++++-------- packages/server/package.json | 6 +- 2 files changed, 114 insertions(+), 96 deletions(-) diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx index bdb65aa364..0c6f442a56 100644 --- a/apps/dokploy/components/shared/advance-breadcrumb.tsx +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -64,6 +64,105 @@ interface ServiceItem { appName?: string; } +interface EnvironmentData { + applications?: Array<{ + applicationId: string; + name: string; + appName: string; + }>; + compose?: Array<{ composeId: string; name: string; appName: string }>; + postgres?: Array<{ postgresId: string; name: string; appName: string }>; + mysql?: Array<{ mysqlId: string; name: string; appName: string }>; + mariadb?: Array<{ mariadbId: string; name: string; appName: string }>; + redis?: Array<{ redisId: string; name: string; appName: string }>; + mongo?: Array<{ mongoId: string; name: string; appName: string }>; +} + +// Helper function to count total services in an environment +const countEnvironmentServices = (env: EnvironmentData): number => { + return ( + (env.applications?.length || 0) + + (env.compose?.length || 0) + + (env.postgres?.length || 0) + + (env.mysql?.length || 0) + + (env.mariadb?.length || 0) + + (env.redis?.length || 0) + + (env.mongo?.length || 0) + ); +}; + +// Helper function to extract services from an environment into a flat array +const extractServicesFromEnvironment = ( + env: EnvironmentData, +): ServiceItem[] => { + const services: ServiceItem[] = []; + + env.applications?.forEach((app) => { + services.push({ + id: app.applicationId, + name: app.name, + type: "application", + appName: app.appName, + }); + }); + + env.compose?.forEach((comp) => { + services.push({ + id: comp.composeId, + name: comp.name, + type: "compose", + appName: comp.appName, + }); + }); + + env.postgres?.forEach((pg) => { + services.push({ + id: pg.postgresId, + name: pg.name, + type: "postgres", + appName: pg.appName, + }); + }); + + env.mysql?.forEach((my) => { + services.push({ + id: my.mysqlId, + name: my.name, + type: "mysql", + appName: my.appName, + }); + }); + + env.mariadb?.forEach((maria) => { + services.push({ + id: maria.mariadbId, + name: maria.name, + type: "mariadb", + appName: maria.appName, + }); + }); + + env.redis?.forEach((red) => { + services.push({ + id: red.redisId, + name: red.name, + type: "redis", + appName: red.appName, + }); + }); + + env.mongo?.forEach((mon) => { + services.push({ + id: mon.mongoId, + name: mon.name, + type: "mongo", + appName: mon.appName, + }); + }); + + return services; +}; + export const AdvanceBreadcrumb = ({ projectId, environmentId, @@ -114,79 +213,9 @@ export const AdvanceBreadcrumb = ({ }, []); // Extract services from current environment - const services: ServiceItem[] = []; - if (currentEnvironment) { - currentEnvironment.applications?.forEach( - (app: { applicationId: string; name: string; appName: string }) => { - services.push({ - id: app.applicationId, - name: app.name, - type: "application", - appName: app.appName, - }); - }, - ); - currentEnvironment.compose?.forEach( - (comp: { composeId: string; name: string; appName: string }) => { - services.push({ - id: comp.composeId, - name: comp.name, - type: "compose", - appName: comp.appName, - }); - }, - ); - currentEnvironment.postgres?.forEach( - (pg: { postgresId: string; name: string; appName: string }) => { - services.push({ - id: pg.postgresId, - name: pg.name, - type: "postgres", - appName: pg.appName, - }); - }, - ); - currentEnvironment.mysql?.forEach( - (my: { mysqlId: string; name: string; appName: string }) => { - services.push({ - id: my.mysqlId, - name: my.name, - type: "mysql", - appName: my.appName, - }); - }, - ); - currentEnvironment.mariadb?.forEach( - (maria: { mariadbId: string; name: string; appName: string }) => { - services.push({ - id: maria.mariadbId, - name: maria.name, - type: "mariadb", - appName: maria.appName, - }); - }, - ); - currentEnvironment.redis?.forEach( - (red: { redisId: string; name: string; appName: string }) => { - services.push({ - id: red.redisId, - name: red.name, - type: "redis", - appName: red.appName, - }); - }, - ); - currentEnvironment.mongo?.forEach( - (mon: { mongoId: string; name: string; appName: string }) => { - services.push({ - id: mon.mongoId, - name: mon.name, - type: "mongo", - appName: mon.appName, - }); - }, - ); - } + const services: ServiceItem[] = currentEnvironment + ? extractServicesFromEnvironment(currentEnvironment) + : []; // Get current service const currentService = services.find((s) => s.id === serviceId); @@ -199,15 +228,18 @@ export const AdvanceBreadcrumb = ({ const project = allProjects?.find((p) => p.projectId === selectedProjectId); if (project && project.environments.length > 0) { // Use provided environment or find production environment or use the first one + const firstEnvironment = project.environments[0]; const targetEnvId = selectedEnvironmentId || project.environments.find((e) => e.name === "production") ?.environmentId || - project.environments[0]?.environmentId; + firstEnvironment?.environmentId; - router.push( - `/dashboard/project/${selectedProjectId}/environment/${targetEnvId}`, - ); + if (targetEnvId) { + router.push( + `/dashboard/project/${selectedProjectId}/environment/${targetEnvId}`, + ); + } } setProjectOpen(false); setExpandedProjectId(null); @@ -305,15 +337,7 @@ export const AdvanceBreadcrumb = ({ <ScrollArea className="h-[300px]"> {filteredProjects.map((project) => { const totalServices = project.environments.reduce( - (total, env) => - total + - (env.applications?.length || 0) + - (env.compose?.length || 0) + - (env.postgres?.length || 0) + - (env.mysql?.length || 0) + - (env.mariadb?.length || 0) + - (env.redis?.length || 0) + - (env.mongo?.length || 0), + (total, env) => total + countEnvironmentServices(env), 0, ); const isSelected = project.projectId === projectId; @@ -370,13 +394,7 @@ export const AdvanceBreadcrumb = ({ <div className="ml-11 border-l pl-3 py-1 space-y-1"> {project.environments.map((env) => { const envServices = - (env.applications?.length || 0) + - (env.compose?.length || 0) + - (env.postgres?.length || 0) + - (env.mysql?.length || 0) + - (env.mariadb?.length || 0) + - (env.redis?.length || 0) + - (env.mongo?.length || 0); + countEnvironmentServices(env); const isEnvSelected = env.environmentId === environmentId; diff --git a/packages/server/package.json b/packages/server/package.json index bf7a43ab39..f78a6654af 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -44,7 +44,7 @@ "@oslojs/crypto": "1.0.1", "@oslojs/encoding": "1.1.0", "@react-email/components": "^0.0.21", - "@better-auth/sso":"1.4.18", + "@better-auth/sso": "1.4.18", "@trpc/server": "^10.45.2", "adm-zip": "^0.5.16", "ai": "^5.0.17", @@ -85,7 +85,7 @@ "semver": "7.7.3" }, "devDependencies": { - "@better-auth/cli": "1.4.18", + "@better-auth/cli": "1.4.18", "@types/semver": "7.7.1", "@types/adm-zip": "^0.5.7", "@types/bcrypt": "5.0.2", @@ -115,4 +115,4 @@ "node": "^20.16.0", "pnpm": ">=9.12.0" } -} +} \ No newline at end of file From ebf5f486bc17f66347705826d86538fd5d2d220e Mon Sep 17 00:00:00 2001 From: Mauricio Siu <siumauricio@icloud.com> Date: Thu, 26 Feb 2026 22:44:57 -0600 Subject: [PATCH 06/11] refactor: simplify AdvanceBreadcrumb component by removing props and utilizing URL query parameters for ID retrieval --- .../components/shared/advance-breadcrumb.tsx | 116 +++++++++++------- 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx index 0c6f442a56..2e7e8715f9 100644 --- a/apps/dokploy/components/shared/advance-breadcrumb.tsx +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -36,12 +36,7 @@ import { Separator } from "@/components/ui/separator"; import { SidebarTrigger } from "@/components/ui/sidebar"; import { api } from "@/utils/api"; -interface AdvanceBreadcrumbProps { - projectId?: string; - environmentId?: string; - serviceId?: string; - serviceType?: ServiceType; -} +type AdvanceBreadcrumbProps = {}; const getServiceIcon = (type: ServiceType, className = "size-4") => { const icons: Record<ServiceType, React.ReactNode> = { @@ -163,17 +158,31 @@ const extractServicesFromEnvironment = ( return services; }; -export const AdvanceBreadcrumb = ({ - projectId, - environmentId, - serviceId, -}: AdvanceBreadcrumbProps) => { +export const AdvanceBreadcrumb = () => { const router = useRouter(); + const { query } = router; + + // Read IDs from URL (dynamic route segments) + const projectId = + typeof query.projectId === "string" ? query.projectId : null; + const environmentId = + typeof query.environmentId === "string" ? query.environmentId : null; + const serviceId = + (typeof query.applicationId === "string" ? query.applicationId : null) ?? + (typeof query.composeId === "string" ? query.composeId : null) ?? + (typeof query.postgresId === "string" ? query.postgresId : null) ?? + (typeof query.mysqlId === "string" ? query.mysqlId : null) ?? + (typeof query.mariadbId === "string" ? query.mariadbId : null) ?? + (typeof query.redisId === "string" ? query.redisId : null) ?? + (typeof query.mongoId === "string" ? query.mongoId : null) ?? + null; + const [projectOpen, setProjectOpen] = useState(false); const [serviceOpen, setServiceOpen] = useState(false); const [environmentOpen, setEnvironmentOpen] = useState(false); const [projectSearch, setProjectSearch] = useState(""); const [serviceSearch, setServiceSearch] = useState(""); + const [environmentSearch, setEnvironmentSearch] = useState(""); const [expandedProjectId, setExpandedProjectId] = useState<string | null>( null, ); @@ -183,19 +192,19 @@ export const AdvanceBreadcrumb = ({ // Fetch current project data const { data: currentProject } = api.project.one.useQuery( - { projectId: projectId || "" }, + { projectId: projectId ?? "" }, { enabled: !!projectId }, ); // Fetch current environment const { data: currentEnvironment } = api.environment.one.useQuery( - { environmentId: environmentId || "" }, + { environmentId: environmentId ?? "" }, { enabled: !!environmentId }, ); // Fetch environments for current project const { data: projectEnvironments } = api.environment.byProjectId.useQuery( - { projectId: projectId || "" }, + { projectId: projectId ?? "" }, { enabled: !!projectId }, ); @@ -276,6 +285,12 @@ export const AdvanceBreadcrumb = ({ s.appName?.toLowerCase().includes(serviceSearch.toLowerCase()), ); + // Filter environments based on search + const filteredEnvironments = + projectEnvironments?.filter((env) => + env.name.toLowerCase().includes(environmentSearch.toLowerCase()), + ) ?? []; + // If we're just on the projects page, show simple breadcrumb if (!projectId) { return ( @@ -367,7 +382,7 @@ export const AdvanceBreadcrumb = ({ <span className="font-medium"> {project.name} </span> - <span className="text-xs text-muted-foreground"> + <span className="text-muted-foreground"> {project.environments.length} env {project.environments.length !== 1 ? "s" @@ -442,39 +457,58 @@ export const AdvanceBreadcrumb = ({ <Button variant="ghost" aria-expanded={environmentOpen} - className="h-auto px-2 py-1.5 hover:bg-accent gap-1" + className="h-auto px-2 py-1.5 hover:bg-accent gap-2" > - <p className="text-xs font-normal"> + <span className="font-medium max-w-[150px] truncate"> {currentEnvironment?.name || "production"} - </p> - <ChevronDown className="size-3 text-muted-foreground" /> + </span> + <ChevronDown className="size-4 text-muted-foreground" /> </Button> </PopoverTrigger> <PopoverContent - className="w-[200px] p-1" + className="w-[350px] p-0" align="start" sideOffset={8} > - <div className="space-y-1"> - {projectEnvironments.map((env) => { - const isSelected = env.environmentId === environmentId; - return ( - <button - type="button" - key={env.environmentId} - onClick={() => - handleEnvironmentSelect(env.environmentId) - } - className="flex items-center justify-between w-full px-2 py-1.5 text-sm rounded-md hover:bg-accent cursor-pointer" - > - <p className="text-xs">{env.name}</p> - {isSelected && ( - <Check className="size-3 text-primary" /> - )} - </button> - ); - })} - </div> + <Command shouldFilter={false}> + <div className="relative"> + <CommandInput + placeholder="Find Environment..." + value={environmentSearch} + onValueChange={setEnvironmentSearch} + className="w-full focus:ring-0" + /> + <kbd className="pointer-events-none h-5 absolute right-2 top-1/2 -translate-y-1/2 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex"> + Esc + </kbd> + </div> + <CommandList> + <CommandEmpty>No environments found.</CommandEmpty> + <CommandGroup> + <ScrollArea className="h-[300px]"> + {filteredEnvironments.map((env) => { + const isSelected = + env.environmentId === environmentId; + return ( + <CommandItem + key={env.environmentId} + value={env.environmentId} + onSelect={() => + handleEnvironmentSelect(env.environmentId) + } + className="flex items-center justify-between py-2 cursor-pointer" + > + <span className="font-medium">{env.name}</span> + {isSelected && ( + <Check className="size-4 text-primary" /> + )} + </CommandItem> + ); + })} + </ScrollArea> + </CommandGroup> + </CommandList> + </Command> </PopoverContent> </Popover> )} @@ -532,7 +566,7 @@ export const AdvanceBreadcrumb = ({ key={service.id} value={service.id} onSelect={() => handleServiceSelect(service)} - className="flex items-center justify-between py-3 px-2 cursor-pointer" + className="flex items-center justify-between py-2 cursor-pointer" > <div className="flex items-center gap-3"> <div className="flex items-center justify-center size-8 rounded-md bg-muted"> From a1cf5520a9fece8bbd51df02465d990c81d46e91 Mon Sep 17 00:00:00 2001 From: Mohammed Imran <mohammedimran86992@gmail.com> Date: Mon, 2 Mar 2026 02:18:05 +0530 Subject: [PATCH 07/11] refactor: remove props being passes to AdvanceBreadcrumb --- .../project/[projectId]/environment/[environmentId].tsx | 2 +- .../services/application/[applicationId].tsx | 7 +------ .../[environmentId]/services/compose/[composeId].tsx | 7 +------ .../[environmentId]/services/mariadb/[mariadbId].tsx | 7 +------ .../[environmentId]/services/mongo/[mongoId].tsx | 7 +------ .../[environmentId]/services/mysql/[mysqlId].tsx | 7 +------ .../[environmentId]/services/postgres/[postgresId].tsx | 7 +------ .../[environmentId]/services/redis/[redisId].tsx | 7 +------ 8 files changed, 8 insertions(+), 43 deletions(-) diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx index 3fe6b7845c..d934f263df 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId].tsx @@ -865,7 +865,7 @@ const EnvironmentPage = ( return ( <div> - <AdvanceBreadcrumb projectId={projectId} environmentId={environmentId} /> + <AdvanceBreadcrumb /> <Head> <title> Environment: {currentEnvironment.name} | {projectData?.name} | Dokploy diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx index e88b02ff0f..82433679ae 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/application/[applicationId].tsx @@ -104,12 +104,7 @@ const Service = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="application" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={applicationId} - serviceType="application" - /> + <AdvanceBreadcrumb /> <Head> <title> Application: {data?.name} - {data?.environment.project.name} | Dokploy diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx index fa52f957d8..2d04f0d37e 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/compose/[composeId].tsx @@ -93,12 +93,7 @@ const Service = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="compose" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={composeId} - serviceType="compose" - /> + <AdvanceBreadcrumb /> <Head> <title> Compose: {data?.name} - {data?.environment?.project?.name} | Dokploy diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx index 2f45ad0cfd..c233872e27 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mariadb/[mariadbId].tsx @@ -74,12 +74,7 @@ const Mariadb = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="mariadb" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={mariadbId} - serviceType="mariadb" - /> + <AdvanceBreadcrumb /> <div className="flex flex-col gap-4"> <Head> <title> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx index a810ca6076..143ef93c66 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mongo/[mongoId].tsx @@ -73,12 +73,7 @@ const Mongo = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="mongodb" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={mongoId} - serviceType="mongo" - /> + <AdvanceBreadcrumb /> <Head> <title> Database: {data?.name} - {data?.environment?.project?.name} | Dokploy diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx index bf7dae0e6c..2520bd8f7e 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/mysql/[mysqlId].tsx @@ -72,12 +72,7 @@ const MySql = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="mysql" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={mysqlId} - serviceType="mysql" - /> + <AdvanceBreadcrumb /> <div className="flex flex-col gap-4"> <Head> <title> diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx index fe1c88c02f..984c9deab5 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/postgres/[postgresId].tsx @@ -72,12 +72,7 @@ const Postgresql = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="postgres" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={postgresId} - serviceType="postgres" - /> + <AdvanceBreadcrumb /> <Head> <title> Database: {data?.name} - {data?.environment?.project?.name} | Dokploy diff --git a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx index c40e76bd3a..54617221df 100644 --- a/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx +++ b/apps/dokploy/pages/dashboard/project/[projectId]/environment/[environmentId]/services/redis/[redisId].tsx @@ -72,12 +72,7 @@ const Redis = ( return ( <div className="pb-10"> <UseKeyboardNav forPage="redis" /> - <AdvanceBreadcrumb - projectId={projectId as string} - environmentId={environmentId as string} - serviceId={redisId} - serviceType="redis" - /> + <AdvanceBreadcrumb /> <Head> <title> Database: {data?.name} - {data?.environment?.project?.name} | Dokploy From f95b29a450b851842bf165ffef08d6d463558fe8 Mon Sep 17 00:00:00 2001 From: Mohammed Imran <mohammedimran86992@gmail.com> Date: Mon, 2 Mar 2026 02:21:18 +0530 Subject: [PATCH 08/11] Export findGitea as default to fix typecheck --- apps/dokploy/pages/api/providers/gitea/helper.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/dokploy/pages/api/providers/gitea/helper.ts b/apps/dokploy/pages/api/providers/gitea/helper.ts index 207eb90756..67d630436a 100644 --- a/apps/dokploy/pages/api/providers/gitea/helper.ts +++ b/apps/dokploy/pages/api/providers/gitea/helper.ts @@ -38,3 +38,5 @@ export const redirectWithError = (res: NextApiResponse, error: string) => { `/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`, ); }; + +export default findGitea; From 1c5b92729a31f897525d02d76849a3e839b4508f Mon Sep 17 00:00:00 2001 From: Mohammed Imran <mohammedimran86992@gmail.com> Date: Mon, 2 Mar 2026 10:05:39 +0530 Subject: [PATCH 09/11] refactor: resolved type errors in advance-breadcrumb.ts --- .../components/shared/advance-breadcrumb.tsx | 340 +++++++++--------- 1 file changed, 176 insertions(+), 164 deletions(-) diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx index 2e7e8715f9..0f20546bdd 100644 --- a/apps/dokploy/components/shared/advance-breadcrumb.tsx +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -9,7 +9,7 @@ import { X, } from "lucide-react"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; +import { type ComponentType, useEffect, useMemo, useState } from "react"; import { MariadbIcon, MongodbIcon, @@ -34,128 +34,145 @@ import { import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { SidebarTrigger } from "@/components/ui/sidebar"; -import { api } from "@/utils/api"; +import { api, type RouterOutputs } from "@/utils/api"; -type AdvanceBreadcrumbProps = {}; +type ProjectItem = RouterOutputs["project"]["all"][number]; +type ProjectEnvironment = ProjectItem["environments"][number]; +type EnvironmentDetails = RouterOutputs["environment"]["one"]; -const getServiceIcon = (type: ServiceType, className = "size-4") => { - const icons: Record<ServiceType, React.ReactNode> = { - application: <GlobeIcon className={className} />, - compose: <CircuitBoard className={className} />, - postgres: <PostgresqlIcon className={className} />, - mysql: <MysqlIcon className={className} />, - mariadb: <MariadbIcon className={className} />, - redis: <RedisIcon className={className} />, - mongo: <MongodbIcon className={className} />, - }; - - return icons[type]; -}; - -interface ServiceItem { +type ServiceItem = { id: string; name: string; type: ServiceType; - appName?: string; -} - -interface EnvironmentData { - applications?: Array<{ - applicationId: string; - name: string; - appName: string; - }>; - compose?: Array<{ composeId: string; name: string; appName: string }>; - postgres?: Array<{ postgresId: string; name: string; appName: string }>; - mysql?: Array<{ mysqlId: string; name: string; appName: string }>; - mariadb?: Array<{ mariadbId: string; name: string; appName: string }>; - redis?: Array<{ redisId: string; name: string; appName: string }>; - mongo?: Array<{ mongoId: string; name: string; appName: string }>; -} - -// Helper function to count total services in an environment -const countEnvironmentServices = (env: EnvironmentData): number => { - return ( - (env.applications?.length || 0) + - (env.compose?.length || 0) + - (env.postgres?.length || 0) + - (env.mysql?.length || 0) + - (env.mariadb?.length || 0) + - (env.redis?.length || 0) + - (env.mongo?.length || 0) - ); }; -// Helper function to extract services from an environment into a flat array +type NamedService = { + name: string; +}; + +type EnvironmentServiceCollections = { + applications: (NamedService & { applicationId: string })[]; + compose: (NamedService & { composeId: string })[]; + postgres: (NamedService & { postgresId: string })[]; + mysql: (NamedService & { mysqlId: string })[]; + mariadb: (NamedService & { mariadbId: string })[]; + redis: (NamedService & { redisId: string })[]; + mongo: (NamedService & { mongoId: string })[]; +}; + +type ServiceCollections = Pick< + ProjectEnvironment, + | "applications" + | "compose" + | "postgres" + | "mysql" + | "mariadb" + | "redis" + | "mongo" +>; + +const SERVICE_COLLECTION_KEYS = [ + "applications", + "compose", + "postgres", + "mysql", + "mariadb", + "redis", + "mongo", +] as const satisfies ReadonlyArray<keyof ServiceCollections>; + +const SERVICE_QUERY_KEYS = [ + "applicationId", + "composeId", + "postgresId", + "mysqlId", + "mariadbId", + "redisId", + "mongoId", +] as const; + +const SERVICE_ICONS: Record< + ServiceType, + ComponentType<{ className?: string }> +> = { + application: GlobeIcon, + compose: CircuitBoard, + postgres: PostgresqlIcon, + mysql: MysqlIcon, + mariadb: MariadbIcon, + redis: RedisIcon, + mongo: MongodbIcon, +}; + +const getStringQueryParam = (value: string | string[] | undefined) => + typeof value === "string" ? value : null; + +const includesSearch = (value: string | null | undefined, search: string) => + value?.toLowerCase().includes(search.toLowerCase()) ?? false; + +const getServiceIcon = (type: ServiceType, className = "size-4") => { + const Icon = SERVICE_ICONS[type]; + return <Icon className={className} />; +}; + +const countEnvironmentServices = (environment: ServiceCollections): number => + SERVICE_COLLECTION_KEYS.reduce( + (total, key) => total + environment[key].length, + 0, + ); + +const mapServices = <T extends { name: string }>( + items: readonly T[], + getId: (item: T) => string, + type: ServiceType, +): ServiceItem[] => + items.map((item) => ({ + id: getId(item), + name: item.name, + type, + })); + const extractServicesFromEnvironment = ( - env: EnvironmentData, + environment: EnvironmentDetails | null | undefined, ): ServiceItem[] => { - const services: ServiceItem[] = []; - - env.applications?.forEach((app) => { - services.push({ - id: app.applicationId, - name: app.name, - type: "application", - appName: app.appName, - }); - }); - - env.compose?.forEach((comp) => { - services.push({ - id: comp.composeId, - name: comp.name, - type: "compose", - appName: comp.appName, - }); - }); - - env.postgres?.forEach((pg) => { - services.push({ - id: pg.postgresId, - name: pg.name, - type: "postgres", - appName: pg.appName, - }); - }); - - env.mysql?.forEach((my) => { - services.push({ - id: my.mysqlId, - name: my.name, - type: "mysql", - appName: my.appName, - }); - }); - - env.mariadb?.forEach((maria) => { - services.push({ - id: maria.mariadbId, - name: maria.name, - type: "mariadb", - appName: maria.appName, - }); - }); - - env.redis?.forEach((red) => { - services.push({ - id: red.redisId, - name: red.name, - type: "redis", - appName: red.appName, - }); - }); - - env.mongo?.forEach((mon) => { - services.push({ - id: mon.mongoId, - name: mon.name, - type: "mongo", - appName: mon.appName, - }); - }); - - return services; + if (!environment) return []; + + const servicesByType = + environment as unknown as EnvironmentServiceCollections; + + return [ + ...mapServices( + servicesByType.applications, + (item) => item.applicationId, + "application", + ), + ...mapServices(servicesByType.compose, (item) => item.composeId, "compose"), + ...mapServices( + servicesByType.postgres, + (item) => item.postgresId, + "postgres", + ), + ...mapServices(servicesByType.mysql, (item) => item.mysqlId, "mysql"), + ...mapServices(servicesByType.mariadb, (item) => item.mariadbId, "mariadb"), + ...mapServices(servicesByType.redis, (item) => item.redisId, "redis"), + ...mapServices(servicesByType.mongo, (item) => item.mongoId, "mongo"), + ]; +}; + +const getTargetEnvironmentId = ( + project: ProjectItem, + selectedEnvironmentId?: string, +) => { + if (selectedEnvironmentId) return selectedEnvironmentId; + + const productionEnvironment = project.environments.find( + (environment) => environment.name === "production", + ); + + return ( + productionEnvironment?.environmentId ?? + project.environments[0]?.environmentId + ); }; export const AdvanceBreadcrumb = () => { @@ -163,19 +180,12 @@ export const AdvanceBreadcrumb = () => { const { query } = router; // Read IDs from URL (dynamic route segments) - const projectId = - typeof query.projectId === "string" ? query.projectId : null; - const environmentId = - typeof query.environmentId === "string" ? query.environmentId : null; + const projectId = getStringQueryParam(query.projectId); + const environmentId = getStringQueryParam(query.environmentId); const serviceId = - (typeof query.applicationId === "string" ? query.applicationId : null) ?? - (typeof query.composeId === "string" ? query.composeId : null) ?? - (typeof query.postgresId === "string" ? query.postgresId : null) ?? - (typeof query.mysqlId === "string" ? query.mysqlId : null) ?? - (typeof query.mariadbId === "string" ? query.mariadbId : null) ?? - (typeof query.redisId === "string" ? query.redisId : null) ?? - (typeof query.mongoId === "string" ? query.mongoId : null) ?? - null; + SERVICE_QUERY_KEYS.map((key) => getStringQueryParam(query[key])).find( + (value): value is string => !!value, + ) ?? null; const [projectOpen, setProjectOpen] = useState(false); const [serviceOpen, setServiceOpen] = useState(false); @@ -221,13 +231,15 @@ export const AdvanceBreadcrumb = () => { return () => window.removeEventListener("keydown", handleKeyDown); }, []); - // Extract services from current environment - const services: ServiceItem[] = currentEnvironment - ? extractServicesFromEnvironment(currentEnvironment) - : []; + const services = useMemo( + () => extractServicesFromEnvironment(currentEnvironment), + [currentEnvironment], + ); - // Get current service - const currentService = services.find((s) => s.id === serviceId); + const currentService = useMemo( + () => services.find((service) => service.id === serviceId), + [serviceId, services], + ); // Navigate to project's default environment const handleProjectSelect = ( @@ -235,18 +247,15 @@ export const AdvanceBreadcrumb = () => { selectedEnvironmentId?: string, ) => { const project = allProjects?.find((p) => p.projectId === selectedProjectId); - if (project && project.environments.length > 0) { - // Use provided environment or find production environment or use the first one - const firstEnvironment = project.environments[0]; - const targetEnvId = - selectedEnvironmentId || - project.environments.find((e) => e.name === "production") - ?.environmentId || - firstEnvironment?.environmentId; - - if (targetEnvId) { + if (project) { + const targetEnvironmentId = getTargetEnvironmentId( + project, + selectedEnvironmentId, + ); + + if (targetEnvironmentId) { router.push( - `/dashboard/project/${selectedProjectId}/environment/${targetEnvId}`, + `/dashboard/project/${selectedProjectId}/environment/${targetEnvironmentId}`, ); } } @@ -262,34 +271,37 @@ export const AdvanceBreadcrumb = () => { // Navigate to service const handleServiceSelect = (service: ServiceItem) => { - const serviceTypePath = - service.type === "application" ? "application" : service.type; + if (!environmentId) return; + router.push( - `/dashboard/project/${projectId}/environment/${environmentId}/services/${serviceTypePath}/${service.id}`, + `/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`, ); setServiceOpen(false); }; - // Filter projects based on search - const filteredProjects = - allProjects?.filter( - (p) => - p.name.toLowerCase().includes(projectSearch.toLowerCase()) || - p.description?.toLowerCase().includes(projectSearch.toLowerCase()), - ) || []; - - // Filter services based on search - const filteredServices = services.filter( - (s) => - s.name.toLowerCase().includes(serviceSearch.toLowerCase()) || - s.appName?.toLowerCase().includes(serviceSearch.toLowerCase()), + const filteredProjects = useMemo( + () => + (allProjects ?? []).filter( + (project) => + includesSearch(project.name, projectSearch) || + includesSearch(project.description, projectSearch), + ), + [allProjects, projectSearch], + ); + + const filteredServices = useMemo( + () => + services.filter((service) => includesSearch(service.name, serviceSearch)), + [serviceSearch, services], ); - // Filter environments based on search - const filteredEnvironments = - projectEnvironments?.filter((env) => - env.name.toLowerCase().includes(environmentSearch.toLowerCase()), - ) ?? []; + const filteredEnvironments = useMemo( + () => + (projectEnvironments ?? []).filter((environment) => + includesSearch(environment.name, environmentSearch), + ), + [environmentSearch, projectEnvironments], + ); // If we're just on the projects page, show simple breadcrumb if (!projectId) { From 81ecf214f1c60f9127b381e33a9b452436b7b83b Mon Sep 17 00:00:00 2001 From: Mauricio Siu <siumauricio@icloud.com> Date: Thu, 19 Mar 2026 00:44:43 -0600 Subject: [PATCH 10/11] fix: update input focus styles in AdvanceBreadcrumb component - Changed input class from "focus:ring-0" to "focus-visible:ring-0" for improved accessibility and visual feedback on focus. --- apps/dokploy/components/shared/advance-breadcrumb.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/dokploy/components/shared/advance-breadcrumb.tsx b/apps/dokploy/components/shared/advance-breadcrumb.tsx index 0f20546bdd..52401482fa 100644 --- a/apps/dokploy/components/shared/advance-breadcrumb.tsx +++ b/apps/dokploy/components/shared/advance-breadcrumb.tsx @@ -352,7 +352,7 @@ export const AdvanceBreadcrumb = () => { placeholder="Find Project..." value={projectSearch} onValueChange={setProjectSearch} - className="w-full focus:ring-0" + className="w-full focus-visible:ring-0" /> <kbd className="pointer-events-none h-5 absolute right-2 top-1/2 -translate-y-1/2 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex"> Esc @@ -488,7 +488,7 @@ export const AdvanceBreadcrumb = () => { placeholder="Find Environment..." value={environmentSearch} onValueChange={setEnvironmentSearch} - className="w-full focus:ring-0" + className="w-full focus-visible:ring-0" /> <kbd className="pointer-events-none h-5 absolute right-2 top-1/2 -translate-y-1/2 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex"> Esc @@ -561,7 +561,7 @@ export const AdvanceBreadcrumb = () => { placeholder="Find Service..." value={serviceSearch} onValueChange={setServiceSearch} - className="w-full focus:ring-0" + className="w-full focus-visible:ring-0" /> <kbd className="pointer-events-none h-5 select-none absolute right-2 top-1/2 -translate-y-1/2 items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 flex"> Esc From 51d744ba45dfd31f8fc88ad9bffb60126b3bfc4c Mon Sep 17 00:00:00 2001 From: Mauricio Siu <siumauricio@icloud.com> Date: Thu, 19 Mar 2026 00:45:11 -0600 Subject: [PATCH 11/11] refactor: remove unused AdvanceBreadcrumb import from project show component --- apps/dokploy/components/dashboard/projects/show.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index c352503f75..7fa4aefb55 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -12,7 +12,6 @@ import Link from "next/link"; import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; -import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb"; import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar"; import { DateTooltip } from "@/components/shared/date-tooltip"; import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";