From d469fe50c7399433543a7789c33f7ca0632db9a8 Mon Sep 17 00:00:00 2001 From: Yash Kumar Date: Fri, 19 Jun 2026 17:51:22 +0530 Subject: [PATCH] feat(enterprise): whitelabeling sections, SSO/restriction controls, license gating --- .../server/update-server-config.test.ts | 14 + .../actions/toggle-hide-help-links.tsx | 48 + .../actions/toggle-hide-social-links.tsx | 51 + .../servers/actions/toggle-hide-sso-login.tsx | 48 + .../servers/actions/toggle-show-sso.tsx | 48 + .../actions/toggle-show-whitelabeling.tsx | 51 + .../components/layouts/onboarding-layout.tsx | 74 +- apps/dokploy/components/layouts/side.tsx | 140 +- .../license-keys/license-feature-settings.tsx | 70 + .../whitelabeling/whitelabeling-provider.tsx | 31 - .../whitelabeling/whitelabeling-settings.tsx | 758 +- apps/dokploy/components/ui/sidebar.tsx | 2 +- apps/dokploy/drizzle/0173_chief_firedrake.sql | 1 + apps/dokploy/drizzle/0174_violet_mojo.sql | 1 + apps/dokploy/drizzle/0175_thick_starbolt.sql | 1 + apps/dokploy/drizzle/0176_bright_rhino.sql | 1 + apps/dokploy/drizzle/0177_elite_famine.sql | 1 + .../drizzle/0178_chemical_black_knight.sql | 2 + apps/dokploy/drizzle/0179_amused_stingray.sql | 1 + apps/dokploy/drizzle/meta/0173_snapshot.json | 8465 ++++++++++++++++ apps/dokploy/drizzle/meta/0174_snapshot.json | 8472 ++++++++++++++++ apps/dokploy/drizzle/meta/0175_snapshot.json | 8479 ++++++++++++++++ apps/dokploy/drizzle/meta/0176_snapshot.json | 8479 ++++++++++++++++ apps/dokploy/drizzle/meta/0177_snapshot.json | 8479 ++++++++++++++++ apps/dokploy/drizzle/meta/0178_snapshot.json | 8493 +++++++++++++++++ apps/dokploy/drizzle/meta/0179_snapshot.json | 8493 +++++++++++++++++ apps/dokploy/drizzle/meta/_journal.json | 49 + apps/dokploy/pages/_app.tsx | 6 - apps/dokploy/pages/_document.tsx | 150 +- .../pages/dashboard/settings/license.tsx | 2 + apps/dokploy/pages/dashboard/settings/sso.tsx | 46 +- apps/dokploy/pages/index.tsx | 88 +- apps/dokploy/pages/register.tsx | 20 + apps/dokploy/server/api/routers/compose.ts | 17 +- .../server/api/routers/proprietary/sso.ts | 5 + .../api/routers/proprietary/whitelabeling.ts | 49 +- apps/dokploy/server/api/routers/settings.ts | 110 + .../src/db/schema/web-server-settings.ts | 45 + .../src/services/proprietary/license-key.ts | 34 +- .../src/services/web-server-settings.ts | 49 + 40 files changed, 60898 insertions(+), 475 deletions(-) create mode 100644 apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-help-links.tsx create mode 100644 apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-social-links.tsx create mode 100644 apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-sso-login.tsx create mode 100644 apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-sso.tsx create mode 100644 apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-whitelabeling.tsx create mode 100644 apps/dokploy/components/proprietary/license-keys/license-feature-settings.tsx delete mode 100644 apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx create mode 100644 apps/dokploy/drizzle/0173_chief_firedrake.sql create mode 100644 apps/dokploy/drizzle/0174_violet_mojo.sql create mode 100644 apps/dokploy/drizzle/0175_thick_starbolt.sql create mode 100644 apps/dokploy/drizzle/0176_bright_rhino.sql create mode 100644 apps/dokploy/drizzle/0177_elite_famine.sql create mode 100644 apps/dokploy/drizzle/0178_chemical_black_knight.sql create mode 100644 apps/dokploy/drizzle/0179_amused_stingray.sql create mode 100644 apps/dokploy/drizzle/meta/0173_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0174_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0175_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0176_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0177_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0178_snapshot.json create mode 100644 apps/dokploy/drizzle/meta/0179_snapshot.json diff --git a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts index ba09c2c80a..aebf5e5b5f 100644 --- a/apps/dokploy/__test__/traefik/server/update-server-config.test.ts +++ b/apps/dokploy/__test__/traefik/server/update-server-config.test.ts @@ -61,13 +61,27 @@ const baseSettings: WebServerSettings = { errorPageTitle: null, errorPageDescription: null, metaTitle: null, + metaDescription: null, + ogImageUrl: null, footerText: null, + passwordResetGuide: null, + supportEmail: null, + brandingEnabled: false, + appearanceEnabled: false, + metadataEnabled: false, + errorPagesEnabled: false, + forgotPasswordEnabled: false, }, cleanupCacheApplications: false, cleanupCacheOnCompose: false, cleanupCacheOnPreviews: false, remoteServersOnly: false, enforceSSO: false, + hideHelpLinks: false, + hideSocialLinks: false, + hideSSOLogin: false, + showSSOInSidebar: true, + showWhitelabelingInSidebar: true, createdAt: null, updatedAt: new Date(), }; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-help-links.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-help-links.tsx new file mode 100644 index 0000000000..c79428bc5e --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-help-links.tsx @@ -0,0 +1,48 @@ +import { HelpCircle } from "lucide-react"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; + +export const ToggleHideHelpLinks = () => { + const { data, refetch } = api.settings.getWebServerSettings.useQuery(); + const { mutateAsync } = api.settings.updateHideHelpLinks.useMutation(); + + const handleToggle = async (checked: boolean) => { + try { + await mutateAsync({ hideHelpLinks: checked }); + await refetch(); + toast.success("Hide Help Links updated"); + } catch { + toast.error("Error updating Hide Help Links"); + } + }; + + return ( +
+ + + + + + + +

+ When enabled, the Documentation and Support links are hidden from + the sidebar. +

+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-social-links.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-social-links.tsx new file mode 100644 index 0000000000..9f642ad2b2 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-social-links.tsx @@ -0,0 +1,51 @@ +import { HelpCircle } from "lucide-react"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; + +export const ToggleHideSocialLinks = () => { + const { data, refetch } = api.settings.getWebServerSettings.useQuery(); + const { mutateAsync } = api.settings.updateHideSocialLinks.useMutation(); + + const handleToggle = async (checked: boolean) => { + try { + await mutateAsync({ hideSocialLinks: checked }); + await refetch(); + toast.success("Hide Social Links updated"); + } catch { + toast.error("Error updating Hide Social Links"); + } + }; + + return ( +
+ + + + + + + +

+ When enabled, the GitHub, X, and Discord links are hidden from the + login and onboarding pages. +

+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-sso-login.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-sso-login.tsx new file mode 100644 index 0000000000..fa3e247d6f --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-hide-sso-login.tsx @@ -0,0 +1,48 @@ +import { HelpCircle } from "lucide-react"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; + +export const ToggleHideSSOLogin = () => { + const { data, refetch } = api.settings.getWebServerSettings.useQuery(); + const { mutateAsync } = api.settings.updateHideSSOLogin.useMutation(); + + const handleToggle = async (checked: boolean) => { + try { + await mutateAsync({ hideSSOLogin: checked }); + await refetch(); + toast.success("Hide SSO Login updated"); + } catch { + toast.error("Error updating Hide SSO Login"); + } + }; + + return ( +
+ + + + + + + +

+ When enabled, the "Sign in with SSO" option is hidden from the + login page. Has no effect when "Enforce SSO" is on. +

+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-sso.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-sso.tsx new file mode 100644 index 0000000000..65cc62199c --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-sso.tsx @@ -0,0 +1,48 @@ +import { HelpCircle } from "lucide-react"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; + +export const ToggleShowSSO = () => { + const { data, refetch } = api.settings.getWebServerSettings.useQuery(); + const { mutateAsync } = api.settings.updateShowSSOInSidebar.useMutation(); + + const handleToggle = async (checked: boolean) => { + try { + await mutateAsync({ showSSOInSidebar: checked }); + await refetch(); + toast.success("Single Sign-On (SSO) updated"); + } catch { + toast.error("Error updating Single Sign-On (SSO)"); + } + }; + + return ( +
+ + + + + + + +

When enabled, the SSO settings appear in the sidebar.

+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-whitelabeling.tsx b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-whitelabeling.tsx new file mode 100644 index 0000000000..7e213e71d9 --- /dev/null +++ b/apps/dokploy/components/dashboard/settings/servers/actions/toggle-show-whitelabeling.tsx @@ -0,0 +1,51 @@ +import { HelpCircle } from "lucide-react"; +import { toast } from "sonner"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { api } from "@/utils/api"; + +export const ToggleShowWhitelabeling = () => { + const { data, refetch } = api.settings.getWebServerSettings.useQuery(); + const { mutateAsync } = + api.settings.updateShowWhitelabelingInSidebar.useMutation(); + + const handleToggle = async (checked: boolean) => { + try { + await mutateAsync({ showWhitelabelingInSidebar: checked }); + await refetch(); + toast.success("Whitelabeling (Branding) updated"); + } catch { + toast.error("Error updating Whitelabeling (Branding)"); + } + }; + + return ( +
+ + + + + + + +

+ When enabled, the Whitelabeling settings appear in the sidebar. +

+
+
+
+
+ ); +}; diff --git a/apps/dokploy/components/layouts/onboarding-layout.tsx b/apps/dokploy/components/layouts/onboarding-layout.tsx index c76c920fde..e5d34c22e8 100644 --- a/apps/dokploy/components/layouts/onboarding-layout.tsx +++ b/apps/dokploy/components/layouts/onboarding-layout.tsx @@ -39,43 +39,45 @@ export const OnboardingLayout = ({ children }: Props) => {
{children}
-
- - - -
+ strokeWidth="0" + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + className="size-5" + > + + + + + + + )} ); diff --git a/apps/dokploy/components/layouts/side.tsx b/apps/dokploy/components/layouts/side.tsx index ba99df5db4..64a085646d 100644 --- a/apps/dokploy/components/layouts/side.tsx +++ b/apps/dokploy/components/layouts/side.tsx @@ -102,6 +102,9 @@ type EnabledOpts = { auth?: AuthQueryOutput; permissions?: PermissionsOutput; isCloud: boolean; + isEnterpriseActive?: boolean; + showSSOInSidebar?: boolean; + showWhitelabelingInSidebar?: boolean; }; type SingleNavItem = { @@ -410,16 +413,35 @@ const MENU: Menu = { title: "SSO", url: "/dashboard/settings/sso", icon: LogIn, - // Enabled for admins in both cloud and self-hosted (enterprise) - isEnabled: ({ permissions }) => !!permissions?.organization.update, + // Enabled for admins. On cloud SSO is always available; on self-hosted + // it requires an active enterprise license. Can also be hidden via the + // License page. + isEnabled: ({ + permissions, + isCloud, + isEnterpriseActive, + showSSOInSidebar, + }) => + !!permissions?.organization.update && + (isCloud || !!isEnterpriseActive) && + showSSOInSidebar !== false, }, { isSingle: true, title: "Whitelabeling", url: "/dashboard/settings/whitelabeling", icon: Palette, - // Only enabled for owners in non-cloud environments (enterprise) - isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud), + // Only enabled for owners in non-cloud environments with an active + // enterprise license. Can be hidden from the sidebar via the License page. + isEnabled: ({ + auth, + isCloud, + isEnterpriseActive, + showWhitelabelingInSidebar, + }) => + !!(auth?.role === "owner" && !isCloud) && + !!isEnterpriseActive && + showWhitelabelingInSidebar !== false, }, ], @@ -449,6 +471,10 @@ function createMenuForAuthUser(opts: { docsUrl?: string | null; supportUrl?: string | null; } | null; + hideHelpLinks?: boolean; + isEnterpriseActive?: boolean; + showSSOInSidebar?: boolean; + showWhitelabelingInSidebar?: boolean; }): Menu { const filterEnabled = < T extends { @@ -464,19 +490,26 @@ function createMenuForAuthUser(opts: { auth: opts.auth, permissions: opts.permissions, isCloud: opts.isCloud, + isEnterpriseActive: opts.isEnterpriseActive, + showSSOInSidebar: opts.showSSOInSidebar, + showWhitelabelingInSidebar: opts.showWhitelabelingInSidebar, }), ) as T[]; - // Apply whitelabeling URL overrides to help items - const helpItems = filterEnabled(MENU.help).map((item) => { - if (opts.whitelabeling?.docsUrl && item.name === "Documentation") { - return { ...item, url: opts.whitelabeling.docsUrl }; - } - if (opts.whitelabeling?.supportUrl && item.name === "Support") { - return { ...item, url: opts.whitelabeling.supportUrl }; - } - return item; - }); + // "Hide Help Links" is an enterprise restriction — only apply it when the + // license is active, otherwise always show the help links. + const helpItems = + opts.hideHelpLinks && opts.isEnterpriseActive + ? [] + : filterEnabled(MENU.help).map((item) => { + if (opts.whitelabeling?.docsUrl && item.name === "Documentation") { + return { ...item, url: opts.whitelabeling.docsUrl }; + } + if (opts.whitelabeling?.supportUrl && item.name === "Support") { + return { ...item, url: opts.whitelabeling.supportUrl }; + } + return item; + }); return { home: filterEnabled(MENU.home), @@ -592,7 +625,9 @@ function SidebarLogo() { )} > {/* Organization Logo and Selector */} - +
@@ -625,17 +660,17 @@ function SidebarLogo() {
-

+

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

@@ -907,6 +942,10 @@ export default function Page({ children }: Props) { staleTime: 5 * 60 * 1000, refetchOnWindowFocus: false, }); + const { data: webServerSettings } = + api.settings.getWebServerSettings.useQuery(); + const { data: haveValidLicense } = + api.licenseKey.haveValidLicenseKey.useQuery(); const includesProjects = pathname?.includes("/dashboard/project"); const { data: isCloud } = api.settings.isCloud.useQuery(); @@ -919,7 +958,16 @@ export default function Page({ children }: Props) { auth, permissions, isCloud: !!isCloud, - whitelabeling, + whitelabeling: whitelabeling?.metadataEnabled + ? { + docsUrl: whitelabeling.docsUrl, + supportUrl: whitelabeling.supportUrl, + } + : null, + hideHelpLinks: !!webServerSettings?.hideHelpLinks, + isEnterpriseActive: !!haveValidLicense, + showSSOInSidebar: webServerSettings?.showSSOInSidebar, + showWhitelabelingInSidebar: webServerSettings?.showWhitelabelingInSidebar, }); const activeItem = findActiveNavItem( @@ -943,8 +991,8 @@ export default function Page({ children }: Props) { }} style={ { - "--sidebar-width": "19.5rem", - "--sidebar-width-mobile": "19.5rem", + "--sidebar-width": "15rem", + "--sidebar-width-mobile": "18rem", } as React.CSSProperties } > @@ -1137,28 +1185,30 @@ export default function Page({ children }: Props) { })} - - Extra - - {help.map((item: ExternalLink) => ( - - - - - - - {item.name} - - - - ))} - - + {help.length > 0 && ( + + Extra + + {help.map((item: ExternalLink) => ( + + + + + + + {item.name} + + + + ))} + + + )} diff --git a/apps/dokploy/components/proprietary/license-keys/license-feature-settings.tsx b/apps/dokploy/components/proprietary/license-keys/license-feature-settings.tsx new file mode 100644 index 0000000000..652afdd7e7 --- /dev/null +++ b/apps/dokploy/components/proprietary/license-keys/license-feature-settings.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { ToggleEnforceSSO } from "@/components/dashboard/settings/servers/actions/toggle-enforce-sso"; +import { ToggleHideHelpLinks } from "@/components/dashboard/settings/servers/actions/toggle-hide-help-links"; +import { ToggleHideSocialLinks } from "@/components/dashboard/settings/servers/actions/toggle-hide-social-links"; +import { ToggleHideSSOLogin } from "@/components/dashboard/settings/servers/actions/toggle-hide-sso-login"; +import { ToggleRemoteServersOnly } from "@/components/dashboard/settings/servers/actions/toggle-remote-servers-only"; +import { ToggleShowSSO } from "@/components/dashboard/settings/servers/actions/toggle-show-sso"; +import { ToggleShowWhitelabeling } from "@/components/dashboard/settings/servers/actions/toggle-show-whitelabeling"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { api } from "@/utils/api"; + +/** + * Enterprise feature controls shown on the License page once a valid license is + * active. Self-hosted only (these settings don't apply to Dokploy Cloud). + */ +export function LicenseFeatureSettings() { + const { data: haveValidLicense } = + api.licenseKey.haveValidLicenseKey.useQuery(); + const { data: isCloud } = api.settings.isCloud.useQuery(); + + // Only relevant for self-hosted instances with an active license. + if (isCloud || !haveValidLicense) { + return null; + } + + return ( + <> + +
+ + Features + + Enable or disable enterprise features and control whether they + appear in the sidebar. + + + + + + +
+
+ + +
+ + Self-hosted Restrictions + + Control deployment targets and authentication behavior. + + + + + + + + + +
+
+ + ); +} diff --git a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx b/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx deleted file mode 100644 index a1a3275618..0000000000 --- a/apps/dokploy/components/proprietary/whitelabeling/whitelabeling-provider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import Head from "next/head"; -import { api } from "@/utils/api"; - -export function WhitelabelingProvider() { - const { data: config } = api.whitelabeling.getPublic.useQuery(undefined, { - staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, - }); - - if (!config) return null; - - return ( - <> - - {config.metaTitle && {config.metaTitle}} - {config.faviconUrl && } - - - {config.customCss && ( - - - Dokploy - - {getLayout()} diff --git a/apps/dokploy/pages/_document.tsx b/apps/dokploy/pages/_document.tsx index 120bb827e1..0abc72fa8a 100644 --- a/apps/dokploy/pages/_document.tsx +++ b/apps/dokploy/pages/_document.tsx @@ -1,10 +1,112 @@ -import { Head, Html, Main, NextScript } from "next/document"; +import { + getEffectiveWhitelabelingConfig, + getWebServerSettings, + hasValidLicenseForInstance, + IS_CLOUD, +} from "@dokploy/server"; +import NextDocument, { + type DocumentContext, + type DocumentInitialProps, + Head, + Html, + Main, + NextScript, +} from "next/document"; -export default function Document() { +interface WhitelabelingDocumentProps { + metaTitle: string | null; + metaDescription: string | null; + ogImageUrl: string | null; + faviconHref: string | null; + customCss: string | null; +} + +// Cache the resolved favicon (inlined as a data URI) so we don't re-fetch the +// remote image on every server render. Keyed by the configured favicon URL. +const FAVICON_CACHE_TTL = 60 * 60 * 1000; // 1 hour +const faviconCache = new Map(); + +/** + * Resolve the favicon to an inline data URI so it is present in the initial + * HTML and renders without a network round-trip (no flash of the default + * favicon). Falls back to the raw URL if the image can't be fetched. + */ +async function resolveFaviconHref( + faviconUrl: string | null | undefined, +): Promise { + if (!faviconUrl) return null; + + const cached = faviconCache.get(faviconUrl); + if (cached && cached.expiresAt > Date.now()) { + return cached.href; + } + + // Default to the raw URL so the custom favicon still loads if inlining fails. + let href = faviconUrl; + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 3000); + const response = await fetch(faviconUrl, { signal: controller.signal }); + clearTimeout(timeout); + + if (response.ok) { + const buffer = Buffer.from(await response.arrayBuffer()); + // Avoid embedding very large images directly in the HTML. + if (buffer.byteLength <= 512 * 1024) { + const contentType = response.headers.get("content-type") || "image/png"; + href = `data:${contentType};base64,${buffer.toString("base64")}`; + } + } + } catch { + // Keep the raw URL fallback. + } + + faviconCache.set(faviconUrl, { + href, + expiresAt: Date.now() + FAVICON_CACHE_TTL, + }); + return href; +} + +export default function Document({ + metaTitle, + metaDescription, + ogImageUrl, + faviconHref, + customCss, +}: WhitelabelingDocumentProps) { + const title = metaTitle || "Dokploy"; return ( - + {/* Rendered on the server so the correct branding is present on first + paint (and for social scrapers), avoiding a flash of / fallback to + the default Dokploy branding. */} + {title} + + + {metaDescription && ( + <> + + + + + )} + + {ogImageUrl && ( + <> + + + + + )} + + {customCss && ( +