LayoutHeader.tsx292 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import { AnimatePresence, motion } from 'framer-motion'
4import { ChevronLeft } from 'lucide-react'
5import Link from 'next/link'
6import { useRouter } from 'next/router'
7import { ReactNode, useMemo } from 'react'
8import { Badge, cn } from 'ui'
9import { CommandMenuTriggerInput } from 'ui-patterns'
10
11import { BreadcrumbsView } from './BreadcrumbsView'
12import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown'
13import { HomeIcon } from './HomeIcon'
14import { LocalVersionPopover } from './LocalVersionPopover'
15import { MergeRequestButton } from './MergeRequestButton'
16import { ConnectButton } from '@/components/interfaces/ConnectButton/ConnectButton'
17import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
18import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
19import { UserDropdown } from '@/components/interfaces/UserDropdown'
20import { AdvisorButton } from '@/components/layouts/AppLayout/AdvisorButton'
21import { AssistantButton } from '@/components/layouts/AppLayout/AssistantButton'
22import { BranchDropdown } from '@/components/layouts/AppLayout/BranchDropdown'
23import { InlineEditorButton } from '@/components/layouts/AppLayout/InlineEditorButton'
24import { OrganizationDropdown } from '@/components/layouts/AppLayout/OrganizationDropdown'
25import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
26import { HelpButton } from '@/components/ui/HelpPanel/HelpButton'
27import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
28import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
29import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
30import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
31import { IS_PLATFORM } from '@/lib/constants'
32import { useTrack } from '@/lib/telemetry/track'
33import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
34import { useIsShortcutEnabled } from '@/state/shortcuts/useIsShortcutEnabled'
35
36const LayoutHeaderDivider = ({ className, ...props }: React.HTMLProps<HTMLSpanElement>) => (
37 <span className={cn('text-border-stronger pr-2', className)} {...props}>
38 <svg
39 viewBox="0 0 24 24"
40 width="16"
41 height="16"
42 stroke="currentColor"
43 strokeWidth="1"
44 strokeLinecap="round"
45 strokeLinejoin="round"
46 fill="none"
47 shapeRendering="geometricPrecision"
48 aria-hidden={true}
49 >
50 <path d="M16 3.549L7.12 20.600" />
51 </svg>
52 </span>
53)
54
55interface LayoutHeaderProps {
56 customHeaderComponents?: ReactNode
57 breadcrumbs?: unknown[]
58 headerTitle?: string
59 backToDashboardURL?: string
60}
61
62export const LayoutHeader = ({
63 customHeaderComponents,
64 breadcrumbs = [],
65 headerTitle,
66 backToDashboardURL,
67}: LayoutHeaderProps) => {
68 const router = useRouter()
69 const { ref: projectRef, slug } = useParams()
70 const { data: selectedProject } = useSelectedProjectQuery()
71 const { data: selectedOrganization } = useSelectedOrganizationQuery()
72 const track = useTrack()
73
74 const commandMenuEnabled = useIsShortcutEnabled(SHORTCUT_IDS.COMMAND_MENU_OPEN)
75
76 const isAccountPage = router.pathname.startsWith('/account')
77
78 // 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)
79 const { data: orgUsage } = useOrgUsageQuery(
80 { orgSlug: selectedOrganization?.slug },
81 { enabled: selectedOrganization?.usage_billing_enabled === false }
82 )
83
84 const exceedingLimits = useMemo(() => {
85 if (orgUsage) {
86 return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
87 } else {
88 return false
89 }
90 }, [orgUsage])
91
92 const isNewProject =
93 selectedProject?.inserted_at !== undefined &&
94 dayjs(selectedProject.inserted_at).isAfter(dayjs().subtract(5, 'day'))
95
96 const connectButtonType = isNewProject ? 'primary' : 'default'
97
98 // show org selection if we are on a project page or on a explicit org route
99 const showOrgSelection = slug || (selectedOrganization && projectRef)
100
101 return (
102 <>
103 <header className="hidden md:flex h-11 md:h-12 items-center shrink-0 border-b">
104 {backToDashboardURL && isAccountPage && (
105 <div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
106 <Link
107 href={backToDashboardURL}
108 onClick={() => track('header_back_to_dashboard_clicked')}
109 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"
110 >
111 <ChevronLeft strokeWidth={1.5} size={16} />
112 </Link>
113 </div>
114 )}
115 <div
116 className={cn(
117 'flex items-center justify-between h-full pr-3 flex-1 overflow-x-auto gap-x-8 pl-4'
118 )}
119 >
120 <div className="flex md:hidden items-center text-sm not-sr-only">
121 <AnimatePresence>
122 {headerTitle && (
123 <motion.div
124 className="flex items-center -ml-1"
125 initial={{ opacity: 0, x: -20 }}
126 animate={{ opacity: 1, x: 0 }}
127 exit={{ opacity: 0, x: -20 }}
128 transition={{
129 duration: 0.15,
130 ease: 'easeOut',
131 }}
132 >
133 <LayoutHeaderDivider />
134 <span className="text-foreground">{headerTitle}</span>
135 </motion.div>
136 )}
137 </AnimatePresence>
138 </div>
139
140 <div className="hidden md:flex items-center text-sm">
141 <HomeIcon />
142 <div className="flex items-center md:pl-2">
143 {showOrgSelection && IS_PLATFORM ? (
144 <>
145 <LayoutHeaderDivider className="hidden md:block" />
146 <OrganizationDropdown />
147 </>
148 ) : null}
149
150 <AnimatePresence>
151 {projectRef && (
152 <motion.div
153 className="flex items-center"
154 initial={{ opacity: 0, x: -20 }}
155 animate={{ opacity: 1, x: 0 }}
156 exit={{ opacity: 0, x: -20 }}
157 transition={{
158 duration: 0.15,
159 ease: 'easeOut',
160 }}
161 >
162 {IS_PLATFORM && <LayoutHeaderDivider />}
163
164 <ProjectDropdown />
165
166 {exceedingLimits && (
167 <div className="ml-2">
168 <Link
169 href={`/org/${selectedOrganization?.slug}/usage`}
170 onClick={() => track('header_exceeding_usage_badge_clicked')}
171 >
172 <Badge variant="destructive">Exceeding usage limits</Badge>
173 </Link>
174 </div>
175 )}
176
177 {selectedProject && IS_PLATFORM && (
178 <>
179 <LayoutHeaderDivider />
180 <BranchDropdown />
181 </>
182 )}
183 </motion.div>
184 )}
185 </AnimatePresence>
186 </div>
187
188 <AnimatePresence>
189 {headerTitle && (
190 <motion.div
191 className="flex items-center"
192 initial={{ opacity: 0, x: -20 }}
193 animate={{ opacity: 1, x: 0 }}
194 exit={{ opacity: 0, x: -20 }}
195 transition={{
196 duration: 0.15,
197 ease: 'easeOut',
198 }}
199 >
200 <LayoutHeaderDivider />
201 <span className="text-foreground">{headerTitle}</span>
202 </motion.div>
203 )}
204 </AnimatePresence>
205
206 <AnimatePresence>
207 {projectRef && (
208 <motion.div
209 className="ml-3 items-center gap-x-2 flex"
210 initial={{ opacity: 0, x: -20 }}
211 animate={{ opacity: 1, x: 0 }}
212 exit={{ opacity: 0, x: -20 }}
213 transition={{
214 duration: 0.15,
215 ease: 'easeOut',
216 }}
217 >
218 {IS_PLATFORM && <MergeRequestButton />}
219 <ConnectButton buttonType={connectButtonType} />
220 </motion.div>
221 )}
222 </AnimatePresence>
223 <BreadcrumbsView defaultValue={breadcrumbs} />
224 </div>
225 <div className="flex items-center gap-x-2">
226 {customHeaderComponents && customHeaderComponents}
227 {IS_PLATFORM ? (
228 <>
229 <FeedbackDropdown />
230
231 <div className="flex items-center gap-1 md:gap-2">
232 <CommandMenuTriggerInput
233 showShortcut={commandMenuEnabled}
234 placeholder="Search..."
235 className={cn(
236 'hidden md:flex md:min-w-32 xl:min-w-32 rounded-full bg-transparent',
237 '[&_.command-shortcut]:border-none',
238 '[&_.command-shortcut]:pr-2',
239 '[&_.command-shortcut]:bg-transparent',
240 '[&_.command-shortcut]:text-foreground-lighter',
241 '[&_.command-shortcut]:shadow-none'
242 )}
243 />
244 <HelpButton />
245 <AdvisorButton projectRef={projectRef} />
246 <AnimatePresence initial={false}>
247 {!!projectRef && (
248 <>
249 <InlineEditorButton />
250 <AssistantButton />
251 </>
252 )}
253 </AnimatePresence>
254 </div>
255 <UserDropdown triggerClassName="hidden md:flex" />
256 </>
257 ) : (
258 <>
259 <LocalVersionPopover />
260 <div className="flex items-center gap-1 md:gap-2">
261 <CommandMenuTriggerInput
262 placeholder="Search..."
263 className="hidden md:flex md:min-w-32 xl:min-w-32 rounded-full bg-transparent
264 [&_.command-shortcut]:border-none
265 [&_.command-shortcut]:pr-2
266 [&_.command-shortcut]:bg-transparent
267 [&_.command-shortcut]:text-foreground-lighter
268 [&_.command-shortcut]:shadow-none
269 "
270 />
271 <HelpButton />
272 <AdvisorButton projectRef={projectRef} />
273 <AnimatePresence initial={false}>
274 {!!projectRef && (
275 <>
276 <InlineEditorButton />
277 <AssistantButton />
278 </>
279 )}
280 </AnimatePresence>
281 </div>
282 <LocalDropdown triggerClassName="hidden md:flex" />
283 </>
284 )}
285 </div>
286 </div>
287 </header>
288
289 <ConnectSheet />
290 </>
291 )
292}