OrgMenuContent.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. 'use client'
  2. import { useIsMFAEnabled, useParams } from 'common'
  3. import { Blocks, Boxes, ChartArea, ChevronLeft, Receipt, Settings, Users } from 'lucide-react'
  4. import { useRouter } from 'next/router'
  5. import React, { useMemo } from 'react'
  6. import { Button, cn, SidebarGroup, SidebarMenu } from 'ui'
  7. import { GenericSkeletonLoader } from 'ui-patterns'
  8. import { getOrgMenuComponent } from './mobileOrgMenuRegistry'
  9. import type { OrgNavItem } from './OrgMenuContent.utils'
  10. import {
  11. getOrgActiveRoute,
  12. getOrgSectionKeyFromPathname,
  13. isOrgMenuActive,
  14. } from './OrgMenuContent.utils'
  15. import { OrgMenuItem } from './OrgMenuItem'
  16. import { orgItemHasSubmenu, useOrgMenuNavigation } from './useOrgMenuNavigation'
  17. import { ICON_SIZE, ICON_STROKE_WIDTH } from '@/components/interfaces/Sidebar'
  18. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  19. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  20. import { getPathnameWithoutQuery } from '@/lib/pathname.utils'
  21. import { useTrack } from '@/lib/telemetry/track'
  22. export interface OrgMenuContentProps {
  23. onCloseSheet?: () => void
  24. }
  25. export function OrgMenuContent({ onCloseSheet }: OrgMenuContentProps) {
  26. const router = useRouter()
  27. const { slug } = useParams()
  28. const organizationSlug: string = slug ?? (router.query.orgSlug as string) ?? ''
  29. const { data: org } = useSelectedOrganizationQuery()
  30. const isUserMFAEnabled = useIsMFAEnabled()
  31. const disableAccessMfa = org?.organization_requires_mfa && !isUserMFAEnabled
  32. const showBilling = useIsFeatureEnabled('billing:all')
  33. const pathname = getPathnameWithoutQuery(router.asPath, router.pathname)
  34. const activeRoute = getOrgActiveRoute(pathname)
  35. const initialSectionKey = getOrgSectionKeyFromPathname(activeRoute)
  36. const track = useTrack()
  37. const {
  38. viewLevel,
  39. selectedSectionKey,
  40. handleSubmenuClick: navigateToSubmenu,
  41. handleBackToTop: navigateBackToTop,
  42. } = useOrgMenuNavigation({ initialSectionKey })
  43. const handleSubmenuClick = (item: OrgNavItem) => {
  44. track('org_submenu_opened', { itemKey: item.key, itemLabel: item.label })
  45. navigateToSubmenu(item)
  46. }
  47. const handleBackToTop = () => {
  48. track('org_menu_back_clicked')
  49. navigateBackToTop()
  50. }
  51. const navMenuItems: OrgNavItem[] = useMemo(
  52. () => [
  53. {
  54. label: 'Projects',
  55. href: `/org/${organizationSlug}`,
  56. key: 'projects',
  57. icon: <Boxes size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  58. },
  59. {
  60. label: 'Team',
  61. href: `/org/${organizationSlug}/team`,
  62. key: 'team',
  63. icon: <Users size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  64. },
  65. {
  66. label: 'Integrations',
  67. href: `/org/${organizationSlug}/integrations`,
  68. key: 'integrations',
  69. icon: <Blocks size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  70. },
  71. {
  72. label: 'Usage',
  73. href: `/org/${organizationSlug}/usage`,
  74. key: 'usage',
  75. icon: <ChartArea size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  76. },
  77. ...(showBilling
  78. ? [
  79. {
  80. label: 'Billing',
  81. href: `/org/${organizationSlug}/billing`,
  82. key: 'billing',
  83. icon: <Receipt size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  84. },
  85. ]
  86. : []),
  87. {
  88. label: 'Organization settings',
  89. href: `/org/${organizationSlug}/general`,
  90. key: 'settings',
  91. icon: <Settings size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
  92. },
  93. ],
  94. [organizationSlug, showBilling]
  95. )
  96. const sectionKeyToShow = viewLevel === 'section' ? selectedSectionKey : null
  97. const sectionLabel =
  98. sectionKeyToShow && navMenuItems.find((item) => item.key === sectionKeyToShow)?.label
  99. const SectionMenuContent = sectionKeyToShow ? getOrgMenuComponent(sectionKeyToShow) : null
  100. if (!organizationSlug) return null
  101. if (viewLevel === 'section' && sectionKeyToShow && SectionMenuContent) {
  102. return (
  103. <div className="flex flex-col h-full">
  104. <div
  105. className={cn(
  106. 'shrink-0 flex items-center gap-2 border-b border-default px-3 min-h-(--header-height)'
  107. )}
  108. >
  109. <Button
  110. type="text"
  111. className="p-1! justify-start"
  112. icon={<ChevronLeft size={20} />}
  113. onClick={handleBackToTop}
  114. aria-label="Back to menu"
  115. block
  116. >
  117. <span className="font-medium truncate text-sm">{sectionLabel ?? sectionKeyToShow}</span>
  118. </Button>
  119. </div>
  120. <div className="flex-1 overflow-y-auto text-sidebar-foreground px-2">
  121. <React.Suspense fallback={<GenericSkeletonLoader className="p-4" />}>
  122. <SectionMenuContent onCloseSheet={onCloseSheet} />
  123. </React.Suspense>
  124. </div>
  125. </div>
  126. )
  127. }
  128. return (
  129. <div className="flex flex-col h-full">
  130. <div className="flex-1 overflow-y-auto text-sidebar-foreground">
  131. <nav className="flex flex-col gap-2 p-1" aria-label="Organization menu">
  132. <SidebarMenu>
  133. <SidebarGroup className="gap-0.5">
  134. {navMenuItems.map((item, i) => (
  135. <OrgMenuItem
  136. key={item.key}
  137. item={item}
  138. isActive={isOrgMenuActive(item, i, pathname, activeRoute)}
  139. disabled={disableAccessMfa}
  140. onCloseSheet={onCloseSheet}
  141. onSubmenuClick={orgItemHasSubmenu(item) ? handleSubmenuClick : undefined}
  142. onSelect={() =>
  143. track('org_menu_item_clicked', {
  144. itemKey: item.key,
  145. itemHref: item.href,
  146. })
  147. }
  148. />
  149. ))}
  150. </SidebarGroup>
  151. </SidebarMenu>
  152. </nav>
  153. </div>
  154. </div>
  155. )
  156. }