diff --git a/bun.lockb b/bun.lockb deleted file mode 100755 index 8ec2709..0000000 Binary files a/bun.lockb and /dev/null differ diff --git a/package.json b/package.json index 1b6328d..d38f592 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@prisma/client": "^5.21.1", "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-hover-card": "^1.1.2", @@ -29,22 +30,23 @@ "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", - "@radix-ui/react-tooltip": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.4", "@tanstack/react-table": "^8.20.5", "@types/bcrypt": "^5.0.2", "@types/qrcode": "^1.5.5", "@types/ws": "^8.5.13", "@xterm/xterm": "^5.5.0", "bcrypt": "^5.1.1", - "class-variance-authority": "^0.7.0", + "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "1.0.0", "cross-env": "^7.0.3", "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", - "lucide-react": "^0.453.0", + "lucide-react": "^0.462.0", "moment": "^2.30.1", "next": "14.2.15", "next-auth": "^4.24.8", diff --git a/src/app/globals.css b/src/app/globals.css index 8f6bfe9..a935df1 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -29,6 +29,14 @@ --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; + --sidebar-background: 0 0% 98%; + --sidebar-foreground: 240 5.3% 26.1%; + --sidebar-primary: 240 5.9% 10%; + --sidebar-primary-foreground: 0 0% 98%; + --sidebar-accent: 240 4.8% 95.9%; + --sidebar-accent-foreground: 240 5.9% 10%; + --sidebar-border: 220 13% 91%; + --sidebar-ring: 217.2 91.2% 59.8%; } .dark { @@ -56,6 +64,14 @@ --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; + --sidebar-background: 240 5.9% 10%; + --sidebar-foreground: 240 4.8% 95.9%; + --sidebar-primary: 224.3 76.3% 48%; + --sidebar-primary-foreground: 0 0% 100%; + --sidebar-accent: 240 3.7% 15.9%; + --sidebar-accent-foreground: 240 4.8% 95.9%; + --sidebar-border: 240 3.7% 15.9%; + --sidebar-ring: 217.2 91.2% 59.8%; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 9c92c0e..391e531 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,6 +7,11 @@ import { NavBar } from "./nav-bar"; import { Suspense } from "react"; import FullLoadingSpinner from "@/components/ui/full-loading-spinnter"; import { ConfirmDialog } from "@/components/custom/confirm-dialog"; +import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar" +import { AppSidebar } from "./sidebar"; +import { cookies } from "next/headers"; +import { BreadcrumbsGenerator } from "../components/custom/breadcrumbs-generator"; +import { getUserSession } from "@/server/utils/action-wrapper.utils"; const inter = Inter({ subsets: ["latin"], @@ -18,19 +23,45 @@ export const metadata: Metadata = { description: "", // todo }; -export default function RootLayout({ +export default async function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const cookieStore = await cookies() + const defaultOpen = cookieStore.get("sidebar:state")?.value === "true"; + const session = await getUserSession(); + const userIsLoggedIn = !!session; + return ( - -
+ + +
+
+
+ {userIsLoggedIn && } + }> + {children} + +
+
+
+
+ + + + + + ); +} + +/* +
}> @@ -39,10 +70,4 @@ export default function RootLayout({
- - - - - ); -} - +*/ diff --git a/src/app/project/app/[appId]/app-breadcrumbs.tsx b/src/app/project/app/[appId]/app-breadcrumbs.tsx new file mode 100644 index 0000000..aed8465 --- /dev/null +++ b/src/app/project/app/[appId]/app-breadcrumbs.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; +import { AppExtendedModel } from "@/shared/model/app-extended.model"; + +export default function AppBreadcrumbs({ app }: { app: AppExtendedModel }) { + const { setBreadcrumbs } = useBreadcrumbs(); + useEffect(() => setBreadcrumbs([ + { name: "Projects", url: "/" }, + { name: app.project.name, url: "/project?projectId=" + app.projectId }, + { name: app.name }, + ]), []); + return <>; +} \ No newline at end of file diff --git a/src/app/project/app/[appId]/app-tabs.tsx b/src/app/project/app/[appId]/app-tabs.tsx index 181e6b9..b510ff4 100644 --- a/src/app/project/app/[appId]/app-tabs.tsx +++ b/src/app/project/app/[appId]/app-tabs.tsx @@ -15,6 +15,8 @@ import Logs from "./overview/logs"; import MonitoringTab from "./overview/monitoring-app"; import InternalHostnames from "./domains/internal-hostnames"; import TerminalStreamed from "./overview/terminal-streamed"; +import { useEffect } from "react"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; export default function AppTabs({ app, diff --git a/src/app/project/app/[appId]/layout.tsx b/src/app/project/app/[appId]/layout.tsx index bb1ddbe..7698e4e 100644 --- a/src/app/project/app/[appId]/layout.tsx +++ b/src/app/project/app/[appId]/layout.tsx @@ -27,22 +27,7 @@ export default async function RootLayout({ const app = await appService.getExtendedById(appId); return ( -
- - - - Projects - - - - {app.project.name} - - - - {app.name} - - - +
diff --git a/src/app/project/app/[appId]/page.tsx b/src/app/project/app/[appId]/page.tsx index 28ba856..fec3a9d 100644 --- a/src/app/project/app/[appId]/page.tsx +++ b/src/app/project/app/[appId]/page.tsx @@ -1,16 +1,7 @@ import { getAuthUserSession } from "@/server/utils/action-wrapper.utils"; import appService from "@/server/services/app.service"; -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbSeparator, -} from "@/components/ui/breadcrumb" -import PageTitle from "@/components/custom/page-title"; import AppTabs from "./app-tabs"; -import AppActionButtons from "./app-action-buttons"; -import buildService from "@/server/services/build.service"; +import AppBreadcrumbs from "./app-breadcrumbs"; export default async function AppPage({ searchParams, @@ -26,7 +17,9 @@ export default async function AppPage({ } const app = await appService.getExtendedById(appId); - return ( + return (<> + + ) } diff --git a/src/app/project/apps-table.tsx b/src/app/project/apps-table.tsx index 58850d4..de64b7d 100644 --- a/src/app/project/apps-table.tsx +++ b/src/app/project/apps-table.tsx @@ -8,13 +8,14 @@ import { formatDateTime } from "@/frontend/utils/format.utils"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { MoreHorizontal } from "lucide-react"; import { Toast } from "@/frontend/utils/toast.utils"; -import { App } from "@prisma/client"; +import { App, Project } from "@prisma/client"; import { deleteApp } from "./actions"; -import { useConfirmDialog } from "@/frontend/states/zustand.states"; +import { useBreadcrumbs, useConfirmDialog } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; -export default function AppTable({ data }: { data: App[] }) { +export default function AppTable({ app }: { app: App[] }) { const { openDialog } = useConfirmDialog(); @@ -31,7 +32,7 @@ export default function AppTable({ data }: { data: App[] }) { ["createdAt", "Created At", true, (item) => formatDateTime(item.createdAt)], ["updatedAt", "Updated At", false, (item) => formatDateTime(item.updatedAt)], ]} - data={data} + data={app} onItemClickLink={(item) => `/project/app/${item.id}`} actionCol={(item) => <> diff --git a/src/app/project/page.tsx b/src/app/project/page.tsx index 672e0e6..6a3dde3 100644 --- a/src/app/project/page.tsx +++ b/src/app/project/page.tsx @@ -17,6 +17,7 @@ import { BreadcrumbSeparator, } from "@/components/ui/breadcrumb" import PageTitle from "@/components/custom/page-title"; +import ProjectBreadcrumbs from "./project-breadcrumbs"; export default async function AppsPage({ @@ -33,24 +34,14 @@ export default async function AppsPage({ const project = await projectService.getById(projectId); const data = await appService.getAllAppsByProjectID(projectId); return ( -
- - - - Projects - - - - {project.name} - - - +
- + +
) } diff --git a/src/app/project/project-breadcrumbs.tsx b/src/app/project/project-breadcrumbs.tsx new file mode 100644 index 0000000..a5f21b7 --- /dev/null +++ b/src/app/project/project-breadcrumbs.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; + +export default function ProjectBreadcrumbs({ project }: { project: { name: string } }) { + const { setBreadcrumbs } = useBreadcrumbs(); + useEffect(() => setBreadcrumbs([ + { name: "Projects", url: "/" }, + { name: project.name } + ]), []); + return <>; +} \ No newline at end of file diff --git a/src/app/projects/create-project-dialog.tsx b/src/app/projects/create-project-dialog.tsx index ef05ab0..a589163 100644 --- a/src/app/projects/create-project-dialog.tsx +++ b/src/app/projects/create-project-dialog.tsx @@ -6,7 +6,7 @@ import { Toast } from "@/frontend/utils/toast.utils"; import { createProject } from "./actions"; -export function CreateProjectDialog() { +export function CreateProjectDialog({ children }: { children?: React.ReactNode }) { const createProj = async (name: string | undefined) => { if (!name) { @@ -21,6 +21,6 @@ export function CreateProjectDialog() { description="Name your new project." fieldName="Name" onResult={createProj}> - + {children} } \ No newline at end of file diff --git a/src/app/projects/project-page.tsx b/src/app/projects/project-page.tsx index 9b0d68a..b905101 100644 --- a/src/app/projects/project-page.tsx +++ b/src/app/projects/project-page.tsx @@ -15,25 +15,24 @@ import { BreadcrumbPage, BreadcrumbSeparator, } from "@/components/ui/breadcrumb" +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; +import ProjectsBreadcrumbs from "./projects-breadcrumbs"; export default async function ProjectPage() { await getAuthUserSession(); const data = await projectService.getAllProjects(); + return ( -
- - - - Projects - - - +

Projects

- + + +
+
) } diff --git a/src/app/projects/projects-breadcrumbs.tsx b/src/app/projects/projects-breadcrumbs.tsx new file mode 100644 index 0000000..14bda4b --- /dev/null +++ b/src/app/projects/projects-breadcrumbs.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; + +export default function ProjectsBreadcrumbs() { + const { setBreadcrumbs } = useBreadcrumbs(); + useEffect(() => setBreadcrumbs([ + { name: "Projects", url: "/" } + ]), []); + return <>; +} \ No newline at end of file diff --git a/src/app/projects/projects-table.tsx b/src/app/projects/projects-table.tsx index 438c358..aba0259 100644 --- a/src/app/projects/projects-table.tsx +++ b/src/app/projects/projects-table.tsx @@ -10,7 +10,8 @@ import { MoreHorizontal } from "lucide-react"; import { Toast } from "@/frontend/utils/toast.utils"; import { Project } from "@prisma/client"; import { deleteProject } from "./actions"; -import { useConfirmDialog } from "@/frontend/states/zustand.states"; +import { useBreadcrumbs, useConfirmDialog } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; diff --git a/src/app/settings/cluster/nodeInfo.tsx b/src/app/settings/cluster/nodeInfo.tsx index f5ef9e6..c27140e 100644 --- a/src/app/settings/cluster/nodeInfo.tsx +++ b/src/app/settings/cluster/nodeInfo.tsx @@ -6,12 +6,20 @@ import { Code } from "@/components/custom/code"; import { Toast } from "@/frontend/utils/toast.utils"; import { setNodeStatus } from "./actions"; import { Button } from "@/components/ui/button"; -import { useConfirmDialog } from "@/frontend/states/zustand.states"; +import { useBreadcrumbs, useConfirmDialog } from "@/frontend/states/zustand.states"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { useEffect } from "react"; export default async function NodeInfo({ nodeInfos }: { nodeInfos: NodeInfoModel[] }) { const { openDialog } = useConfirmDialog(); + + const { setBreadcrumbs } = useBreadcrumbs(); + useEffect(() => setBreadcrumbs([ + { name: "Settings", url: "/settings/profile" }, + { name: "Cluster" }, + ]), []); + const setNodeStatusClick = async (nodeName: string, schedulable: boolean) => { const confirmation = await openDialog({ title: 'Update Node Status', diff --git a/src/app/settings/cluster/page.tsx b/src/app/settings/cluster/page.tsx index b60d85f..2e36fac 100644 --- a/src/app/settings/cluster/page.tsx +++ b/src/app/settings/cluster/page.tsx @@ -14,7 +14,7 @@ export default async function ClusterInfoPage() { const nodeInfo = await clusterService.getNodeInfo(); const clusterJoinToken = await paramService.getString(ParamService.K3S_JOIN_TOKEN); return ( -
+
diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx deleted file mode 100644 index cba2712..0000000 --- a/src/app/settings/layout.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Suspense } from "react"; -import FullLoadingSpinner from "@/components/ui/full-loading-spinnter"; -import { Button } from "@/components/ui/button"; -import SettingsNav from "./settings-nav"; - - -export default function SettingsLayout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { - return ( -
-
- -
-
- }> - {children} - -
-
- ); -} - diff --git a/src/app/settings/profile/page.tsx b/src/app/settings/profile/page.tsx index e00e130..976368f 100644 --- a/src/app/settings/profile/page.tsx +++ b/src/app/settings/profile/page.tsx @@ -11,17 +11,19 @@ import PageTitle from "@/components/custom/page-title"; import ProfilePasswordChange from "./profile-password-change"; import ToTpSettings from "./totp-settings"; import userService from "@/server/services/user.service"; +import BreadcrumbsSettings from "./profile-breadcrumbs"; export default async function ProjectPage() { const session = await getAuthUserSession(); const data = await userService.getUserByEmail(session.email); return ( -
+
+
diff --git a/src/app/settings/profile/profile-breadcrumbs.tsx b/src/app/settings/profile/profile-breadcrumbs.tsx new file mode 100644 index 0000000..7f04fb2 --- /dev/null +++ b/src/app/settings/profile/profile-breadcrumbs.tsx @@ -0,0 +1,18 @@ +'use client'; + +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { deactivate2fa } from "./actions"; +import { Toast } from "@/frontend/utils/toast.utils"; +import TotpCreateDialog from "./totp-create-dialog"; +import { Button } from "@/components/ui/button"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; + +export default function BreadcrumbsSettings() { + const { setBreadcrumbs } = useBreadcrumbs(); + useEffect(() => setBreadcrumbs([ + { name: "Settings", url: "/settings/profile" }, + { name: "Profile" }, + ]), []); + return <>; +} \ No newline at end of file diff --git a/src/app/settings/server/page.tsx b/src/app/settings/server/page.tsx index 3ee71c4..0608da1 100644 --- a/src/app/settings/server/page.tsx +++ b/src/app/settings/server/page.tsx @@ -15,6 +15,7 @@ import QuickStackLetsEncryptSettings from "./qs-letsencrypt-settings"; import QuickStackMaintenanceSettings from "./qs-maintenance-settings"; import podService from "@/server/services/pod.service"; import { Constants } from "@/shared/utils/constants"; +import ServerBreadcrumbs from "./server-breadcrumbs"; export default async function ProjectPage() { @@ -26,11 +27,12 @@ export default async function ProjectPage() { console.log(qsPodInfos) const qsPodInfo = qsPodInfos.find(p => !!p); return ( -
+
+
diff --git a/src/app/settings/server/server-breadcrumbs.tsx b/src/app/settings/server/server-breadcrumbs.tsx new file mode 100644 index 0000000..ee6cadd --- /dev/null +++ b/src/app/settings/server/server-breadcrumbs.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states"; +import { useEffect } from "react"; + +export default function ServerBreadcrumbs() { + const { setBreadcrumbs } = useBreadcrumbs(); + useEffect(() => setBreadcrumbs([ + { name: "Settings", url: "/settings/profile" }, + { name: "QuickStack Server" }, + ]), []); + return <>; +} \ No newline at end of file diff --git a/src/app/settings/settings-nav.tsx b/src/app/settings/settings-nav.tsx deleted file mode 100644 index 0aeded5..0000000 --- a/src/app/settings/settings-nav.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client' -import { Suspense } from "react"; -import FullLoadingSpinner from "@/components/ui/full-loading-spinnter"; -import { Button } from "@/components/ui/button"; -import { Info, Server, Settings, Settings2, User } from "lucide-react"; -import { usePathname } from "next/navigation"; -import Link from "next/link"; - - -export default function SettingsNav() { - - const pathname = usePathname(); - - const selectedCss = ` - inline-flex gap-2 items-center w-full - whitespace-nowrap rounded-md text-sm font-medium - ring-offset-background transition-colors focus-visible:outline-none - focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 - disabled:pointer-events-none disabled:opacity-50 bg-secondary text-secondary-foreground - hover:bg-secondary/80 h-10 px-4 py-2 w-full text-left - [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0`; - - const notSelectedCss = ` - inline-flex items-center gap-2 whitespace-nowrap rounded-md w-full - text-sm font-medium ring-offset-background transition-colors - focus-visible:outline-none focus-visible:ring-2 - focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none - disabled:opacity-50 hover:text-accent-foreground hover:bg-secondary/80 h-10 px-4 py-2 - [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0` - - return ( -
-

Settings

-
- -
-
- -
-
- -
-
- ); -} - diff --git a/src/app/sidebar-logout-button.tsx b/src/app/sidebar-logout-button.tsx new file mode 100644 index 0000000..eba5954 --- /dev/null +++ b/src/app/sidebar-logout-button.tsx @@ -0,0 +1,51 @@ +'use client' + +import { signOut } from "next-auth/react"; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarHeader, + SidebarFooter, + SidebarMenuSub, + SidebarMenuSubItem +} from "@/components/ui/sidebar" +import { AppleIcon, Calendar, ChartNoAxesCombined, ChevronDown, ChevronUp, FolderClosed, Home, Inbox, LogOut, Plus, Search, Server, Settings, Settings2, User, User2 } from "lucide-react" +import Link from "next/link" +import { CreateProjectDialog } from "./projects/create-project-dialog" +import projectService from "@/server/services/project.service" +import { getAuthUserSession } from "@/server/utils/action-wrapper.utils" +import { useConfirmDialog } from "@/frontend/states/zustand.states"; + +export function SidebarLogoutButton() { + + const { openDialog } = useConfirmDialog(); + + const signOutAsync = async () => { + if (!await openDialog({ + title: "Sign out", + description: "Are you sure you want to sign out?", + yesButton: "Sign out", + })) { + return; + } + await signOut({ + callbackUrl: undefined, + redirect: false + }); + window.open("/auth", "_self"); + } + return ( + signOutAsync()}> + + Sign out + + ) +} diff --git a/src/app/sidebar.tsx b/src/app/sidebar.tsx new file mode 100644 index 0000000..63d2d4f --- /dev/null +++ b/src/app/sidebar.tsx @@ -0,0 +1,195 @@ +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarHeader, + SidebarFooter, + SidebarMenuSub, + SidebarMenuSubItem +} from "@/components/ui/sidebar" +import { AppleIcon, Calendar, ChartNoAxesCombined, ChevronDown, ChevronUp, FolderClosed, Home, Inbox, Plus, Search, Server, Settings, Settings2, User, User2 } from "lucide-react" +import Link from "next/link" +import { CreateProjectDialog } from "./projects/create-project-dialog" +import projectService from "@/server/services/project.service" +import { getAuthUserSession, getUserSession } from "@/server/utils/action-wrapper.utils" +import { SidebarLogoutButton } from "./sidebar-logout-button" + +const monitoringMenu = [ + { + title: "Overall Cluster", + url: "/Montioring", + icon: Home, + }, +] + +const settingsMenu = [ + { + title: "Profile", + url: "/settings/profile", + icon: User, + }, + { + title: "QuickStack Settings", + url: "/settings/server", + icon: Settings, + }, + { + title: "Cluster", + url: "/settings/cluster", + icon: Server, + }, +] + +export async function AppSidebar() { + + const session = await getUserSession(); + + if (!session) { + return <> + } + + const projects = await projectService.getAllProjects(); + + return ( + + + + + Menu + + + + + + + + + Projects + + + + + + + + + {projects.map((item) => ( + + + + {item.name} + + + + ))} + + + + + + + + + + + + + + + + + + Monitoring + + + + + + {monitoringMenu.map((item) => ( + + + + {item.title} + + + + ))} + + + + + + + + + + + + + + + + + + + Settings + + + + + + {settingsMenu.map((item) => ( + + + + + {item.title} + + + + ))} + + + + + + + + + + + + + + + + {session.email} + + + + + + + + Profile + + + + + + + + + + ) +} diff --git a/src/components/custom/breadcrumbs-generator.tsx b/src/components/custom/breadcrumbs-generator.tsx new file mode 100644 index 0000000..c0630d7 --- /dev/null +++ b/src/components/custom/breadcrumbs-generator.tsx @@ -0,0 +1,47 @@ +'use client' + +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarGroupContent, + SidebarGroupLabel, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, + SidebarHeader, + SidebarFooter, + SidebarMenuSub, + SidebarMenuSubItem, + SidebarTrigger +} from "@/components/ui/sidebar" +import { AppleIcon, Calendar, ChartNoAxesCombined, ChevronDown, ChevronUp, FolderClosed, Home, Inbox, Plus, Search, Server, Settings, Settings2, User, User2 } from "lucide-react" +import Link from "next/link" +import { CreateProjectDialog } from "../../app/projects/create-project-dialog" +import projectService from "@/server/services/project.service" +import { getAuthUserSession } from "@/server/utils/action-wrapper.utils" +import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from "@/components/ui/breadcrumb"; +import { useBreadcrumbs } from "@/frontend/states/zustand.states" + +export function BreadcrumbsGenerator() { + + const { breadcrumbs } = useBreadcrumbs(); + + return ( +
+ + {breadcrumbs && + + {breadcrumbs.map((x, index) => (<> + {index > 0 && } + + {x.name} + + ))} + + } +
+ ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index aa45a8c..6e325cb 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,9 +1,9 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" - import { cn } from "@/frontend/utils/utils" + const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", { diff --git a/src/components/ui/collapsible.tsx b/src/components/ui/collapsible.tsx new file mode 100644 index 0000000..9fa4894 --- /dev/null +++ b/src/components/ui/collapsible.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" + +const Collapsible = CollapsiblePrimitive.Root + +const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger + +const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent + +export { Collapsible, CollapsibleTrigger, CollapsibleContent } diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index 159608f..cdfb011 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -3,9 +3,9 @@ import * as React from "react" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import { Check, ChevronRight, Circle } from "lucide-react" - import { cn } from "@/frontend/utils/utils" + const DropdownMenu = DropdownMenuPrimitive.Root const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger @@ -27,14 +27,14 @@ const DropdownMenuSubTrigger = React.forwardRef< {children} - + )) DropdownMenuSubTrigger.displayName = @@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef< {} - -const Input = React.forwardRef( +const Input = React.forwardRef>( ({ className, type, ...props }, ref) => { return ( , + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 0000000..e297403 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/frontend/utils/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx new file mode 100644 index 0000000..730b067 --- /dev/null +++ b/src/components/ui/sidebar.tsx @@ -0,0 +1,763 @@ +"use client" + +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { VariantProps, cva } from "class-variance-authority" +import { PanelLeft } from "lucide-react" + +import { cn } from "@/frontend/utils/utils" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator" +import { Sheet, SheetContent } from "@/components/ui/sheet" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { useIsMobile } from "@/frontend/hooks/use-mobile" + +const SIDEBAR_COOKIE_NAME = "sidebar:state" +const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7 +const SIDEBAR_WIDTH = "16rem" +const SIDEBAR_WIDTH_MOBILE = "18rem" +const SIDEBAR_WIDTH_ICON = "3rem" +const SIDEBAR_KEYBOARD_SHORTCUT = "b" + +type SidebarContext = { + state: "expanded" | "collapsed" + open: boolean + setOpen: (open: boolean) => void + openMobile: boolean + setOpenMobile: (open: boolean) => void + isMobile: boolean + toggleSidebar: () => void +} + +const SidebarContext = React.createContext(null) + +function useSidebar() { + const context = React.useContext(SidebarContext) + if (!context) { + throw new Error("useSidebar must be used within a SidebarProvider.") + } + + return context +} + +const SidebarProvider = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + defaultOpen?: boolean + open?: boolean + onOpenChange?: (open: boolean) => void + } +>( + ( + { + defaultOpen = true, + open: openProp, + onOpenChange: setOpenProp, + className, + style, + children, + ...props + }, + ref + ) => { + const isMobile = useIsMobile() + const [openMobile, setOpenMobile] = React.useState(false) + + // This is the internal state of the sidebar. + // We use openProp and setOpenProp for control from outside the component. + const [_open, _setOpen] = React.useState(defaultOpen) + const open = openProp ?? _open + const setOpen = React.useCallback( + (value: boolean | ((value: boolean) => boolean)) => { + const openState = typeof value === "function" ? value(open) : value + if (setOpenProp) { + setOpenProp(openState) + } else { + _setOpen(openState) + } + + // This sets the cookie to keep the sidebar state. + document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}` + }, + [setOpenProp, open] + ) + + // Helper to toggle the sidebar. + const toggleSidebar = React.useCallback(() => { + return isMobile + ? setOpenMobile((open) => !open) + : setOpen((open) => !open) + }, [isMobile, setOpen, setOpenMobile]) + + // Adds a keyboard shortcut to toggle the sidebar. + React.useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if ( + event.key === SIDEBAR_KEYBOARD_SHORTCUT && + (event.metaKey || event.ctrlKey) + ) { + event.preventDefault() + toggleSidebar() + } + } + + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [toggleSidebar]) + + // We add a state so that we can do data-state="expanded" or "collapsed". + // This makes it easier to style the sidebar with Tailwind classes. + const state = open ? "expanded" : "collapsed" + + const contextValue = React.useMemo( + () => ({ + state, + open, + setOpen, + isMobile, + openMobile, + setOpenMobile, + toggleSidebar, + }), + [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar] + ) + + return ( + + +
+ {children} +
+
+
+ ) + } +) +SidebarProvider.displayName = "SidebarProvider" + +const Sidebar = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + side?: "left" | "right" + variant?: "sidebar" | "floating" | "inset" + collapsible?: "offcanvas" | "icon" | "none" + } +>( + ( + { + side = "left", + variant = "sidebar", + collapsible = "offcanvas", + className, + children, + ...props + }, + ref + ) => { + const { isMobile, state, openMobile, setOpenMobile } = useSidebar() + + if (collapsible === "none") { + return ( +
+ {children} +
+ ) + } + + if (isMobile) { + return ( + + +
{children}
+
+
+ ) + } + + return ( +
+ {/* This is what handles the sidebar gap on desktop */} +
+ +
+ ) + } +) +Sidebar.displayName = "Sidebar" + +const SidebarTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, onClick, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( + + ) +}) +SidebarTrigger.displayName = "SidebarTrigger" + +const SidebarRail = React.forwardRef< + HTMLButtonElement, + React.ComponentProps<"button"> +>(({ className, ...props }, ref) => { + const { toggleSidebar } = useSidebar() + + return ( +