Merge pull request #3716 from imran-vz/feat/quick-service-switcher

feat(ui): Add Vercel-style breadcrumb navigation with project and service switchers
This commit is contained in:
Mauricio Siu
2026-03-19 00:50:40 -06:00
committed by GitHub
23 changed files with 672 additions and 145 deletions

View File

@@ -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");

View File

@@ -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 */}
<img
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
className={cn(

View File

@@ -92,6 +92,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("");

View File

@@ -166,6 +166,11 @@ export const ShowProjects = () => {
return (
<>
{!isCloud && (
<div className="absolute top-4 right-4">
<TimeBadge />
</div>
)}
<BreadcrumbSidebar
list={[{ name: "Projects", href: "/dashboard/projects" }]}
/>
@@ -429,7 +434,7 @@ export const ShowProjects = () => {
</CardTitle>
</CardHeader>
<CardFooter className="pt-4">
<div className="space-y-1 text-sm flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}>
Created
</DateTooltip>

View File

@@ -14,13 +14,13 @@ export const extractExpirationDate = (certData: string): Date | null => {
// Helper: read ASN.1 length field
function readLength(pos: number): { length: number; offset: number } {
// biome-ignore lint/style/noParameterAssign: <explanation>
// 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: <explanation>
// biome-ignore lint/style/noParameterAssign: this is for dynamic length calculation
len = (len << 8) + der[pos++];
}
}

View File

@@ -908,6 +908,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={

View File

@@ -0,0 +1,628 @@
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 { type ComponentType, useEffect, useMemo, 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, type RouterOutputs } from "@/utils/api";
type ProjectItem = RouterOutputs["project"]["all"][number];
type ProjectEnvironment = ProjectItem["environments"][number];
type EnvironmentDetails = RouterOutputs["environment"]["one"];
type ServiceItem = {
id: string;
name: string;
type: ServiceType;
};
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 = (
environment: EnvironmentDetails | null | undefined,
): ServiceItem[] => {
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 = () => {
const router = useRouter();
const { query } = router;
// Read IDs from URL (dynamic route segments)
const projectId = getStringQueryParam(query.projectId);
const environmentId = getStringQueryParam(query.environmentId);
const serviceId =
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);
const [environmentOpen, setEnvironmentOpen] = useState(false);
const [projectSearch, setProjectSearch] = useState("");
const [serviceSearch, setServiceSearch] = useState("");
const [environmentSearch, setEnvironmentSearch] = useState("");
const [expandedProjectId, setExpandedProjectId] = useState<string | null>(
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);
}, []);
const services = useMemo(
() => extractServicesFromEnvironment(currentEnvironment),
[currentEnvironment],
);
const currentService = useMemo(
() => services.find((service) => service.id === serviceId),
[serviceId, services],
);
// Navigate to project's default environment
const handleProjectSelect = (
selectedProjectId: string,
selectedEnvironmentId?: string,
) => {
const project = allProjects?.find((p) => p.projectId === selectedProjectId);
if (project) {
const targetEnvironmentId = getTargetEnvironmentId(
project,
selectedEnvironmentId,
);
if (targetEnvironmentId) {
router.push(
`/dashboard/project/${selectedProjectId}/environment/${targetEnvironmentId}`,
);
}
}
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) => {
if (!environmentId) return;
router.push(
`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`,
);
setServiceOpen(false);
};
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],
);
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) {
return (
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<div className="flex items-center gap-2">
<FolderInput className="size-4 text-muted-foreground" />
<span className="font-medium">Projects</span>
</div>
</div>
</header>
);
}
return (
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<div className="flex items-center">
{/* Project Selector */}
<Popover open={projectOpen} onOpenChange={setProjectOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
aria-expanded={projectOpen}
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
<FolderInput className="size-4 text-muted-foreground" />
<span className="font-medium max-w-[150px] truncate">
{currentProject?.name || "Select Project"}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[380px] p-0"
align="start"
sideOffset={8}
>
<Command shouldFilter={false}>
<div className="relative">
<CommandInput
placeholder="Find Project..."
value={projectSearch}
onValueChange={setProjectSearch}
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
</kbd>
</div>
<CommandList>
<CommandEmpty>No projects found.</CommandEmpty>
<CommandGroup>
<ScrollArea className="h-[300px]">
{filteredProjects.map((project) => {
const totalServices = project.environments.reduce(
(total, env) => total + countEnvironmentServices(env),
0,
);
const isSelected = project.projectId === projectId;
const isExpanded =
expandedProjectId === project.projectId;
return (
<div key={project.projectId}>
<CommandItem
value={project.projectId}
onSelect={() => {
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"
>
<div className="flex items-center gap-3">
<div className="flex items-center justify-center size-8 rounded-md bg-muted text-xs font-semibold uppercase">
{project.name.slice(0, 2)}
</div>
<div className="flex flex-col">
<span className="font-medium">
{project.name}
</span>
<span className="text-muted-foreground">
{project.environments.length} env
{project.environments.length !== 1
? "s"
: ""}{" "}
· {totalServices} service
{totalServices !== 1 ? "s" : ""}
</span>
</div>
</div>
<div className="flex items-center gap-2">
{isSelected && (
<Check className="size-4 text-primary" />
)}
{project.environments.length > 1 && (
<ChevronRight
className={`size-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-90" : ""}`}
/>
)}
</div>
</CommandItem>
{/* Expanded environments */}
{isExpanded && (
<div className="ml-11 border-l pl-3 py-1 space-y-1">
{project.environments.map((env) => {
const envServices =
countEnvironmentServices(env);
const isEnvSelected =
env.environmentId === environmentId;
return (
<CommandItem
key={env.environmentId}
value={env.environmentId}
onSelect={() =>
handleProjectSelect(
project.projectId,
env.environmentId,
)
}
className="flex items-center justify-between py-2 px-2 cursor-pointer text-sm"
>
<div className="flex items-center gap-2">
<p className="text-xs">{env.name}</p>
<span className="text-xs text-muted-foreground">
{envServices} service
{envServices !== 1 ? "s" : ""}
</span>
</div>
{isEnvSelected && (
<Check className="size-3 text-primary" />
)}
</CommandItem>
);
})}
</div>
)}
</div>
);
})}
</ScrollArea>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Environment Selector */}
{projectEnvironments && projectEnvironments.length > 1 && (
<Popover open={environmentOpen} onOpenChange={setEnvironmentOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
aria-expanded={environmentOpen}
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
<span className="font-medium max-w-[150px] truncate">
{currentEnvironment?.name || "production"}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[350px] p-0"
align="start"
sideOffset={8}
>
<Command shouldFilter={false}>
<div className="relative">
<CommandInput
placeholder="Find Environment..."
value={environmentSearch}
onValueChange={setEnvironmentSearch}
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
</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>
)}
{projectEnvironments && projectEnvironments.length === 1 && (
<p className="text-xs font-normal ml-1">
{currentEnvironment?.name || "production"}
</p>
)}
{/* Service Selector - only show when viewing a service */}
{serviceId && currentService && (
<>
<Separator orientation="vertical" className="mx-2 h-6" />
<Popover open={serviceOpen} onOpenChange={setServiceOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
aria-expanded={serviceOpen}
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
{getServiceIcon(currentService.type)}
<span className="font-medium max-w-[150px] truncate">
{currentService.name}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[350px] p-0"
align="start"
sideOffset={8}
>
<Command shouldFilter={false}>
<div className="relative">
<CommandInput
placeholder="Find Service..."
value={serviceSearch}
onValueChange={setServiceSearch}
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
</kbd>
</div>
<CommandList>
<CommandEmpty>No services found.</CommandEmpty>
<CommandGroup>
<ScrollArea className="h-[300px]">
{filteredServices.map((service) => {
const isSelected = service.id === serviceId;
return (
<CommandItem
key={service.id}
value={service.id}
onSelect={() => handleServiceSelect(service)}
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">
{getServiceIcon(service.type)}
</div>
<div className="flex flex-col">
<span className="font-medium">
{service.name}
</span>
<span className="text-xs text-muted-foreground capitalize">
{service.type}
</span>
</div>
</div>
{isSelected && (
<Check className="size-4 text-primary" />
)}
</CommandItem>
);
})}
</ScrollArea>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{/* Close button to go back to environment */}
<Button
variant="ghost"
size="icon"
className="size-7 ml-1"
onClick={() => {
router.push(
`/dashboard/project/${projectId}/environment/${environmentId}`,
);
}}
>
<X className="size-4 text-muted-foreground" />
</Button>
</>
)}
</div>
</div>
</header>
);
};

View File

@@ -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
<img
src={logoUrl}
alt="Organization Logo"

View File

@@ -82,7 +82,7 @@ const SidebarProvider = React.forwardRef<
_setOpen(value);
// This sets the cookie to keep the sidebar state.
// biome-ignore lint/suspicious/noDocumentCookie: This sets the cookie to keep the sidebar state.
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
},
[setOpenProp, open],

View File

@@ -38,3 +38,5 @@ export const redirectWithError = (res: NextApiResponse, error: string) => {
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
);
};
export default findGitea;

View File

@@ -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";
@@ -861,18 +861,7 @@ const EnvironmentPage = (
return (
<div>
<BreadcrumbSidebar
list={[
{ name: "Projects", href: "/dashboard/projects" },
{
name: projectData?.name || "",
},
{
name: currentEnvironment.name,
dropdownItems: environmentDropdownItems,
},
]}
/>
<AdvanceBreadcrumb />
<Head>
<title>
Environment: {currentEnvironment.name} | {projectData?.name} |{" "}

View File

@@ -35,7 +35,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 {
@@ -108,22 +108,7 @@ 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 />
<Head>
<title>
Application: {data?.name} - {data?.environment.project.name} |{" "}

View File

@@ -31,7 +31,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 {
@@ -97,22 +97,7 @@ 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 />
<Head>
<title>
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}

View File

@@ -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 {
@@ -78,22 +78,7 @@ 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 />
<div className="flex flex-col gap-4">
<Head>
<title>

View File

@@ -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 {
@@ -77,22 +77,7 @@ 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 />
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |{" "}

View File

@@ -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 {
@@ -76,22 +76,7 @@ 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 />
<div className="flex flex-col gap-4">
<Head>
<title>

View File

@@ -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 {
@@ -76,22 +76,7 @@ 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 />
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |{" "}

View File

@@ -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 {
@@ -76,22 +76,7 @@ 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 />
<Head>
<title>
Database: {data?.name} - {data?.environment?.project?.name} |{" "}

View File

@@ -23,15 +23,15 @@ import { mysqlRouter } from "./routers/mysql";
import { notificationRouter } from "./routers/notification";
import { organizationRouter } from "./routers/organization";
import { patchRouter } from "./routers/patch";
import { portRouter } from "./routers/port";
import { postgresRouter } from "./routers/postgres";
import { previewDeploymentRouter } from "./routers/preview-deployment";
import { projectRouter } from "./routers/project";
import { auditLogRouter } from "./routers/proprietary/audit-log";
import { customRoleRouter } from "./routers/proprietary/custom-role";
import { licenseKeyRouter } from "./routers/proprietary/license-key";
import { ssoRouter } from "./routers/proprietary/sso";
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
import { portRouter } from "./routers/port";
import { postgresRouter } from "./routers/postgres";
import { previewDeploymentRouter } from "./routers/preview-deployment";
import { projectRouter } from "./routers/project";
import { redirectsRouter } from "./routers/redirects";
import { redisRouter } from "./routers/redis";
import { registryRouter } from "./routers/registry";

View File

@@ -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";

View File

@@ -48973,4 +48973,4 @@
"apiKey": []
}
]
}
}

View File

@@ -45,7 +45,7 @@ export const createDomain = async (input: z.infer<typeof apiCreateDomain>) => {
export const generateTraefikMeDomain = async (
appName: string,
userId: string,
_userId: string,
serverId?: string,
) => {
if (serverId) {

View File

@@ -236,7 +236,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 "*."');