| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- import { useParams } from 'common'
- import dayjs from 'dayjs'
- import { AnimatePresence, motion } from 'framer-motion'
- import { ChevronLeft } from 'lucide-react'
- import Link from 'next/link'
- import { useRouter } from 'next/router'
- import { ReactNode, useMemo } from 'react'
- import { Badge, cn } from 'ui'
- import { CommandMenuTriggerInput } from 'ui-patterns'
- import { BreadcrumbsView } from './BreadcrumbsView'
- import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown'
- import { HomeIcon } from './HomeIcon'
- import { LocalVersionPopover } from './LocalVersionPopover'
- import { MergeRequestButton } from './MergeRequestButton'
- import { ConnectButton } from '@/components/interfaces/ConnectButton/ConnectButton'
- import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
- import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
- import { UserDropdown } from '@/components/interfaces/UserDropdown'
- import { AdvisorButton } from '@/components/layouts/AppLayout/AdvisorButton'
- import { AssistantButton } from '@/components/layouts/AppLayout/AssistantButton'
- import { BranchDropdown } from '@/components/layouts/AppLayout/BranchDropdown'
- import { InlineEditorButton } from '@/components/layouts/AppLayout/InlineEditorButton'
- import { OrganizationDropdown } from '@/components/layouts/AppLayout/OrganizationDropdown'
- import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
- import { HelpButton } from '@/components/ui/HelpPanel/HelpButton'
- import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
- import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { IS_PLATFORM } from '@/lib/constants'
- import { useTrack } from '@/lib/telemetry/track'
- import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
- import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
- const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
- <span className={cn('text-border-stronger pr-2', className)} {...props}>
- <svg
- viewBox="0 0 24 24"
- width="16"
- height="16"
- stroke="currentColor"
- strokeWidth="1"
- strokeLinecap="round"
- strokeLinejoin="round"
- fill="none"
- shapeRendering="geometricPrecision"
- aria-hidden={true}
- >
- <path d="M16 3.549L7.12 20.600" />
- </svg>
- </span>
- )
- interface LayoutHeaderProps {
- customHeaderComponents?: ReactNode
- breadcrumbs?: unknown[]
- headerTitle?: string
- backToDashboardURL?: string
- }
- export const LayoutHeader = ({
- customHeaderComponents,
- breadcrumbs = [],
- headerTitle,
- backToDashboardURL,
- }: LayoutHeaderProps) => {
- const router = useRouter()
- const { ref: projectRef, slug } = useParams()
- const { data: selectedProject } = useSelectedProjectQuery()
- const { data: selectedOrganization } = useSelectedOrganizationQuery()
- const track = useTrack()
- const commandMenuEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
- const isAccountPage = router.pathname.startsWith('/account')
- // We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
- const { data: orgUsage } = useOrgUsageQuery(
- { orgSlug: selectedOrganization?.slug },
- { enabled: selectedOrganization?.usage_billing_enabled === false }
- )
- const exceedingLimits = useMemo(() => {
- if (orgUsage) {
- return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
- } else {
- return false
- }
- }, [orgUsage])
- const isNewProject =
- selectedProject?.inserted_at !== undefined &&
- dayjs(selectedProject.inserted_at).isAfter(dayjs().subtract(5, 'day'))
- const connectButtonType = isNewProject ? 'primary' : 'default'
- // show org selection if we are on a project page or on a explicit org route
- const showOrgSelection = slug || (selectedOrganization && projectRef)
- return (
- <>
- <header className="hidden md:flex h-11 md:h-12 items-center shrink-0 border-b">
- {backToDashboardURL && isAccountPage && (
- <div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
- <Link
- href={backToDashboardURL}
- onClick={() => track('header_back_to_dashboard_clicked')}
- className="flex items-center justify-center border-none bg-transparent! rounded-md min-w-[30px] w-[30px] h-[30px] text-foreground-lighter hover:text-foreground transition-colors"
- >
- <ChevronLeft strokeWidth={1.5} size={16} />
- </Link>
- </div>
- )}
- <div
- className={cn(
- 'flex items-center justify-between h-full pr-3 flex-1 overflow-x-auto gap-x-8 pl-4'
- )}
- >
- <div className="flex md:hidden items-center text-sm not-sr-only">
- <AnimatePresence>
- {headerTitle && (
- <motion.div
- className="flex items-center -ml-1"
- initial={{ opacity: 0, x: -20 }}
- animate={{ opacity: 1, x: 0 }}
- exit={{ opacity: 0, x: -20 }}
- transition={{
- duration: 0.15,
- ease: 'easeOut',
- }}
- >
- <LayoutHeaderDivider />
- <span className="text-foreground">{headerTitle}</span>
- </motion.div>
- )}
- </AnimatePresence>
- </div>
- <div className="hidden md:flex items-center text-sm">
- <HomeIcon />
- <div className="flex items-center md:pl-2">
- {showOrgSelection && IS_PLATFORM ? (
- <>
- <LayoutHeaderDivider className="hidden md:block" />
- <OrganizationDropdown />
- </>
- ) : null}
- <AnimatePresence>
- {projectRef && (
- <motion.div
- className="flex items-center"
- initial={{ opacity: 0, x: -20 }}
- animate={{ opacity: 1, x: 0 }}
- exit={{ opacity: 0, x: -20 }}
- transition={{
- duration: 0.15,
- ease: 'easeOut',
- }}
- >
- {IS_PLATFORM && <LayoutHeaderDivider />}
- <ProjectDropdown />
- {exceedingLimits && (
- <div className="ml-2">
- <Link
- href={`/org/${selectedOrganization?.slug}/usage`}
- onClick={() => track('header_exceeding_usage_badge_clicked')}
- >
- <Badge variant="destructive">Exceeding usage limits</Badge>
- </Link>
- </div>
- )}
- {selectedProject && IS_PLATFORM && (
- <>
- <LayoutHeaderDivider />
- <BranchDropdown />
- </>
- )}
- </motion.div>
- )}
- </AnimatePresence>
- </div>
- <AnimatePresence>
- {headerTitle && (
- <motion.div
- className="flex items-center"
- initial={{ opacity: 0, x: -20 }}
- animate={{ opacity: 1, x: 0 }}
- exit={{ opacity: 0, x: -20 }}
- transition={{
- duration: 0.15,
- ease: 'easeOut',
- }}
- >
- <LayoutHeaderDivider />
- <span className="text-foreground">{headerTitle}</span>
- </motion.div>
- )}
- </AnimatePresence>
- <AnimatePresence>
- {projectRef && (
- <motion.div
- className="ml-3 items-center gap-x-2 flex"
- initial={{ opacity: 0, x: -20 }}
- animate={{ opacity: 1, x: 0 }}
- exit={{ opacity: 0, x: -20 }}
- transition={{
- duration: 0.15,
- ease: 'easeOut',
- }}
- >
- {IS_PLATFORM && <MergeRequestButton />}
- <ConnectButton buttonType={connectButtonType} />
- </motion.div>
- )}
- </AnimatePresence>
- <BreadcrumbsView defaultValue={breadcrumbs} />
- </div>
- <div className="flex items-center gap-x-2">
- {customHeaderComponents && customHeaderComponents}
- {IS_PLATFORM ? (
- <>
- <FeedbackDropdown />
- <div className="flex items-center gap-1 md:gap-2">
- <CommandMenuTriggerInput
- showShortcut={commandMenuEnabled}
- placeholder="Search..."
- className={cn(
- 'hidden md:flex md:min-w-32 xl:min-w-32 rounded-full bg-transparent',
- '[&_.command-shortcut]:border-none',
- '[&_.command-shortcut]:pr-2',
- '[&_.command-shortcut]:bg-transparent',
- '[&_.command-shortcut]:text-foreground-lighter',
- '[&_.command-shortcut]:shadow-none'
- )}
- />
- <HelpButton />
- <AdvisorButton projectRef={projectRef} />
- <AnimatePresence initial={false}>
- {!!projectRef && (
- <>
- <InlineEditorButton />
- <AssistantButton />
- </>
- )}
- </AnimatePresence>
- </div>
- <UserDropdown triggerClassName="hidden md:flex" />
- </>
- ) : (
- <>
- <LocalVersionPopover />
- <div className="flex items-center gap-1 md:gap-2">
- <CommandMenuTriggerInput
- placeholder="Search..."
- className="hidden md:flex md:min-w-32 xl:min-w-32 rounded-full bg-transparent
- [&_.command-shortcut]:border-none
- [&_.command-shortcut]:pr-2
- [&_.command-shortcut]:bg-transparent
- [&_.command-shortcut]:text-foreground-lighter
- [&_.command-shortcut]:shadow-none
- "
- />
- <HelpButton />
- <AdvisorButton projectRef={projectRef} />
- <AnimatePresence initial={false}>
- {!!projectRef && (
- <>
- <InlineEditorButton />
- <AssistantButton />
- </>
- )}
- </AnimatePresence>
- </div>
- <LocalDropdown triggerClassName="hidden md:flex" />
- </>
- )}
- </div>
- </div>
- </header>
- <ConnectSheet />
- </>
- )
- }
|