Skip to content

Commit 469b0d2

Browse files
devin-ai-integration[bot]kgowrudannysheridan
authored
Dashboard navigation refinements (#4569)
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Kapil Gowru <[email protected]> Co-authored-by: Danny Sheridan <[email protected]>
1 parent 1f8e2d0 commit 469b0d2

File tree

8 files changed

+173
-94
lines changed

8 files changed

+173
-94
lines changed

packages/fern-dashboard/src/app/[orgName]/(homepage)/@header/default.tsx

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { PopoverArrow } from "@radix-ui/react-popover";
2-
import { Book, RotateCcw } from "lucide-react";
2+
import { BookOpen, ExternalLink, RotateCcw } from "lucide-react";
33
import { Suspense } from "react";
44

55
import { getCurrentSession } from "@/app/services/auth0/getCurrentSession";
66
import type { Auth0OrgName } from "@/app/services/auth0/types";
7-
import { CreateMenuItem } from "@/components/auth/CreateMenuItem";
87
import { LogoutButton } from "@/components/auth/LogoutButton";
98
import { OrgSwitcher } from "@/components/auth/OrgSwitcher";
109
import { HeaderLinkButton } from "@/components/layout/HeaderLinkButton";
@@ -43,41 +42,61 @@ export default async function HeaderLayout({
4342
<div className="flex shrink-0 gap-2">
4443
<div className="hidden items-center md:flex">
4544
<SupportHeaderLink icon={false} />
46-
<HeaderLinkButton text="Docs" href="https://buildwithfern.com/learn" />
47-
<HeaderLinkButton
48-
text="Changelog"
49-
href="https://buildwithfern.com/learn/docs/getting-started/changelog"
50-
/>
51-
<CreateMenuItem accessToken={accessToken} />
45+
<Popover>
46+
<PopoverTrigger asChild>
47+
<button
48+
type="button"
49+
className="text-gray-1100 hover:text-gray-1200 flex h-8 w-8 cursor-pointer items-center justify-center transition-colors"
50+
>
51+
<BookOpen className="h-4 w-4" />
52+
</button>
53+
</PopoverTrigger>
54+
<PopoverContent align="end" collisionPadding={8} className="w-[200px] p-1">
55+
<PopoverArrow className="fill-popover" />
56+
<div className="flex flex-col">
57+
<HeaderLinkButton
58+
text="Docs"
59+
className="justify-start px-2 text-left"
60+
href="https://buildwithfern.com/learn"
61+
rightIcon={<ExternalLink className="h-3 w-3" />}
62+
/>
63+
<HeaderLinkButton
64+
text="Changelog"
65+
className="justify-start px-2 text-left"
66+
href="https://buildwithfern.com/learn/docs/getting-started/changelog"
67+
rightIcon={<ExternalLink className="h-3 w-3" />}
68+
/>
69+
</div>
70+
</PopoverContent>
71+
</Popover>
5272
<ThemeToggle />
5373
</div>
5474
<Popover>
5575
<PopoverTrigger className="cursor-pointer">
5676
<ProfileImage picture={picture} name={name} />
5777
</PopoverTrigger>
58-
<PopoverContent collisionPadding={8}>
78+
<PopoverContent collisionPadding={8} className="w-[200px]">
5979
<PopoverArrow className="fill-popover" />
6080
<div className="flex flex-col gap-4">
6181
<div className="flex flex-col">
6282
<div className="text-gray-1200 text-sm">{name}</div>
6383
<div className="text-xs text-gray-800">{email}</div>
6484
</div>
6585
<div className="flex flex-col md:hidden">
66-
<CreateMenuItem accessToken={accessToken} />
6786
<SupportHeaderLink
68-
className="justify-start px-0 text-left hover:px-2 has-[>svg]:px-0 hover:has-[>svg]:px-2"
87+
className="justify-start text-left !px-0"
6988
buttonProps={{ variant: "ghost" }}
7089
icon={true}
7190
/>
7291
<HeaderLinkButton
7392
text="Docs"
74-
className="justify-start px-0 text-left hover:px-2 has-[>svg]:px-0 hover:has-[>svg]:px-2"
93+
className="justify-start text-left !px-0"
7594
href="https://buildwithfern.com/learn"
76-
icon={<Book className="h-4 w-4" />}
95+
icon={<BookOpen className="h-4 w-4" />}
7796
/>
7897
<HeaderLinkButton
7998
text="Changelog"
80-
className="justify-start px-0 text-left hover:px-2 has-[>svg]:px-0 hover:has-[>svg]:px-2"
99+
className="justify-start text-left !px-0"
81100
href="https://buildwithfern.com/learn/docs/getting-started/changelog"
82101
icon={<RotateCcw className="h-4 w-4" />}
83102
/>

packages/fern-dashboard/src/app/[orgName]/(homepage)/@navbar/default.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ export default async function Navbar({ params }: Readonly<{ params: Promise<{ or
3636
<NavbarItem title="SDKs" iconType="sdks" href="/sdks" />
3737
</FeatureFlaggedServerSide>
3838
<NavbarSectionTitle title="Settings" />
39-
<NavbarItem title="Members" iconType="members" href="/members" />
4039
<FeatureFlaggedServerSide flag={PosthogFeatureFlag.ENABLE_INCIDENTS_PAGE} orgName={orgName}>
4140
<NavbarItem title="Incidents" iconType="incidents" href="/incidents" />
4241
</FeatureFlaggedServerSide>
42+
<NavbarItem title="Members" iconType="members" href="/members" />
43+
<NavbarItem title="General" iconType="settings" href="/settings" />
4344
<FeatureFlaggedServerSide flag={PosthogFeatureFlag.ENABLE_API_KEYS_PAGE} orgName={orgName}>
4445
<NavbarItem title="API Keys" mobileTitle="Keys" iconType="api-keys" href="/api-keys" />
4546
</FeatureFlaggedServerSide>
46-
<NavbarItem title="Settings" iconType="settings" href="/settings" />
4747
<FeatureFlaggedServerSide flag={PosthogFeatureFlag.ENABLE_BILLING_PAGE} orgName={orgName}>
4848
<NavbarItem title="Billing" iconType="billing" href="/billing" />
4949
</FeatureFlaggedServerSide>

packages/fern-dashboard/src/components/auth/OrgSwitcher.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export async function OrgSwitcher({ currentOrgName }: { currentOrgName?: Auth0Or
2727
const isFernAdmin = await isFernEmployee(session.user.sub);
2828

2929
return (
30-
<OrgSwitcherClient organizations={organizations} currentOrgName={currentOrgName} isFernAdmin={isFernAdmin} />
30+
<OrgSwitcherClient
31+
organizations={organizations}
32+
currentOrgName={currentOrgName}
33+
isFernAdmin={isFernAdmin}
34+
accessToken={session.accessToken}
35+
/>
3136
);
3237
}

packages/fern-dashboard/src/components/auth/OrgSwitcherClient.tsx

Lines changed: 105 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
"use client";
22

33
import { useRouter } from "@bprogress/next/app";
4-
import { ChevronDown, Clock } from "lucide-react";
4+
import { ChevronDown, Clock, Plus } from "lucide-react";
55
import Link from "next/link";
66
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
77

88
import type { Auth0Organization, Auth0OrgName } from "@/app/services/auth0/types";
9-
9+
import { CreateOrganizationModal } from "@/components/auth/CreateOrganizationModal";
1010
import { Button } from "@/components/ui/button";
1111
import { SearchableDropdown, type SearchableDropdownRef } from "@/components/ui/SearchableDropdown";
1212
import { WrapWithKeyboardShortcut } from "@/components/ui/WrapWithKeyboardShortcut";
@@ -28,8 +28,9 @@ const OrgSwitcherClientInternal = forwardRef<
2828
organizations: Auth0Organization[];
2929
currentOrgName?: Auth0OrgName;
3030
isFernAdmin: boolean;
31+
accessToken: string;
3132
}
32-
>(({ organizations, currentOrgName, isFernAdmin }, ref) => {
33+
>(({ organizations, currentOrgName, isFernAdmin, accessToken }, ref) => {
3334
const dropdownRef = useRef<SearchableDropdownRef>(null);
3435

3536
useImperativeHandle(ref, () => ({
@@ -41,6 +42,7 @@ const OrgSwitcherClientInternal = forwardRef<
4142
const [localOrgName, setLocalOrgName] = useState(currentOrgName);
4243
const [searchTerm, setSearchTerm] = useState("");
4344
const [recentOrgNames, setRecentOrgNames] = useState<Auth0OrgName[]>([]);
45+
const [showOrgModal, setShowOrgModal] = useState(false);
4446

4547
useEffect(() => {
4648
setLocalOrgName(orgName);
@@ -119,84 +121,113 @@ const OrgSwitcherClientInternal = forwardRef<
119121

120122
const currentOrg = organizations.find((org) => org.name === localOrgName);
121123

124+
const shouldShowSearch = organizations.length > 10;
125+
122126
return (
123-
<SearchableDropdown
124-
ref={dropdownRef}
125-
items={filteredOrganizationsWithAdmin}
126-
searchTerm={searchTerm}
127-
onSearchChange={setSearchTerm}
128-
onSelect={(org) => onSelectOrg(org.name)}
129-
searchPlaceholder="Search organizations..."
130-
emptyMessage="No organizations found"
131-
getItemKey={(org) => org.id}
132-
shouldShowSearch={organizations.length > 10}
133-
renderItem={(organization, onSelectFromDropdown, isHighlighted) => {
134-
// Check if this is the admin option
135-
const isAdminOption = "__isAdminOption" in organization && organization.__isAdminOption;
136-
137-
if (isAdminOption) {
127+
<>
128+
<SearchableDropdown
129+
ref={dropdownRef}
130+
items={filteredOrganizationsWithAdmin}
131+
searchTerm={searchTerm}
132+
onSearchChange={setSearchTerm}
133+
onSelect={(org) => onSelectOrg(org.name)}
134+
searchPlaceholder="Search organizations..."
135+
emptyMessage="No organizations found"
136+
getItemKey={(org) => org.id}
137+
shouldShowSearch={shouldShowSearch}
138+
searchRightContent={
139+
<Button
140+
size="sm"
141+
variant="ghost"
142+
className="h-[38px] w-[38px] shrink-0 p-0"
143+
onClick={() => setShowOrgModal(true)}
144+
>
145+
<Plus className="h-4 w-4" />
146+
<span className="sr-only">Create organization</span>
147+
</Button>
148+
}
149+
headerContent={
150+
!shouldShowSearch ? (
151+
<Button
152+
size="sm"
153+
variant="ghost"
154+
className="flex w-full items-center justify-start gap-2 px-2"
155+
onClick={() => setShowOrgModal(true)}
156+
>
157+
<Plus className="h-4 w-4" />
158+
<span>Create new org</span>
159+
</Button>
160+
) : undefined
161+
}
162+
renderItem={(organization, onSelectFromDropdown, isHighlighted) => {
163+
// Check if this is the admin option
164+
const isAdminOption = "__isAdminOption" in organization && organization.__isAdminOption;
165+
166+
if (isAdminOption) {
167+
return (
168+
<button
169+
type="button"
170+
className={cn(
171+
"flex w-full cursor-pointer items-center justify-between px-3 rounded py-1.5 text-left text-sm focus:outline-none flex-wrap text-muted-foreground",
172+
isHighlighted ? "bg-gray-300" : "hover:bg-gray-300"
173+
)}
174+
onClick={() => {
175+
onSelectOrg(organization.name);
176+
onSelectFromDropdown();
177+
}}
178+
>
179+
<div className="flex flex-1 items-center gap-2">
180+
Go to <code className="text-wrap max-w-[170px]">{organization.name}</code>
181+
</div>
182+
<div className="flex-shrink-0 justify-end"></div>
183+
</button>
184+
);
185+
}
186+
187+
// Regular organization
188+
const isRecent = recentOrgNames.includes(organization.name);
189+
const isCurrent = organization.name === localOrgName;
138190
return (
139-
<button
140-
type="button"
191+
<Link
141192
className={cn(
142-
"flex w-full cursor-pointer items-center justify-between px-3 rounded py-1.5 text-left text-sm focus:outline-none flex-wrap text-muted-foreground",
143-
isHighlighted ? "bg-gray-300" : "hover:bg-gray-300"
193+
"flex w-full cursor-pointer items-center justify-between px-3 rounded py-1.5 text-left text-sm focus:outline-none",
194+
searchTerm.length > 0 && isHighlighted ? "bg-gray-300" : "hover:bg-gray-300"
144195
)}
196+
href={getPathnameForOrg(organization.name)}
197+
onMouseOver={() => {
198+
onHoverOrg(organization.name);
199+
}}
145200
onClick={() => {
146-
onSelectOrg(organization.name);
201+
if (isCurrent) {
202+
return;
203+
}
204+
onClickOrg(organization.name);
147205
onSelectFromDropdown();
148206
}}
149207
>
150-
<div className="flex flex-1 items-center gap-2">
151-
Go to <code className="text-wrap max-w-[170px]">{organization.name}</code>
208+
<div className="flex items-center gap-2">
209+
<OrgLogo organization={organization} />
210+
{getOrgDisplayName(organization)}
152211
</div>
153-
<div className="flex-shrink-0 justify-end"></div>
154-
</button>
212+
{isRecent && <Clock className="h-4 w-4 text-gray-600" />}
213+
</Link>
155214
);
156-
}
157-
158-
// Regular organization
159-
const isRecent = recentOrgNames.includes(organization.name);
160-
const isCurrent = organization.name === localOrgName;
161-
return (
162-
<Link
163-
className={cn(
164-
"flex w-full cursor-pointer items-center justify-between px-3 rounded py-1.5 text-left text-sm focus:outline-none",
165-
searchTerm.length > 0 && isHighlighted ? "bg-gray-300" : "hover:bg-gray-300"
166-
)}
167-
href={getPathnameForOrg(organization.name)}
168-
onMouseOver={() => {
169-
onHoverOrg(organization.name);
170-
}}
171-
onClick={() => {
172-
if (isCurrent) {
173-
return;
174-
}
175-
onClickOrg(organization.name);
176-
onSelectFromDropdown();
177-
}}
178-
>
179-
<div className="flex items-center gap-2">
180-
<OrgLogo organization={organization} />
181-
{getOrgDisplayName(organization)}
182-
</div>
183-
{isRecent && <Clock className="h-4 w-4 text-gray-600" />}
184-
</Link>
185-
);
186-
}}
187-
>
188-
<Button
189-
variant="outline"
190-
className="shrink-0 justify-between !pl-2 md:min-w-[200px]"
191-
disabled={organizations.length === 0}
215+
}}
192216
>
193-
<div className="flex items-center gap-2">
194-
{currentOrg && <OrgLogo organization={currentOrg} />}
195-
{currentOrg ? getOrgDisplayName(currentOrg) : "Select Organization"}
196-
</div>
197-
<ChevronDown className="h-4 w-4 opacity-50" />
198-
</Button>
199-
</SearchableDropdown>
217+
<Button
218+
variant="outline"
219+
className="shrink-0 justify-between !pl-2 md:min-w-[200px]"
220+
disabled={organizations.length === 0}
221+
>
222+
<div className="flex items-center gap-2">
223+
{currentOrg && <OrgLogo organization={currentOrg} />}
224+
{currentOrg ? getOrgDisplayName(currentOrg) : "Select Organization"}
225+
</div>
226+
<ChevronDown className="h-4 w-4 opacity-50" />
227+
</Button>
228+
</SearchableDropdown>
229+
<CreateOrganizationModal accessToken={accessToken} open={showOrgModal} onOpenChange={setShowOrgModal} />
230+
</>
200231
);
201232
});
202233

@@ -205,11 +236,13 @@ OrgSwitcherClientInternal.displayName = "OrgSwitcherClientInternal";
205236
export const OrgSwitcherClient = ({
206237
organizations: initialOrganizations,
207238
currentOrgName,
208-
isFernAdmin
239+
isFernAdmin,
240+
accessToken
209241
}: {
210242
organizations: Auth0Organization[];
211243
currentOrgName?: Auth0OrgName;
212244
isFernAdmin: boolean;
245+
accessToken: string;
213246
}) => {
214247
const orgSwitcherRef = useRef<OrgSwitcherClientRef>(null);
215248

@@ -228,6 +261,7 @@ export const OrgSwitcherClient = ({
228261
organizations={organizations}
229262
currentOrgName={currentOrgName}
230263
isFernAdmin={isFernAdmin}
264+
accessToken={accessToken}
231265
/>
232266
</WrapWithKeyboardShortcut>
233267
);

packages/fern-dashboard/src/components/layout/HeaderLinkButton.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,40 @@ export declare namespace HeaderLinkButton {
77
text: string;
88
href: string;
99
icon?: React.ReactNode;
10+
rightIcon?: React.ReactNode;
1011
className?: string;
1112
onClick?: () => void;
1213
buttonProps?: React.ComponentProps<typeof Button>;
14+
openInNewTab?: boolean;
1315
}
1416
}
1517

16-
export function HeaderLinkButton({ text, href, icon, className, onClick, buttonProps }: HeaderLinkButton.Props) {
18+
export function HeaderLinkButton({
19+
text,
20+
href,
21+
icon,
22+
rightIcon,
23+
className,
24+
onClick,
25+
buttonProps,
26+
openInNewTab = true
27+
}: HeaderLinkButton.Props) {
1728
if (onClick) {
1829
return (
1930
<Button size="sm" variant="ghost" className={className} onClick={onClick} {...buttonProps}>
2031
{icon}
2132
{text}
33+
{rightIcon && <span className="ml-auto">{rightIcon}</span>}
2234
</Button>
2335
);
2436
}
2537

2638
return (
2739
<Button size="sm" variant="ghost" asChild className={className} {...buttonProps}>
28-
<a href={href} target="_blank">
40+
<a href={href} {...(openInNewTab && { target: "_blank" })}>
2941
{icon}
3042
{text}
43+
{rightIcon && <span className="ml-auto">{rightIcon}</span>}
3144
</a>
3245
</Button>
3346
);

0 commit comments

Comments
 (0)