Skip to content

Commit 1804b93

Browse files
authored
Merge pull request #3959 from Dokploy/feat/add-new-whitelabeling
Feat/add new whitelabeling
2 parents 6e67864 + 985c910 commit 1804b93

File tree

35 files changed

+8687
-126
lines changed

35 files changed

+8687
-126
lines changed

apps/dokploy/__test__/traefik/server/update-server-config.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,20 @@ const baseSettings: WebServerSettings = {
4848
urlCallback: "",
4949
},
5050
},
51+
whitelabelingConfig: {
52+
appName: null,
53+
appDescription: null,
54+
logoUrl: null,
55+
faviconUrl: null,
56+
customCss: null,
57+
loginLogoUrl: null,
58+
supportUrl: null,
59+
docsUrl: null,
60+
errorPageTitle: null,
61+
errorPageDescription: null,
62+
metaTitle: null,
63+
footerText: null,
64+
},
5165
cleanupCacheApplications: false,
5266
cleanupCacheOnCompose: false,
5367
cleanupCacheOnPreviews: false,

apps/dokploy/components/dashboard/impersonation/impersonation-bar.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ import {
4545
import { authClient } from "@/lib/auth-client";
4646
import { cn } from "@/lib/utils";
4747
import { api } from "@/utils/api";
48+
import { useWhitelabeling } from "@/utils/hooks/use-whitelabeling";
4849

4950
type User = typeof authClient.$Infer.Session.user;
5051

5152
export const ImpersonationBar = () => {
53+
const { config: whitelabeling } = useWhitelabeling();
5254
const [users, setUsers] = useState<User[]>([]);
5355
const [selectedUser, setSelectedUser] = useState<User | null>(null);
5456
const [isImpersonating, setIsImpersonating] = useState(false);
@@ -180,7 +182,10 @@ export const ImpersonationBar = () => {
180182
)}
181183
>
182184
<div className="flex items-center gap-4 px-4 md:px-20 w-full">
183-
<Logo className="w-10 h-10" />
185+
<Logo
186+
className="w-10 h-10"
187+
logoUrl={whitelabeling?.logoUrl || undefined}
188+
/>
184189
{!isImpersonating ? (
185190
<div className="flex items-center gap-2 w-full">
186191
<Popover open={open} onOpenChange={setOpen}>

apps/dokploy/components/layouts/onboarding-layout.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Link from "next/link";
22
import type React from "react";
33
import { cn } from "@/lib/utils";
4+
import { useWhitelabelingPublic } from "@/utils/hooks/use-whitelabeling";
45
import { GithubIcon } from "../icons/data-tools-icons";
56
import { Logo } from "../shared/logo";
67
import { Button } from "../ui/button";
@@ -9,23 +10,28 @@ interface Props {
910
children: React.ReactNode;
1011
}
1112
export const OnboardingLayout = ({ children }: Props) => {
13+
const { config: whitelabeling } = useWhitelabelingPublic();
14+
const appName = whitelabeling?.appName || "Dokploy";
15+
const appDescription =
16+
whitelabeling?.appDescription ||
17+
"\u201CThe Open Source alternative to Netlify, Vercel, Heroku.\u201D";
18+
const logoUrl =
19+
whitelabeling?.loginLogoUrl || whitelabeling?.logoUrl || undefined;
20+
1221
return (
1322
<div className="container relative min-h-svh flex-col items-center justify-center flex lg:max-w-none lg:grid lg:grid-cols-2 lg:px-0 w-full">
1423
<div className="relative hidden h-full flex-col p-10 text-primary dark:border-r lg:flex">
1524
<div className="absolute inset-0 bg-muted" />
1625
<Link
17-
href="https://dokploy.com"
26+
href="/"
1827
className="relative z-20 flex items-center text-lg font-medium gap-4 text-primary"
1928
>
20-
<Logo className="size-10" />
21-
Dokploy
29+
<Logo className="size-10" logoUrl={logoUrl} />
30+
{appName}
2231
</Link>
2332
<div className="relative z-20 mt-auto">
2433
<blockquote className="space-y-2">
25-
<p className="text-lg text-primary">
26-
&ldquo;The Open Source alternative to Netlify, Vercel,
27-
Heroku.&rdquo;
28-
</p>
34+
<p className="text-lg text-primary">{appDescription}</p>
2935
</blockquote>
3036
</div>
3137
</div>

apps/dokploy/components/layouts/side.tsx

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
LogIn,
2525
type LucideIcon,
2626
Package,
27+
Palette,
2728
PieChart,
2829
Rocket,
2930
Server,
@@ -422,6 +423,14 @@ const MENU: Menu = {
422423
isEnabled: ({ auth }) =>
423424
!!(auth?.role === "owner" || auth?.role === "admin"),
424425
},
426+
{
427+
isSingle: true,
428+
title: "Whitelabeling",
429+
url: "/dashboard/settings/whitelabeling",
430+
icon: Palette,
431+
// Only enabled for owners in non-cloud environments (enterprise)
432+
isEnabled: ({ auth, isCloud }) => !!(auth?.role === "owner" && !isCloud),
433+
},
425434
],
426435

427436
help: [
@@ -445,38 +454,39 @@ const MENU: Menu = {
445454
function createMenuForAuthUser(opts: {
446455
auth?: AuthQueryOutput;
447456
isCloud: boolean;
457+
whitelabeling?: {
458+
docsUrl?: string | null;
459+
supportUrl?: string | null;
460+
} | null;
448461
}): Menu {
449-
return {
450-
// Filter the home items based on the user's role and permissions
451-
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
452-
home: MENU.home.filter((item) =>
453-
!item.isEnabled
454-
? true
455-
: item.isEnabled({
456-
auth: opts.auth,
457-
isCloud: opts.isCloud,
458-
}),
459-
),
460-
// Filter the settings items based on the user's role and permissions
461-
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
462-
settings: MENU.settings.filter((item) =>
463-
!item.isEnabled
464-
? true
465-
: item.isEnabled({
466-
auth: opts.auth,
467-
isCloud: opts.isCloud,
468-
}),
469-
),
470-
// Filter the help items based on the user's role and permissions
471-
// Calls the `isEnabled` function if it exists to determine if the item should be displayed
472-
help: MENU.help.filter((item) =>
462+
const filterEnabled = <
463+
T extends {
464+
isEnabled?: (o: { auth?: AuthQueryOutput; isCloud: boolean }) => boolean;
465+
},
466+
>(
467+
items: readonly T[],
468+
): T[] =>
469+
items.filter((item) =>
473470
!item.isEnabled
474471
? true
475-
: item.isEnabled({
476-
auth: opts.auth,
477-
isCloud: opts.isCloud,
478-
}),
479-
),
472+
: item.isEnabled({ auth: opts.auth, isCloud: opts.isCloud }),
473+
) as T[];
474+
475+
// Apply whitelabeling URL overrides to help items
476+
const helpItems = filterEnabled(MENU.help).map((item) => {
477+
if (opts.whitelabeling?.docsUrl && item.name === "Documentation") {
478+
return { ...item, url: opts.whitelabeling.docsUrl };
479+
}
480+
if (opts.whitelabeling?.supportUrl && item.name === "Support") {
481+
return { ...item, url: opts.whitelabeling.supportUrl };
482+
}
483+
return item;
484+
});
485+
486+
return {
487+
home: filterEnabled(MENU.home),
488+
settings: filterEnabled(MENU.settings),
489+
help: helpItems,
480490
};
481491
}
482492

@@ -885,6 +895,10 @@ export default function Page({ children }: Props) {
885895
const pathname = usePathname();
886896
const { data: auth } = api.user.get.useQuery();
887897
const { data: dokployVersion } = api.settings.getDokployVersion.useQuery();
898+
const { data: whitelabeling } = api.whitelabeling.get.useQuery(undefined, {
899+
staleTime: 5 * 60 * 1000,
900+
refetchOnWindowFocus: false,
901+
});
888902

889903
const includesProjects = pathname?.includes("/dashboard/project");
890904
const { data: isCloud } = api.settings.isCloud.useQuery();
@@ -893,7 +907,7 @@ export default function Page({ children }: Props) {
893907
home: filteredHome,
894908
settings: filteredSettings,
895909
help,
896-
} = createMenuForAuthUser({ auth, isCloud: !!isCloud });
910+
} = createMenuForAuthUser({ auth, isCloud: !!isCloud, whitelabeling });
897911

898912
const activeItem = findActiveNavItem(
899913
[...filteredHome, ...filteredSettings],
@@ -1141,6 +1155,11 @@ export default function Page({ children }: Props) {
11411155
<SidebarMenuItem>
11421156
<UserNav />
11431157
</SidebarMenuItem>
1158+
{whitelabeling?.footerText && (
1159+
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
1160+
{whitelabeling.footerText}
1161+
</div>
1162+
)}
11441163
{dokployVersion && (
11451164
<>
11461165
<div className="px-3 text-xs text-muted-foreground text-center group-data-[collapsible=icon]:hidden">
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"use client";
2+
3+
import {
4+
Card,
5+
CardContent,
6+
CardDescription,
7+
CardHeader,
8+
CardTitle,
9+
} from "@/components/ui/card";
10+
11+
interface WhitelabelingPreviewProps {
12+
config: {
13+
appName?: string;
14+
logoUrl?: string;
15+
footerText?: string;
16+
};
17+
}
18+
19+
export function WhitelabelingPreview({ config }: WhitelabelingPreviewProps) {
20+
const appName = config.appName || "Dokploy";
21+
22+
return (
23+
<Card className="bg-transparent">
24+
<CardHeader>
25+
<CardTitle>Live Preview</CardTitle>
26+
<CardDescription>
27+
A quick preview of how your branding changes will look.
28+
</CardDescription>
29+
</CardHeader>
30+
<CardContent>
31+
<div className="rounded-lg border overflow-hidden">
32+
{/* Simulated sidebar header */}
33+
<div className="flex items-center gap-3 p-4 border-b bg-sidebar">
34+
{config.logoUrl ? (
35+
<img
36+
src={config.logoUrl}
37+
alt="Preview Logo"
38+
className="size-8 rounded-sm object-contain"
39+
/>
40+
) : (
41+
<div className="size-8 rounded-sm flex items-center justify-center bg-primary text-primary-foreground font-bold text-sm">
42+
{appName.charAt(0).toUpperCase()}
43+
</div>
44+
)}
45+
<span className="font-semibold text-sm">{appName}</span>
46+
</div>
47+
48+
{/* Simulated content area */}
49+
<div className="p-4 bg-background">
50+
<div className="flex items-center gap-2 mb-3">
51+
<div className="h-2 w-16 rounded-full bg-primary" />
52+
<div className="h-2 w-24 rounded-full bg-muted" />
53+
</div>
54+
<div className="flex gap-2">
55+
<div className="px-3 py-1.5 rounded-md text-xs bg-primary text-primary-foreground font-medium">
56+
Button
57+
</div>
58+
<div className="px-3 py-1.5 rounded-md text-xs border font-medium">
59+
Secondary
60+
</div>
61+
</div>
62+
</div>
63+
64+
{/* Simulated footer */}
65+
{config.footerText && (
66+
<div className="px-4 py-2 border-t text-xs text-muted-foreground text-center bg-sidebar">
67+
{config.footerText}
68+
</div>
69+
)}
70+
</div>
71+
</CardContent>
72+
</Card>
73+
);
74+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"use client";
2+
3+
import Head from "next/head";
4+
import { api } from "@/utils/api";
5+
6+
export function WhitelabelingProvider() {
7+
const { data: config } = api.whitelabeling.getPublic.useQuery(undefined, {
8+
staleTime: 5 * 60 * 1000,
9+
refetchOnWindowFocus: false,
10+
});
11+
12+
if (!config) return null;
13+
14+
return (
15+
<>
16+
<Head>
17+
{config.metaTitle && <title>{config.metaTitle}</title>}
18+
{config.faviconUrl && <link rel="icon" href={config.faviconUrl} />}
19+
</Head>
20+
21+
{config.customCss && (
22+
<style
23+
id="whitelabeling-styles"
24+
dangerouslySetInnerHTML={{
25+
__html: config.customCss,
26+
}}
27+
/>
28+
)}
29+
</>
30+
);
31+
}

0 commit comments

Comments
 (0)