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:
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -430,3 +430,7 @@ export function docsPath(path: string) {
|
||||
export function docsTroubleshootingPath(path: string) {
|
||||
return `${docsRoot()}/v3/troubleshooting`;
|
||||
}
|
||||
|
||||
export function adminPath() {
|
||||
return `/@`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user