diff --git a/ee/tabby-ui/app/(dashboard)/components/dashboard-layout.tsx b/ee/tabby-ui/app/(dashboard)/components/dashboard-layout.tsx new file mode 100644 index 000000000..e97e42da7 --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/components/dashboard-layout.tsx @@ -0,0 +1,33 @@ +'use client' + +import { useHydrated } from '@/lib/hooks/use-hydration' +import { + toggleSidebar, + useUserPreferencesStore +} from '@/lib/stores/user-preferences-store' +import { SidebarProvider } from '@/components/ui/sidebar' + +import MainContent from './dashboard-main' +import AppSidebar from './dashboard-sidebar' + +export default function Layout({ children }: { children: React.ReactNode }) { + const hydrated = useHydrated() + const isSidebarExpanded = useUserPreferencesStore( + state => state.isSidebarExpanded + ) + + if (!hydrated) return null + + return ( + <> + toggleSidebar(open)} + > + + {children} + + + ) +} diff --git a/ee/tabby-ui/app/(dashboard)/components/main-content.tsx b/ee/tabby-ui/app/(dashboard)/components/dashboard-main.tsx similarity index 100% rename from ee/tabby-ui/app/(dashboard)/components/main-content.tsx rename to ee/tabby-ui/app/(dashboard)/components/dashboard-main.tsx diff --git a/ee/tabby-ui/app/(dashboard)/components/dashboard-sidebar.tsx b/ee/tabby-ui/app/(dashboard)/components/dashboard-sidebar.tsx new file mode 100644 index 000000000..481978f9b --- /dev/null +++ b/ee/tabby-ui/app/(dashboard)/components/dashboard-sidebar.tsx @@ -0,0 +1,297 @@ +'use client' + +import React, { FunctionComponent } from 'react' +import Image from 'next/image' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import logoDarkUrl from '@/assets/logo-dark.png' +import logoUrl from '@/assets/logo.png' +import tabbyLogo from '@/assets/tabby.png' +import { HoverCardPortal } from '@radix-ui/react-hover-card' + +import { useMe } from '@/lib/hooks/use-me' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible' +import { + HoverCard, + HoverCardContent, + HoverCardTrigger +} from '@/components/ui/hover-card' +import { + IconBookOpenText, + IconChevronRight, + IconGear, + IconLightingBolt, + IconUser +} from '@/components/ui/icons' +import { + Sidebar, + SidebarContent, + SidebarGroup, + SidebarHeader, + SidebarMenuButton, + SidebarMenuItem, + SidebarMenuSub, + SidebarMenuSubButton, + SidebarMenuSubItem, + useSidebar +} from '@/components/ui/sidebar' +import LoadingWrapper from '@/components/loading-wrapper' + +export interface SidebarProps { + children?: React.ReactNode + className?: string +} + +type SubMenu = { + title: string + href: string + allowUser?: boolean +} + +type Menu = + | { + title: string + icon: FunctionComponent + allowUser?: boolean + items: SubMenu[] + } + | { + title: string + href: string + icon: FunctionComponent + allowUser?: boolean + items?: never + } + +const menus: Menu[] = [ + { + title: 'Profile', + icon: IconUser, + href: '/profile', + allowUser: true + }, + { + title: 'Information', + icon: IconBookOpenText, + items: [ + { + title: 'System', + href: '/system' + }, + { + title: 'Jobs', + href: '/jobs' + }, + { + title: 'Reports', + href: '/reports' + }, + { + title: 'Activities', + href: '/activities' + } + ] + }, + { + title: 'Settings', + icon: IconGear, + allowUser: true, + items: [ + { + title: 'General', + href: '/settings/general' + }, + { + title: 'Users & Groups', + href: '/settings/team', + allowUser: true + }, + { + title: 'Subscription', + href: '/settings/subscription' + } + ] + }, + { + title: 'Integrations', + icon: IconLightingBolt, + items: [ + { + title: 'Context Providers', + href: '/settings/providers/git' + }, + { + title: 'SSO', + href: '/settings/sso' + }, + { + title: 'Mail Delivery', + href: '/settings/mail' + } + ] + } +] + +export default function AppSidebar() { + const pathname = usePathname() + const [{ data, fetching: fetchingMe }] = useMe() + const isAdmin = data?.me.isAdmin + const { isMobile, state } = useSidebar() + + return ( + + + + <> + logo +
+ logo + logo +
+ + +
+ + + + {menus.map(menu => { + if (isAdmin || menu.allowUser) { + if (menu.items) { + return ( + + + + + + + {!!menu.icon && } + {menu.title} + + + + + + + + + + + {menu.items.map(item => { + if (isAdmin || item.allowUser) { + return ( + + + + {item.title} + + + + ) + } + })} + + + + + ) + } else { + return ( + + + {menu.title} + + ) + }} + > + + {!!menu.icon && } + {menu.title} + + + + ) + } + } + return null + })} + + + +
+ ) +} diff --git a/ee/tabby-ui/app/(dashboard)/layout.tsx b/ee/tabby-ui/app/(dashboard)/layout.tsx index 8d583190b..382c10b55 100644 --- a/ee/tabby-ui/app/(dashboard)/layout.tsx +++ b/ee/tabby-ui/app/(dashboard)/layout.tsx @@ -1,19 +1,17 @@ import { Metadata } from 'next' -import { SidebarProvider } from '@/components/ui/sidebar' import { LicenseBanner } from '@/components/license-banner' -import MainContent from './components/main-content' -import AppSidebar from './components/sidebar' +import Layout from './components/dashboard-layout' export const metadata: Metadata = { title: { - default: 'Home', + default: 'Dashboard', template: `Tabby - %s` } } -export default function RootLayout({ +export default function DashboardLayout({ children }: { children: React.ReactNode @@ -21,10 +19,7 @@ export default function RootLayout({ return ( <> - - - {children} - + {children} ) } diff --git a/ee/tabby-ui/lib/stores/user-preferences-store.ts b/ee/tabby-ui/lib/stores/user-preferences-store.ts new file mode 100644 index 000000000..d14db098e --- /dev/null +++ b/ee/tabby-ui/lib/stores/user-preferences-store.ts @@ -0,0 +1,37 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +export interface UserPreferences { + isSidebarExpanded: boolean + _hasHydrated: boolean + setHasHydrated: (v: boolean) => void +} + +const initialState: Omit = { + isSidebarExpanded: true, + _hasHydrated: false +} + +export const useUserPreferencesStore = create()( + persist( + set => ({ + ...initialState, + setHasHydrated: (v: boolean) => set(() => ({ _hasHydrated: v })) + }), + { + name: 'user-preferences-storage', + version: 0, + onRehydrateStorage: state => { + return () => { + state.setHasHydrated(true) + } + } + } + ) +) + +const set = useUserPreferencesStore.setState + +export const toggleSidebar = (expanded: boolean) => { + return set(() => ({ isSidebarExpanded: expanded })) +}