ProjectDropdown.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import { useParams } from 'common'
  2. import { Box, Plus } from 'lucide-react'
  3. import Link from 'next/link'
  4. import { useRouter } from 'next/router'
  5. import type { ComponentProps } from 'react'
  6. import { useState } from 'react'
  7. import { Button, CommandGroup, CommandItem } from 'ui'
  8. import { ShimmeringLoader } from 'ui-patterns'
  9. import { AppLayoutDropdownTriggerButton } from './AppLayoutDropdown'
  10. import { sanitizeRoute } from './ProjectDropdown.utils'
  11. import { ProjectRowLink } from './ProjectRowLink'
  12. import { useEmbeddedCloseHandler } from './useEmbeddedCloseHandler'
  13. import { OrganizationProjectSelector } from '@/components/ui/OrganizationProjectSelector'
  14. import PartnerIcon from '@/components/ui/PartnerIcon'
  15. import { getManagedByFromOrganizationPartner } from '@/data/organizations/managed-by-utils'
  16. import type { OrgProject } from '@/data/projects/org-projects-infinite-query'
  17. import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
  18. import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
  19. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  20. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  21. import { IS_PLATFORM } from '@/lib/constants'
  22. import type { ManagedBy } from '@/lib/constants/infrastructure'
  23. import { useTrack } from '@/lib/telemetry/track'
  24. // --- Sub-components ---
  25. interface ProjectDropdownNewProjectActionsProps {
  26. organizationSlug: string | undefined
  27. embedded: boolean
  28. onClose: () => void
  29. onNavigate: (href: string) => void
  30. }
  31. function ProjectDropdownNewProjectActions({
  32. organizationSlug,
  33. embedded,
  34. onClose,
  35. onNavigate,
  36. }: ProjectDropdownNewProjectActionsProps) {
  37. const href = `/new/${organizationSlug}`
  38. if (embedded) {
  39. return (
  40. <Button type="default" block size="small" asChild icon={<Plus size={14} strokeWidth={1.5} />}>
  41. <Link
  42. href={href}
  43. onClick={onClose}
  44. className="text-xs text-foreground-light hover:text-foreground"
  45. >
  46. New project
  47. </Link>
  48. </Button>
  49. )
  50. }
  51. return (
  52. <CommandGroup>
  53. <CommandItem
  54. className="cursor-pointer w-full"
  55. onSelect={() => {
  56. onClose()
  57. onNavigate(href)
  58. }}
  59. onClick={onClose}
  60. >
  61. <Link href={href} onClick={onClose} className="w-full flex items-center gap-2">
  62. <Plus size={14} strokeWidth={1.5} />
  63. <p>New project</p>
  64. </Link>
  65. </CommandItem>
  66. </CommandGroup>
  67. )
  68. }
  69. const ProjectDropdownNonPlatformView = ({ projectName }: { projectName: string }) => {
  70. return <div className="text-sm px-3 py-1">{projectName}</div>
  71. }
  72. interface ProjectDropdownPlatformViewProps {
  73. projectRef: string | undefined
  74. projectName: string
  75. projectManagedBy?: ManagedBy
  76. selectorProps: Omit<
  77. ComponentProps<typeof OrganizationProjectSelector>,
  78. 'renderTrigger' | 'embedded'
  79. >
  80. }
  81. function ProjectDropdownPlatformView({
  82. projectRef,
  83. projectName,
  84. projectManagedBy,
  85. selectorProps,
  86. }: ProjectDropdownPlatformViewProps) {
  87. return (
  88. <div className="flex items-center shrink-0">
  89. <Link href={`/project/${projectRef}`} className="flex items-center gap-2 shrink-0 text-sm">
  90. <Box size={14} strokeWidth={1.5} className="text-foreground-lighter" />
  91. <span title={projectName} className="text-foreground max-w-32 lg:max-w-64 truncate">
  92. {projectName}
  93. </span>
  94. {projectManagedBy && <PartnerIcon organization={{ managed_by: projectManagedBy }} />}
  95. </Link>
  96. <OrganizationProjectSelector
  97. {...selectorProps}
  98. renderTrigger={() => <AppLayoutDropdownTriggerButton className="shrink-0" />}
  99. />
  100. </div>
  101. )
  102. }
  103. // --- Main component ---
  104. interface ProjectDropdownProps {
  105. embedded?: boolean
  106. className?: string
  107. onClose?: () => void
  108. }
  109. export const ProjectDropdown = ({
  110. embedded = false,
  111. className,
  112. onClose,
  113. }: ProjectDropdownProps = {}) => {
  114. const router = useRouter()
  115. const { ref } = useParams()
  116. const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
  117. const { data: selectedOrganization } = useSelectedOrganizationQuery()
  118. const isBranch = project?.parentRef !== project?.ref
  119. const { data: parentProject, isPending: isLoadingParentProject } = useProjectDetailQuery(
  120. { ref: project?.parent_project_ref },
  121. { enabled: isBranch }
  122. )
  123. const selectedProject = parentProject ?? project
  124. const projectCreationEnabled = useIsFeatureEnabled('projects:create')
  125. const track = useTrack()
  126. const [open, setOpen] = useState(false)
  127. const close = useEmbeddedCloseHandler(embedded, onClose, setOpen)
  128. const selectedProjectManagedBy = selectedProject?.integration_source
  129. ? getManagedByFromOrganizationPartner(undefined, selectedProject.integration_source)
  130. : selectedOrganization?.billing_partner
  131. ? selectedOrganization.managed_by
  132. : undefined
  133. if (isLoadingProject || (isBranch && isLoadingParentProject) || !selectedProject) {
  134. if (!embedded) return <ShimmeringLoader className="p-2 md:mr-2 md:w-[90px]" />
  135. }
  136. const handleSetOpen = embedded
  137. ? (_value: boolean) => onClose?.()
  138. : (next: boolean) => {
  139. if (next) track('header_project_dropdown_opened')
  140. setOpen(next)
  141. }
  142. const selectorProps = {
  143. open,
  144. setOpen: handleSetOpen,
  145. selectedRef: ref,
  146. onSelect: (project: { ref: string }) => {
  147. const sanitizedRoute = sanitizeRoute(router.route, router.query)
  148. const href = sanitizedRoute?.replace('[ref]', project.ref) ?? `/project/${project.ref}`
  149. close()
  150. router.push(href)
  151. },
  152. renderRow: (project: Pick<OrgProject, 'ref' | 'name' | 'status' | 'integration_source'>) => (
  153. <ProjectRowLink
  154. project={project}
  155. selectedRef={ref}
  156. route={router.route}
  157. routerQueries={router.query}
  158. />
  159. ),
  160. renderActions: (_setOpen: (value: boolean) => void, options?: { embedded?: boolean }) =>
  161. projectCreationEnabled ? (
  162. <ProjectDropdownNewProjectActions
  163. organizationSlug={selectedOrganization?.slug}
  164. embedded={options?.embedded ?? false}
  165. onClose={close}
  166. onNavigate={(href) => router.push(href)}
  167. />
  168. ) : null,
  169. }
  170. if (embedded)
  171. return (
  172. <OrganizationProjectSelector {...selectorProps} embedded className={className} fetchOnMount />
  173. )
  174. return IS_PLATFORM ? (
  175. <ProjectDropdownPlatformView
  176. projectRef={project?.ref}
  177. projectName={selectedProject?.name ?? ''}
  178. projectManagedBy={selectedProjectManagedBy}
  179. selectorProps={selectorProps}
  180. />
  181. ) : (
  182. <ProjectDropdownNonPlatformView projectName={selectedProject?.name ?? ''} />
  183. )
  184. }