"use client"; import type { inferRouterOutputs } from "@trpc/server"; import { Activity, BarChartHorizontalBigIcon, Bell, BlocksIcon, BookIcon, BotIcon, Boxes, ChevronRight, ChevronsUpDown, CircleHelp, Clock, CreditCard, Database, Folder, Forward, GalleryVerticalEnd, GitBranch, Key, KeyRound, Loader2, LogIn, type LucideIcon, Package, PieChart, Server, ShieldCheck, Star, Trash2, User, Users, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, } from "@/components/ui/breadcrumb"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Separator } from "@/components/ui/separator"; import { SIDEBAR_COOKIE_NAME, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupLabel, SidebarHeader, SidebarInset, SidebarMenu, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar, } from "@/components/ui/sidebar"; import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; import type { AppRouter } from "@/server/api/root"; import { api } from "@/utils/api"; import { AddOrganization } from "../dashboard/organization/handle-organization"; import { DialogAction } from "../shared/dialog-action"; import { Logo } from "../shared/logo"; import { Button } from "../ui/button"; import { TimeBadge } from "../ui/time-badge"; import { UpdateServerButton } from "./update-server"; import { UserNav } from "./user-nav"; // The types of the queries we are going to use type AuthQueryOutput = inferRouterOutputs["user"]["get"]; type SingleNavItem = { isSingle?: true; title: string; url: string; icon?: LucideIcon; isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; }; // NavItem type // Consists of a single item or a group of items // If `isSingle` is true or undefined, the item is a single item // If `isSingle` is false, the item is a group of items type NavItem = | SingleNavItem | { isSingle: false; title: string; icon: LucideIcon; items: SingleNavItem[]; isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean; }) => boolean; }; // ExternalLink type // Represents an external link item (used for the help section) type ExternalLink = { name: string; url: string; icon: React.ComponentType<{ className?: string }>; isEnabled?: (opts: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean; }; // Menu type // Consists of home, settings, and help items type Menu = { home: NavItem[]; settings: NavItem[]; help: ExternalLink[]; }; // Menu items // Consists of unfiltered home, settings, and help items // The items are filtered based on the user's role and permissions // The `isEnabled` function is called to determine if the item should be displayed const MENU: Menu = { home: [ { isSingle: true, title: "Projects", url: "/dashboard/projects", icon: Folder, }, { isSingle: true, title: "Monitoring", url: "/dashboard/monitoring", icon: BarChartHorizontalBigIcon, // Only enabled in non-cloud environments isEnabled: ({ isCloud }) => !isCloud, }, { isSingle: true, title: "Schedules", url: "/dashboard/schedules", icon: Clock, // Only enabled in non-cloud environments isEnabled: ({ isCloud, auth }) => !isCloud && (auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "Traefik File System", url: "/dashboard/traefik", icon: GalleryVerticalEnd, // Only enabled for admins and users with access to Traefik files in non-cloud environments isEnabled: ({ auth, isCloud }) => !!( (auth?.role === "owner" || auth?.role === "admin" || auth?.canAccessToTraefikFiles) && !isCloud ), }, { isSingle: true, title: "Docker", url: "/dashboard/docker", icon: BlocksIcon, // Only enabled for admins and users with access to Docker in non-cloud environments isEnabled: ({ auth, isCloud }) => !!( (auth?.role === "owner" || auth?.role === "admin" || auth?.canAccessToDocker) && !isCloud ), }, { isSingle: true, title: "Swarm", url: "/dashboard/swarm", icon: PieChart, // Only enabled for admins and users with access to Docker in non-cloud environments isEnabled: ({ auth, isCloud }) => !!( (auth?.role === "owner" || auth?.role === "admin" || auth?.canAccessToDocker) && !isCloud ), }, { isSingle: true, title: "Requests", url: "/dashboard/requests", icon: Forward, // Only enabled for admins and users with access to Docker in non-cloud environments isEnabled: ({ auth, isCloud }) => !!( (auth?.role === "owner" || auth?.role === "admin" || auth?.canAccessToDocker) && !isCloud ), }, // Legacy unused menu, adjusted to the new structure // { // isSingle: true, // title: "Projects", // url: "/dashboard/projects", // icon: Folder, // }, // { // isSingle: true, // title: "Monitoring", // icon: BarChartHorizontalBigIcon, // url: "/dashboard/settings/monitoring", // }, // { // isSingle: false, // title: "Settings", // icon: Settings2, // items: [ // { // title: "Profile", // url: "/dashboard/settings/profile", // }, // { // title: "Users", // url: "/dashboard/settings/users", // }, // { // title: "SSH Key", // url: "/dashboard/settings/ssh-keys", // }, // { // title: "Git", // url: "/dashboard/settings/git-providers", // }, // ], // }, // { // isSingle: false, // title: "Integrations", // icon: BlocksIcon, // items: [ // { // title: "S3 Destinations", // url: "/dashboard/settings/destinations", // }, // { // title: "Registry", // url: "/dashboard/settings/registry", // }, // { // title: "Notifications", // url: "/dashboard/settings/notifications", // }, // ], // }, ], settings: [ { isSingle: true, title: "Web Server", url: "/dashboard/settings/server", icon: Activity, // Only enabled for admins in non-cloud environments isEnabled: ({ auth, isCloud }) => !!((auth?.role === "owner" || auth?.role === "admin") && !isCloud), }, { isSingle: true, title: "Profile", url: "/dashboard/settings/profile", icon: User, }, { isSingle: true, title: "Remote Servers", url: "/dashboard/settings/servers", icon: Server, // Only enabled for admins isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "Users", icon: Users, url: "/dashboard/settings/users", // Only enabled for admins isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "SSH Keys", icon: KeyRound, url: "/dashboard/settings/ssh-keys", // Only enabled for admins and users with access to SSH keys isEnabled: ({ auth }) => !!( auth?.role === "owner" || auth?.canAccessToSSHKeys || auth?.role === "admin" ), }, { title: "AI", icon: BotIcon, url: "/dashboard/settings/ai", isSingle: true, isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "Git", url: "/dashboard/settings/git-providers", icon: GitBranch, // Only enabled for admins and users with access to Git providers isEnabled: ({ auth }) => !!( auth?.role === "owner" || auth?.canAccessToGitProviders || auth?.role === "admin" ), }, { isSingle: true, title: "Registry", url: "/dashboard/settings/registry", icon: Package, // Only enabled for admins isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "S3 Destinations", url: "/dashboard/settings/destinations", icon: Database, // Only enabled for admins isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "Certificates", url: "/dashboard/settings/certificates", icon: ShieldCheck, // Only enabled for admins isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "Cluster", url: "/dashboard/settings/cluster", icon: Boxes, // Only enabled for admins in non-cloud environments isEnabled: ({ auth, isCloud }) => !!((auth?.role === "owner" || auth?.role === "admin") && !isCloud), }, { isSingle: true, title: "Notifications", url: "/dashboard/settings/notifications", icon: Bell, // Only enabled for admins isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "Billing", url: "/dashboard/settings/billing", icon: CreditCard, // Only enabled for admins in cloud environments isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && isCloud), }, { isSingle: true, title: "License", url: "/dashboard/settings/license", icon: Key, // Only enabled for admins in non-cloud environments isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, { isSingle: true, title: "SSO", url: "/dashboard/settings/sso", icon: LogIn, // Enabled for admins in both cloud and self-hosted (enterprise) isEnabled: ({ auth }) => !!(auth?.role === "owner" || auth?.role === "admin"), }, ], help: [ { name: "Documentation", url: "https://docs.dokploy.com/docs/core", icon: BookIcon, }, { name: "Support", url: "https://discord.gg/2tBnJ3jDJc", icon: CircleHelp, }, ], } as const; /** * Creates a menu based on the current user's role and permissions * @returns a menu object with the home, settings, and help items */ function createMenuForAuthUser(opts: { auth?: AuthQueryOutput; isCloud: boolean; }): Menu { return { // Filter the home items based on the user's role and permissions // Calls the `isEnabled` function if it exists to determine if the item should be displayed home: MENU.home.filter((item) => !item.isEnabled ? true : item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud, }), ), // Filter the settings items based on the user's role and permissions // Calls the `isEnabled` function if it exists to determine if the item should be displayed settings: MENU.settings.filter((item) => !item.isEnabled ? true : item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud, }), ), // Filter the help items based on the user's role and permissions // Calls the `isEnabled` function if it exists to determine if the item should be displayed help: MENU.help.filter((item) => !item.isEnabled ? true : item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud, }), ), }; } /** * Determines if an item url is active based on the current pathname * @returns true if the item url is active, false otherwise */ function isActiveRoute(opts: { /** The url of the item. Usually obtained from `item.url` */ itemUrl: string; /** The current pathname. Usually obtained from `usePathname()` */ pathname: string; }): boolean { const normalizedItemUrl = opts.itemUrl?.replace("/projects", "/project"); const normalizedPathname = opts.pathname?.replace("/projects", "/project"); if (!normalizedPathname) return false; if (normalizedPathname === normalizedItemUrl) return true; if (normalizedPathname.startsWith(normalizedItemUrl)) { const nextChar = normalizedPathname.charAt(normalizedItemUrl.length); return nextChar === "/"; } return false; } /** * Finds the active nav item based on the current pathname * @returns the active nav item with `SingleNavItem` type or undefined if none is active */ function findActiveNavItem( navItems: NavItem[], pathname: string, ): SingleNavItem | undefined { const found = navItems.find((item) => item.isSingle !== false ? // The current item is single, so check if the item url is active isActiveRoute({ itemUrl: item.url, pathname }) : // The current item is not single, so check if any of the sub items are active item.items.some((item) => isActiveRoute({ itemUrl: item.url, pathname }), ), ); if (found?.isSingle !== false) { // The found item is single, so return it return found; } // The found item is not single, so find the active sub item return found?.items.find((item) => isActiveRoute({ itemUrl: item.url, pathname }), ); } interface Props { children: React.ReactNode; } function LogoWrapper() { return ; } function SidebarLogo() { const { state } = useSidebar(); const { data: isCloud } = api.settings.isCloud.useQuery(); const { data: user } = api.user.get.useQuery(); const { data: session } = authClient.useSession(); const { data: organizations, refetch, isLoading, } = api.organization.all.useQuery(); const { mutateAsync: deleteOrganization, isLoading: isRemoving } = api.organization.delete.useMutation(); const { mutateAsync: setDefaultOrganization, isLoading: isSettingDefault } = api.organization.setDefault.useMutation(); const { isMobile } = useSidebar(); const { data: activeOrganization } = authClient.useActiveOrganization(); const _utils = api.useUtils(); const { data: invitations, refetch: refetchInvitations } = api.user.getInvitations.useQuery(); const [_activeTeam, setActiveTeam] = useState< typeof activeOrganization | null >(null); useEffect(() => { if (activeOrganization) { setActiveTeam(activeOrganization); } }, [activeOrganization]); return ( <> {isLoading ? (
) : ( {/* Organization Logo and Selector */}

{activeOrganization?.name ?? "Select Organization"}

Organizations {organizations?.map((org) => { const isDefault = org.members?.[0]?.isDefault ?? false; return (
{ await authClient.organization.setActive({ organizationId: org.id, }); window.location.reload(); }} className="w-full gap-2 p-2" >
{org.name}
{org.ownerId === session?.user?.id && ( <> { await deleteOrganization({ organizationId: org.id, }) .then(() => { refetch(); toast.success( "Organization deleted successfully", ); }) .catch((error) => { toast.error( error?.message || "Error deleting organization", ); }); }} > )}
); })} {(user?.role === "owner" || user?.role === "admin" || isCloud) && ( <> )}
{/* Notification Bell */} Pending Invitations
{invitations && invitations.length > 0 ? ( invitations.map((invitation) => (
e.preventDefault()} >
{invitation?.organization?.name}
Expires:{" "} {new Date(invitation.expiresAt).toLocaleString()}
Role: {invitation.role}
{ const { error } = await authClient.organization.acceptInvitation({ invitationId: invitation.id, }); if (error) { toast.error( error.message || "Error accepting invitation", ); } else { toast.success("Invitation accepted successfully"); await refetchInvitations(); await refetch(); } }} >
)) ) : ( No pending invitations )}
)} ); } export default function Page({ children }: Props) { const [defaultOpen, setDefaultOpen] = useState( undefined, ); const [isLoaded, setIsLoaded] = useState(false); useEffect(() => { const cookieValue = document.cookie .split("; ") .find((row) => row.startsWith(`${SIDEBAR_COOKIE_NAME}=`)) ?.split("=")[1]; setDefaultOpen(cookieValue === undefined ? true : cookieValue === "true"); setIsLoaded(true); }, []); const pathname = usePathname(); const { data: auth } = api.user.get.useQuery(); const { data: dokployVersion } = api.settings.getDokployVersion.useQuery(); const includesProjects = pathname?.includes("/dashboard/project"); const { data: isCloud } = api.settings.isCloud.useQuery(); const { home: filteredHome, settings: filteredSettings, help, } = createMenuForAuthUser({ auth, isCloud: !!isCloud }); const activeItem = findActiveNavItem( [...filteredHome, ...filteredSettings], pathname, ); if (!isLoaded) { return
; // Placeholder mientras se carga } return ( { setDefaultOpen(open); document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`; }} style={ { "--sidebar-width": "19.5rem", "--sidebar-width-mobile": "19.5rem", } as React.CSSProperties } > {/* */} {/* */} Home {filteredHome.map((item) => { const isSingle = item.isSingle !== false; const isActive = isSingle ? isActiveRoute({ itemUrl: item.url, pathname }) : item.items.some((item) => isActiveRoute({ itemUrl: item.url, pathname }), ); return ( {isSingle ? ( {item.icon && ( )} {item.title} ) : ( <> {item.icon && } {item.title} {item.items?.length && ( )} {item.items?.map((subItem) => ( {subItem.icon && ( )} {subItem.title} ))} )} ); })} Settings {filteredSettings.map((item) => { const isSingle = item.isSingle !== false; const isActive = isSingle ? isActiveRoute({ itemUrl: item.url, pathname }) : item.items.some((item) => isActiveRoute({ itemUrl: item.url, pathname }), ); return ( {isSingle ? ( {item.icon && ( )} {item.title} ) : ( <> {item.icon && } {item.title} {item.items?.length && ( )} {item.items?.map((subItem) => ( {subItem.icon && ( )} {subItem.title} ))} )} ); })} Extra {help.map((item: ExternalLink) => ( {item.name} ))} {!isCloud && (auth?.role === "owner" || auth?.role === "admin") && ( )} {dokployVersion && ( <>
Version {dokployVersion}
{dokployVersion}
)}
{!includesProjects && (
{activeItem?.title}
{!isCloud && }
)}
{children}
); }