mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat: initial commit
This commit is contained in:
29
components/layouts/dashboard-layout.tsx
Normal file
29
components/layouts/dashboard-layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Navbar } from "./navbar";
|
||||
import { NavigationTabs, type TabState } from "./navigation-tabs";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
tab: TabState;
|
||||
}
|
||||
|
||||
export const DashboardLayout = ({ children, tab }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="bg-radial relative flex flex-col bg-background pt-6"
|
||||
id="app-container"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="w-full">
|
||||
<Navbar />
|
||||
<main className="mt-6 flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-8xl px-4 lg:px-8">
|
||||
<NavigationTabs tab={tab}>{children}</NavigationTabs>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
137
components/layouts/navbar.tsx
Normal file
137
components/layouts/navbar.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { Logo } from "../shared/logo";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { useRouter } from "next/router";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export const Navbar = () => {
|
||||
const router = useRouter();
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
const { mutateAsync } = api.auth.logout.useMutation();
|
||||
return (
|
||||
<nav className="border-divider sticky inset-x-0 top-0 z-40 flex h-auto w-full items-center justify-center border-b bg-background/70 backdrop-blur-lg backdrop-saturate-150 data-[menu-open=true]:border-none data-[menu-open=true]:backdrop-blur-xl">
|
||||
<header className="relative z-40 flex h-[var(--navbar-height)] w-full max-w-8xl flex-row flex-nowrap items-center justify-between gap-4 px-4 sm:px-6">
|
||||
<div className="text-medium box-border flex flex-grow basis-0 flex-row flex-nowrap items-center justify-start whitespace-nowrap bg-transparent no-underline">
|
||||
<Link
|
||||
href="/dashboard/projects"
|
||||
className={cn("flex flex-row items-center gap-2")}
|
||||
>
|
||||
<Logo />
|
||||
<span className="text-sm font-semibold text-primary">Dokploy</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul
|
||||
className="ml-auto flex h-12 max-w-fit flex-row flex-nowrap items-center gap-0 data-[justify=end]:flex-grow data-[justify=start]:flex-grow data-[justify=end]:basis-0 data-[justify=start]:basis-0 data-[justify=start]:justify-start data-[justify=end]:justify-end data-[justify=center]:justify-center"
|
||||
data-justify="end"
|
||||
>
|
||||
<li className="text-medium mr-2 box-border hidden list-none whitespace-nowrap data-[active=true]:font-semibold data-[active=true]:text-primary lg:flex">
|
||||
{/* <Badge>PRO</Badge> */}
|
||||
</li>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Avatar className="size-10 cursor-pointer border border-border items-center">
|
||||
<AvatarImage src={data?.image || ""} alt="@shadcn" />
|
||||
<AvatarFallback>
|
||||
{data?.email
|
||||
?.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-56" align="end">
|
||||
<DropdownMenuLabel className="flex flex-col">
|
||||
My Account
|
||||
<span className="text-xs font-normal text-muted-foreground">
|
||||
{data?.email}
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/projects");
|
||||
}}
|
||||
>
|
||||
Projects
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/monitoring");
|
||||
}}
|
||||
>
|
||||
Monitoring
|
||||
</DropdownMenuItem>
|
||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/traefik");
|
||||
}}
|
||||
>
|
||||
Traefik
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/docker", undefined, {
|
||||
shallow: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Docker
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
router.push("/dashboard/settings/server");
|
||||
}}
|
||||
>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
await mutateAsync().then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
}}
|
||||
>
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</ul>
|
||||
</header>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
121
components/layouts/navigation-tabs.tsx
Normal file
121
components/layouts/navigation-tabs.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { AddProject } from "@/components/dashboard/projects/add";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export type TabState =
|
||||
| "projects"
|
||||
| "monitoring"
|
||||
| "settings"
|
||||
| "traefik"
|
||||
| "docker";
|
||||
|
||||
interface Props {
|
||||
tab: TabState;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const NavigationTabs = ({ tab, children }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const [activeTab, setActiveTab] = useState<TabState>(tab);
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveTab(tab);
|
||||
}, [tab]);
|
||||
|
||||
return (
|
||||
<div className="gap-12 min-h-screen">
|
||||
<header className="mb-6 flex w-full items-center gap-2 justify-between flex-wrap">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="text-xl font-bold lg:text-3xl">
|
||||
{tab === "projects" && "Projects"}
|
||||
{tab === "monitoring" && "Monitoring"}
|
||||
{tab === "settings" && "Settings"}
|
||||
{tab === "traefik" && "Traefik"}
|
||||
{tab === "docker" && "Docker"}
|
||||
</h1>
|
||||
<p className="lg:text-medium text-muted-foreground">
|
||||
{tab === "projects" && "Manage your deployments"}
|
||||
{tab === "monitoring" && "Watch the usage of your server"}
|
||||
{tab === "settings" && "Check the configuration"}
|
||||
{tab === "traefik" && "Read the traefik config and update it"}
|
||||
{tab === "docker" && "Manage the docker containers"}
|
||||
</p>
|
||||
</div>
|
||||
{tab === "projects" &&
|
||||
(data?.rol === "admin" || user?.canCreateProjects) && <AddProject />}
|
||||
</header>
|
||||
<div className="flex w-full justify-between gap-8 ">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
className="w-full"
|
||||
onValueChange={(e) => {
|
||||
if (e === "settings") {
|
||||
router.push("/dashboard/settings/server");
|
||||
} else {
|
||||
router.push(`/dashboard/${e}`);
|
||||
}
|
||||
setActiveTab(e as TabState);
|
||||
}}
|
||||
>
|
||||
{/* className="grid w-fit grid-cols-4 bg-transparent" */}
|
||||
<div className="flex flex-row items-center justify-between w-full gap-4 max-sm:overflow-x-auto">
|
||||
<TabsList className="md:grid md:w-fit md:grid-cols-5 justify-start bg-transparent">
|
||||
<TabsTrigger
|
||||
value="projects"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Projects
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="monitoring"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Monitoring
|
||||
</TabsTrigger>
|
||||
|
||||
{(data?.rol === "admin" || user?.canAccessToTraefikFiles) && (
|
||||
<TabsTrigger
|
||||
value="traefik"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Traefik File System
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{(data?.rol === "admin" || user?.canAccessToDocker) && (
|
||||
<TabsTrigger
|
||||
value="docker"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Docker
|
||||
</TabsTrigger>
|
||||
)}
|
||||
|
||||
<TabsTrigger
|
||||
value="settings"
|
||||
className="rounded-none border-b-2 border-b-transparent data-[state=active]:border-b-2 data-[state=active]:border-b-border"
|
||||
>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
<TabsContent value={activeTab} className="w-full">
|
||||
{children}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
8
components/layouts/onboarding-layout.tsx
Normal file
8
components/layouts/onboarding-layout.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import type React from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
export const OnboardingLayout = ({ children }: Props) => {
|
||||
return <>{children}</>;
|
||||
};
|
||||
25
components/layouts/project-layout.tsx
Normal file
25
components/layouts/project-layout.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Navbar } from "./navbar";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ProjectLayout = ({ children }: Props) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className="bg-radial relative flex flex-col bg-background pt-6"
|
||||
id="app-container"
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="w-full">
|
||||
<Navbar />
|
||||
<main className="mt-6 flex w-full flex-col items-center">
|
||||
<div className="w-full max-w-8xl px-4 lg:px-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
0
components/layouts/service-layout.tsx
Normal file
0
components/layouts/service-layout.tsx
Normal file
241
components/layouts/settings-layout.tsx
Normal file
241
components/layouts/settings-layout.tsx
Normal file
@@ -0,0 +1,241 @@
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const SettingsLayout = ({ children }: Props) => {
|
||||
const { data } = api.auth.get.useQuery();
|
||||
const { data: user } = api.user.byAuthId.useQuery(
|
||||
{
|
||||
authId: data?.id || "",
|
||||
},
|
||||
{
|
||||
enabled: !!data?.id && data?.rol === "user",
|
||||
},
|
||||
);
|
||||
return (
|
||||
<div className="flex flex-row gap-4 my-8 w-full flex-wrap md:flex-nowrap">
|
||||
<div className="md:max-w-[18rem] w-full">
|
||||
<Nav
|
||||
links={[
|
||||
...(data?.rol === "admin"
|
||||
? [
|
||||
{
|
||||
title: "Server",
|
||||
icon: Activity,
|
||||
href: "/dashboard/settings/server",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
title: "Profile",
|
||||
icon: User2,
|
||||
href: "/dashboard/settings/profile",
|
||||
},
|
||||
{
|
||||
title: "Appareance",
|
||||
label: "",
|
||||
icon: Route,
|
||||
href: "/dashboard/settings/appearance",
|
||||
},
|
||||
|
||||
...(data?.rol === "admin"
|
||||
? [
|
||||
{
|
||||
title: "S3 Destinations",
|
||||
label: "",
|
||||
icon: Database,
|
||||
href: "/dashboard/settings/destinations",
|
||||
},
|
||||
{
|
||||
title: "Certificates",
|
||||
label: "",
|
||||
icon: ShieldCheck,
|
||||
href: "/dashboard/settings/certificates",
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
label: "",
|
||||
icon: Users,
|
||||
href: "/dashboard/settings/users",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Activity,
|
||||
Database,
|
||||
Route,
|
||||
ShieldCheck,
|
||||
User2,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { useRouter } from "next/router";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
interface NavProps {
|
||||
links: {
|
||||
title: string;
|
||||
label?: string;
|
||||
icon: LucideIcon;
|
||||
href: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const Nav = ({ links }: NavProps) => {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2 ">
|
||||
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
|
||||
{links.map((link, index) => {
|
||||
const isActive = router.pathname === link.href;
|
||||
// biome-ignore lint/correctness/useJsxKeyInIterable: <explanation>
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "sm" }),
|
||||
isActive &&
|
||||
"dark:bg-muted dark:text-white dark:hover:bg-muted dark:hover:text-white",
|
||||
"justify-start",
|
||||
)}
|
||||
>
|
||||
<link.icon className="mr-2 h-4 w-4" />
|
||||
{link.title}
|
||||
{link.label && (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
isActive && "text-background dark:text-white",
|
||||
)}
|
||||
>
|
||||
{link.label}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{/* {!isCollapsed ? (
|
||||
<Accordion collapsible type="single" className="">
|
||||
<AccordionItem value="follow-up" className="">
|
||||
<AccordionTrigger
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"hover:no-underline py-0 text-start justify-start flex items-center gap-2 px-3 mb-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2 justify-between w-full">
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<Settings className="h-4 w-4" />
|
||||
<span className=" dark:hover:text-white">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="ml-9">
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"hover:no-underline w-full text-start justify-start px-2 gap-2",
|
||||
)}
|
||||
href="/dashboard/settings"
|
||||
>
|
||||
<User2 className="h-4 w-4" />
|
||||
Account
|
||||
</Link>
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"hover:no-underline w-full text-start justify-start px-2 gap-2",
|
||||
)}
|
||||
href="/dashboard/server"
|
||||
>
|
||||
<Computer className="h-4 w-4" />
|
||||
Server
|
||||
</Link>
|
||||
<Link
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"hover:no-underline w-full text-start justify-start px-2 gap-2",
|
||||
)}
|
||||
href="/dashboard/users"
|
||||
>
|
||||
<Users className="h-4 w-4" />
|
||||
Users
|
||||
</Link>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
) : (
|
||||
<>
|
||||
{[
|
||||
{
|
||||
title: "Account",
|
||||
icon: User2,
|
||||
label: "",
|
||||
href: "/dashboard/server",
|
||||
},
|
||||
{
|
||||
title: "Server",
|
||||
icon: Computer,
|
||||
label: "",
|
||||
href: "/dashboard/users",
|
||||
},
|
||||
{
|
||||
title: "Users",
|
||||
icon: Users,
|
||||
label: "",
|
||||
href: "/dashboard/traefik",
|
||||
},
|
||||
].map((link, index) => {
|
||||
const isActive = router.pathname === link.href;
|
||||
return (
|
||||
<Tooltip key={index} delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={link.href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon" }),
|
||||
"h-9 w-9",
|
||||
isActive &&
|
||||
"dark:bg-muted dark:text-muted-foreground dark:hover:bg-muted dark:hover:text-white",
|
||||
)}
|
||||
>
|
||||
<link.icon className="h-4 w-4" />
|
||||
<span className="sr-only">{link.title}</span>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
className="flex items-center gap-4"
|
||||
>
|
||||
{link.title}
|
||||
{link.label && (
|
||||
<span className="ml-auto text-muted-foreground">
|
||||
{link.label}
|
||||
</span>
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)} */}
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user