Dashboard: Org menu improvements for admins (#1969)

* Adds path for admin route

* Updates impersonation banner and show new org button if only 1 org

* Simpler UI for displaying admin access
This commit is contained in:
James Ritchie
2025-05-07 18:38:00 +01:00
committed by GitHub
parent 04b9973f20
commit 3e45f785a0
3 changed files with 55 additions and 23 deletions
@@ -1,21 +1,29 @@
import { UserMinusIcon } from "@heroicons/react/20/solid";
import { Form } from "@remix-run/react";
import { Button } from "./primitives/Buttons";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./primitives/Tooltip";
export function ImpersonationBanner() {
return (
<div className="w-full">
<Form action="/resources/impersonation" method="delete" reloadDocument className="w-full">
<Button
type="submit"
variant="small-menu-item"
LeadingIcon={UserMinusIcon}
fullWidth
textAlignLeft
className="text-amber-400"
>
Stop impersonating
</Button>
<div>
<Form action="/resources/impersonation" method="delete" reloadDocument>
<TooltipProvider disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger>
<Button
type="submit"
variant="small-menu-item"
LeadingIcon={UserMinusIcon}
fullWidth
textAlignLeft
className="text-amber-400"
/>
</TooltipTrigger>
<TooltipContent side="bottom" className={"text-xs"}>
Stop impersonating
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Form>
</div>
);
@@ -16,6 +16,7 @@ import {
RectangleStackIcon,
ServerStackIcon,
Squares2X2Icon,
UsersIcon,
} from "@heroicons/react/20/solid";
import { useLocation, useNavigation } from "@remix-run/react";
import { useEffect, useRef, useState, type ReactNode } from "react";
@@ -28,12 +29,14 @@ import { Avatar } from "~/components/primitives/Avatar";
import { type MatchedEnvironment } from "~/hooks/useEnvironment";
import { type MatchedOrganization } from "~/hooks/useOrganizations";
import { type MatchedProject } from "~/hooks/useProject";
import { useHasAdminAccess } from "~/hooks/useUser";
import { type User } from "~/models/user.server";
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
import { type FeedbackType } from "~/routes/resources.feedback";
import { cn } from "~/utils/cn";
import {
accountPath,
adminPath,
logoutPath,
newOrganizationPath,
newProjectPath,
@@ -110,6 +113,7 @@ export function SideMenu({
const currentPlan = useCurrentPlan();
const { isConnected } = useDevPresence();
const isFreeUser = currentPlan?.v3Subscription?.isPaying === false;
const isAdmin = useHasAdminAccess();
useEffect(() => {
const handleScroll = () => {
@@ -143,6 +147,20 @@ export function SideMenu({
project={project}
user={user}
/>
{isAdmin && !user.isImpersonating ? (
<TooltipProvider disableHoverableContent={true}>
<Tooltip>
<TooltipTrigger>
<LinkButton variant="minimal/medium" to={adminPath()} TrailingIcon={UsersIcon} />
</TooltipTrigger>
<TooltipContent side="bottom" className={"text-xs"}>
Admin dashboard
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : isAdmin && user.isImpersonating ? (
<ImpersonationBanner />
) : null}
</div>
<div
className="overflow-hidden overflow-y-auto pt-2 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
@@ -325,10 +343,7 @@ function ProjectSelector({
<PopoverArrowTrigger
isOpen={isOrgMenuOpen}
overflowHidden
className={cn(
"h-8 w-full justify-between py-1 pl-1.5",
user.isImpersonating && "border border-dashed border-amber-400"
)}
className="h-8 w-full justify-between py-1 pl-1.5"
>
<span className="flex items-center gap-1.5 overflow-hidden">
<Avatar avatar={organization.avatar} size={1.25} orgName={organization.title} />
@@ -412,19 +427,24 @@ function ProjectSelector({
<PopoverMenuItem to={newProjectPath(organization)} title="New project" icon={PlusIcon} />
</div>
<div className="border-t border-charcoal-700 p-1">
<SwitchOrganizations organizations={organizations} organization={organization} />
{organizations.length > 1 ? (
<SwitchOrganizations organizations={organizations} organization={organization} />
) : (
<PopoverMenuItem
to={newOrganizationPath()}
title="New organization"
icon={PlusIcon}
leadingIconClassName="text-text-dimmed"
/>
)}
</div>
<div className="border-t border-charcoal-700 p-1">
<PopoverMenuItem
to={accountPath()}
title="Account"
icon={UserProfilePhoto}
leadingIconClassName={cn(
"text-text-dimmed rounded-full border border-transparent",
user.isImpersonating && "rounded-full border-yellow-500"
)}
leadingIconClassName="text-text-dimmed rounded-full border border-transparent"
/>
{user.isImpersonating && <ImpersonationBanner />}
</div>
<div className="border-t border-charcoal-700 p-1">
<PopoverMenuItem
@@ -519,7 +539,7 @@ function SwitchOrganizations({
<div className="border-t border-charcoal-700 p-1">
<PopoverMenuItem
to={newOrganizationPath()}
title="New Organization"
title="New organization"
icon={PlusIcon}
leadingIconClassName="text-text-dimmed"
/>
+4
View File
@@ -430,3 +430,7 @@ export function docsPath(path: string) {
export function docsTroubleshootingPath(path: string) {
return `${docsRoot()}/v3/troubleshooting`;
}
export function adminPath() {
return `/@`;
}