mirror of
https://github.com/andwati/dokploy.git
synced 2026-11-15 08:25:16 +00:00
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:
@@ -79,7 +79,7 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
api.compose.create.useMutation();
|
api.compose.create.useMutation();
|
||||||
|
|
||||||
// Get environment data to extract projectId
|
// 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;
|
const hasServers = servers && servers.length > 0;
|
||||||
// Show dropdown logic based on cloud environment
|
// Show dropdown logic based on cloud environment
|
||||||
@@ -117,6 +117,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
|
|||||||
await utils.environment.one.invalidate({
|
await utils.environment.one.invalidate({
|
||||||
environmentId,
|
environmentId,
|
||||||
});
|
});
|
||||||
|
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
||||||
|
await utils.project.all.invalidate();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
toast.error("Error creating the compose");
|
toast.error("Error creating the compose");
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ export const AddTemplate = ({ environmentId, baseUrl }: Props) => {
|
|||||||
viewMode === "detailed" && "border-b",
|
viewMode === "detailed" && "border-b",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{/** biome-ignore lint/performance/noImgElement: this is a valid use for img tag */}
|
||||||
<img
|
<img
|
||||||
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
|
src={`${customBaseUrl || "https://templates.dokploy.com/"}/blueprints/${template?.id}/${template?.logo}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ export const AdvancedEnvironmentSelector = ({
|
|||||||
|
|
||||||
toast.success("Environment created successfully");
|
toast.success("Environment created successfully");
|
||||||
utils.environment.byProjectId.invalidate({ projectId });
|
utils.environment.byProjectId.invalidate({ projectId });
|
||||||
|
// Invalidate the project query to refresh the project data for the advance-breadcrumb
|
||||||
|
utils.project.all.invalidate();
|
||||||
setIsCreateDialogOpen(false);
|
setIsCreateDialogOpen(false);
|
||||||
setName("");
|
setName("");
|
||||||
setDescription("");
|
setDescription("");
|
||||||
|
|||||||
@@ -166,6 +166,11 @@ export const ShowProjects = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{!isCloud && (
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<TimeBadge />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<BreadcrumbSidebar
|
<BreadcrumbSidebar
|
||||||
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
list={[{ name: "Projects", href: "/dashboard/projects" }]}
|
||||||
/>
|
/>
|
||||||
@@ -429,7 +434,7 @@ export const ShowProjects = () => {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="pt-4">
|
<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}>
|
<DateTooltip date={project.createdAt}>
|
||||||
Created
|
Created
|
||||||
</DateTooltip>
|
</DateTooltip>
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ export const extractExpirationDate = (certData: string): Date | null => {
|
|||||||
|
|
||||||
// Helper: read ASN.1 length field
|
// Helper: read ASN.1 length field
|
||||||
function readLength(pos: number): { length: number; offset: number } {
|
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++];
|
let len = der[pos++];
|
||||||
if (len & 0x80) {
|
if (len & 0x80) {
|
||||||
const bytes = len & 0x7f;
|
const bytes = len & 0x7f;
|
||||||
len = 0;
|
len = 0;
|
||||||
for (let i = 0; i < bytes; i++) {
|
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++];
|
len = (len << 8) + der[pos++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -908,6 +908,7 @@ export default function Page({ children }: Props) {
|
|||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
setDefaultOpen(open);
|
setDefaultOpen(open);
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noDocumentCookie: this sets the cookie to keep the sidebar state.
|
||||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}`;
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ interface Props {
|
|||||||
export const Logo = ({ className = "size-14", logoUrl }: Props) => {
|
export const Logo = ({ className = "size-14", logoUrl }: Props) => {
|
||||||
if (logoUrl) {
|
if (logoUrl) {
|
||||||
return (
|
return (
|
||||||
|
// biome-ignore lint/performance/noImgElement: this is for dynamic logo loading
|
||||||
<img
|
<img
|
||||||
src={logoUrl}
|
src={logoUrl}
|
||||||
alt="Organization Logo"
|
alt="Organization Logo"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ const SidebarProvider = React.forwardRef<
|
|||||||
|
|
||||||
_setOpen(value);
|
_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}`;
|
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||||
},
|
},
|
||||||
[setOpenProp, open],
|
[setOpenProp, open],
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ export const redirectWithError = (res: NextApiResponse, error: string) => {
|
|||||||
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
|
`/dashboard/settings/git-providers?error=${encodeURIComponent(error)}`,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default findGitea;
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ import {
|
|||||||
RedisIcon,
|
RedisIcon,
|
||||||
} from "@/components/icons/data-tools-icons";
|
} from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { BreadcrumbSidebar } from "@/components/shared/breadcrumb-sidebar";
|
|
||||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||||
@@ -861,18 +861,7 @@ const EnvironmentPage = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
list={[
|
|
||||||
{ name: "Projects", href: "/dashboard/projects" },
|
|
||||||
{
|
|
||||||
name: projectData?.name || "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: currentEnvironment.name,
|
|
||||||
dropdownItems: environmentDropdownItems,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Environment: {currentEnvironment.name} | {projectData?.name} |{" "}
|
Environment: {currentEnvironment.name} | {projectData?.name} |{" "}
|
||||||
|
|||||||
+2
-17
@@ -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 { ContainerFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-container-monitoring";
|
||||||
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-container-monitoring";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -108,22 +108,7 @@ const Service = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="application" />
|
<UseKeyboardNav forPage="application" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Application: {data?.name} - {data?.environment.project.name} |{" "}
|
Application: {data?.name} - {data?.environment.project.name} |{" "}
|
||||||
|
|||||||
+2
-17
@@ -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 { ComposeFreeMonitoring } from "@/components/dashboard/monitoring/free/container/show-free-compose-monitoring";
|
||||||
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
import { ComposePaidMonitoring } from "@/components/dashboard/monitoring/paid/container/show-paid-compose-monitoring";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -97,22 +97,7 @@ const Service = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="compose" />
|
<UseKeyboardNav forPage="compose" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}
|
Compose: {data?.name} - {data?.environment?.project?.name} | {appName}
|
||||||
|
|||||||
+2
-17
@@ -23,7 +23,7 @@ import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/
|
|||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
import { MariadbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -78,22 +78,7 @@ const Mariadb = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="mariadb" />
|
<UseKeyboardNav forPage="mariadb" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
|
|||||||
+2
-17
@@ -23,7 +23,7 @@ import { ContainerPaidMonitoring } from "@/components/dashboard/monitoring/paid/
|
|||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
import { MongodbIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -77,22 +77,7 @@ const Mongo = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="mongodb" />
|
<UseKeyboardNav forPage="mongodb" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
|||||||
+2
-17
@@ -23,7 +23,7 @@ import { UpdateMysql } from "@/components/dashboard/mysql/update-mysql";
|
|||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
import { MysqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -76,22 +76,7 @@ const MySql = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="mysql" />
|
<UseKeyboardNav forPage="mysql" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
|
|||||||
+2
-17
@@ -23,7 +23,7 @@ import { UpdatePostgres } from "@/components/dashboard/postgres/update-postgres"
|
|||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
import { PostgresqlIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -76,22 +76,7 @@ const Postgresql = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="postgres" />
|
<UseKeyboardNav forPage="postgres" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
|||||||
+2
-17
@@ -22,7 +22,7 @@ import { UpdateRedis } from "@/components/dashboard/redis/update-redis";
|
|||||||
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
import { ShowDatabaseAdvancedSettings } from "@/components/dashboard/shared/show-database-advanced-settings";
|
||||||
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
import { RedisIcon } from "@/components/icons/data-tools-icons";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
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 { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -76,22 +76,7 @@ const Redis = (
|
|||||||
return (
|
return (
|
||||||
<div className="pb-10">
|
<div className="pb-10">
|
||||||
<UseKeyboardNav forPage="redis" />
|
<UseKeyboardNav forPage="redis" />
|
||||||
<BreadcrumbSidebar
|
<AdvanceBreadcrumb />
|
||||||
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 || "",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>
|
<title>
|
||||||
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
Database: {data?.name} - {data?.environment?.project?.name} |{" "}
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ import { mysqlRouter } from "./routers/mysql";
|
|||||||
import { notificationRouter } from "./routers/notification";
|
import { notificationRouter } from "./routers/notification";
|
||||||
import { organizationRouter } from "./routers/organization";
|
import { organizationRouter } from "./routers/organization";
|
||||||
import { patchRouter } from "./routers/patch";
|
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 { auditLogRouter } from "./routers/proprietary/audit-log";
|
||||||
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
import { customRoleRouter } from "./routers/proprietary/custom-role";
|
||||||
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
import { licenseKeyRouter } from "./routers/proprietary/license-key";
|
||||||
import { ssoRouter } from "./routers/proprietary/sso";
|
import { ssoRouter } from "./routers/proprietary/sso";
|
||||||
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
|
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 { redirectsRouter } from "./routers/redirects";
|
||||||
import { redisRouter } from "./routers/redis";
|
import { redisRouter } from "./routers/redis";
|
||||||
import { registryRouter } from "./routers/registry";
|
import { registryRouter } from "./routers/registry";
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { exit } from "node:process";
|
|
||||||
import { exec } from "node:child_process";
|
import { exec } from "node:child_process";
|
||||||
|
import { exit } from "node:process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||||
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||||
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||||
|
|||||||
+1
-1
@@ -48973,4 +48973,4 @@
|
|||||||
"apiKey": []
|
"apiKey": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const createDomain = async (input: z.infer<typeof apiCreateDomain>) => {
|
|||||||
|
|
||||||
export const generateTraefikMeDomain = async (
|
export const generateTraefikMeDomain = async (
|
||||||
appName: string,
|
appName: string,
|
||||||
userId: string,
|
_userId: string,
|
||||||
serverId?: string,
|
serverId?: string,
|
||||||
) => {
|
) => {
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ const generateWildcardDomain = async (
|
|||||||
baseDomain: string,
|
baseDomain: string,
|
||||||
appName: string,
|
appName: string,
|
||||||
serverIp: string,
|
serverIp: string,
|
||||||
userId: string,
|
_userId: string,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (!baseDomain.startsWith("*.")) {
|
if (!baseDomain.startsWith("*.")) {
|
||||||
throw new Error('The base domain must start with "*."');
|
throw new Error('The base domain must start with "*."');
|
||||||
|
|||||||
Reference in New Issue
Block a user