mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: don't show admin settings dropdown to everyone (#27481)
This commit is contained in:
@@ -61,25 +61,21 @@ test.describe("roles admin settings access", () => {
|
||||
await login(page, users.templateAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]);
|
||||
await hasAccessToAdminSettings(page, ["Deployment"]);
|
||||
});
|
||||
|
||||
test("user admin can see admin settings", async ({ page }) => {
|
||||
await login(page, users.userAdmin);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, ["Deployment", "Organizations"]);
|
||||
await hasAccessToAdminSettings(page, ["Deployment"]);
|
||||
});
|
||||
|
||||
test("auditor can see admin settings", async ({ page }) => {
|
||||
await login(page, users.auditor);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await hasAccessToAdminSettings(page, [
|
||||
"Deployment",
|
||||
"Organizations",
|
||||
"Audit Logs",
|
||||
]);
|
||||
await hasAccessToAdminSettings(page, ["Deployment", "Audit Logs"]);
|
||||
});
|
||||
|
||||
test("owner can see admin settings", async ({ page }) => {
|
||||
@@ -88,7 +84,6 @@ test.describe("roles admin settings access", () => {
|
||||
|
||||
await hasAccessToAdminSettings(page, [
|
||||
"Deployment",
|
||||
"Organizations",
|
||||
"Healthcheck",
|
||||
"Audit Logs",
|
||||
]);
|
||||
@@ -103,6 +98,23 @@ test.describe("org-scoped roles admin settings access", () => {
|
||||
await setupApiCalls(page);
|
||||
});
|
||||
|
||||
test("member cannot see admin settings", async ({ page }) => {
|
||||
// The unlicensed member test above cannot catch all regressions here.
|
||||
// Many admin settings are locked behind a license and wouldn't be shown.
|
||||
const org = await createOrganization();
|
||||
const member = await createOrganizationMember({
|
||||
orgRoles: {
|
||||
[org.id]: [],
|
||||
},
|
||||
});
|
||||
|
||||
await login(page, member);
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
// None, "Admin settings" button should not be visible
|
||||
await hasAccessToAdminSettings(page, []);
|
||||
});
|
||||
|
||||
test("org template admin can see admin settings", async ({ page }) => {
|
||||
const org = await createOrganization();
|
||||
const orgTemplateAdmin = await createOrganizationMember({
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Link } from "react-router";
|
||||
import { DropdownMenuItem } from "#/components/DropdownMenu/DropdownMenu";
|
||||
|
||||
/**
|
||||
* Permissions that determine which items appear in the Admin settings menu.
|
||||
* Shared by the desktop `DeploymentDropdown` and the mobile `MobileMenu` so
|
||||
* both surfaces render the same set of items from a single source of truth.
|
||||
*/
|
||||
type AdminSettingsItemsProps = {
|
||||
itemClassName?: string;
|
||||
permissions: AdminSettingsPermissions;
|
||||
};
|
||||
|
||||
export type AdminSettingsPermissions = {
|
||||
canViewDeployment?: boolean;
|
||||
canViewOrganizations?: boolean;
|
||||
canViewAISettings?: boolean;
|
||||
canViewAuditLog?: boolean;
|
||||
canViewConnectionLog?: boolean;
|
||||
canViewAIBridge?: boolean;
|
||||
canViewHealth?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the ordered list of Admin settings menu items for the given
|
||||
* permissions. Organizations is always available; the rest are gated behind
|
||||
* their respective permissions.
|
||||
*/
|
||||
export const AdminSettingsItems: React.FC<AdminSettingsItemsProps> = ({
|
||||
itemClassName,
|
||||
permissions,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{permissions.canViewDeployment && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/deployment">Deployment</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissions.canViewOrganizations && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/organizations">Organizations</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissions.canViewAISettings && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/ai/settings">AI</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissions.canViewAuditLog && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/audit">Audit logs</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissions.canViewConnectionLog && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/connectionlog">Connection logs</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissions.canViewAIBridge && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/ai-gateway/sessions">AI sessions</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissions.canViewHealth && (
|
||||
<DropdownMenuItem asChild className={itemClassName}>
|
||||
<Link to="/health">Healthcheck</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the user has any permission that should surface the Admin settings
|
||||
* menu. Organizations alone does not gate visibility, matching prior behavior.
|
||||
*/
|
||||
export const canViewAdminSettings = (
|
||||
permissions: AdminSettingsPermissions,
|
||||
): boolean => Object.values(permissions).some((canView) => canView);
|
||||
@@ -1,30 +1,21 @@
|
||||
import type { FC } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "#/components/DropdownMenu/DropdownMenu";
|
||||
import {
|
||||
AdminSettingsItems,
|
||||
type AdminSettingsPermissions,
|
||||
canViewAdminSettings,
|
||||
getAdminSettingsItems,
|
||||
} from "./adminSettings";
|
||||
} from "./AdminSettings";
|
||||
|
||||
type DeploymentDropdownProps = AdminSettingsPermissions;
|
||||
type AdminSettingsDropdownProps = { permissions: AdminSettingsPermissions };
|
||||
|
||||
export const DeploymentDropdown: FC<DeploymentDropdownProps> = (
|
||||
export const AdminSettingsDropdown: FC<AdminSettingsDropdownProps> = ({
|
||||
permissions,
|
||||
) => {
|
||||
if (!canViewAdminSettings(permissions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = getAdminSettingsItems(permissions);
|
||||
|
||||
}) => {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -36,11 +27,7 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = (
|
||||
|
||||
<DropdownMenuContent align="end" className="w-[180px] min-w-auto">
|
||||
<nav>
|
||||
{items.map((item) => (
|
||||
<DropdownMenuItem key={item.to} asChild>
|
||||
<Link to={item.to}>{item.label}</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<AdminSettingsItems permissions={permissions} />
|
||||
</nav>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -40,13 +40,15 @@ const meta: Meta<typeof MobileMenu> = {
|
||||
supportLinks: MockSupportLinks,
|
||||
onSignOut: fn(),
|
||||
isDefaultOpen: true,
|
||||
canViewAuditLog: true,
|
||||
canViewConnectionLog: true,
|
||||
canViewDeployment: true,
|
||||
canViewHealth: true,
|
||||
canViewOrganizations: true,
|
||||
canViewAIBridge: true,
|
||||
canViewAISettings: true,
|
||||
adminPermissions: {
|
||||
canViewDeployment: true,
|
||||
canViewOrganizations: true,
|
||||
canViewAISettings: true,
|
||||
canViewAuditLog: true,
|
||||
canViewConnectionLog: true,
|
||||
canViewAIBridge: true,
|
||||
canViewHealth: true,
|
||||
},
|
||||
},
|
||||
decorators: [withNavbarMock],
|
||||
};
|
||||
@@ -67,13 +69,9 @@ export const Admin: Story = {
|
||||
export const Auditor: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: true,
|
||||
canViewConnectionLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewOrganizations: false,
|
||||
canViewAIBridge: false,
|
||||
canViewAISettings: false,
|
||||
adminPermissions: {
|
||||
canViewAuditLog: true,
|
||||
},
|
||||
},
|
||||
play: openAdminSettings,
|
||||
};
|
||||
@@ -81,13 +79,10 @@ export const Auditor: Story = {
|
||||
export const OrgAdmin: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: true,
|
||||
canViewConnectionLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewOrganizations: true,
|
||||
canViewAIBridge: false,
|
||||
canViewAISettings: false,
|
||||
adminPermissions: {
|
||||
canViewAuditLog: true,
|
||||
canViewOrganizations: true,
|
||||
},
|
||||
},
|
||||
play: openAdminSettings,
|
||||
};
|
||||
@@ -95,13 +90,7 @@ export const OrgAdmin: Story = {
|
||||
export const Member: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: false,
|
||||
canViewConnectionLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewOrganizations: false,
|
||||
canViewAIBridge: false,
|
||||
canViewAISettings: false,
|
||||
adminPermissions: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -27,9 +27,10 @@ import { Latency } from "#/components/Latency/Latency";
|
||||
import type { ProxyContextValue } from "#/contexts/ProxyContext";
|
||||
import { cn } from "#/utils/cn";
|
||||
import {
|
||||
AdminSettingsItems,
|
||||
type AdminSettingsPermissions,
|
||||
getAdminSettingsItems,
|
||||
} from "./adminSettings";
|
||||
canViewAdminSettings,
|
||||
} from "./AdminSettings";
|
||||
import { sortProxiesByLatency } from "./proxyUtils";
|
||||
|
||||
const itemStyles = {
|
||||
@@ -38,8 +39,9 @@ const itemStyles = {
|
||||
open: "text-content-primary",
|
||||
};
|
||||
|
||||
type MobileMenuProps = AdminSettingsPermissions & {
|
||||
type MobileMenuProps = {
|
||||
proxyContextValue?: ProxyContextValue;
|
||||
adminPermissions: AdminSettingsPermissions;
|
||||
user?: TypesGen.User;
|
||||
supportLinks?: readonly TypesGen.LinkConfig[];
|
||||
onSignOut: () => void;
|
||||
@@ -47,15 +49,14 @@ type MobileMenuProps = AdminSettingsPermissions & {
|
||||
};
|
||||
|
||||
export const MobileMenu: FC<MobileMenuProps> = ({
|
||||
isDefaultOpen,
|
||||
adminPermissions,
|
||||
proxyContextValue,
|
||||
user,
|
||||
supportLinks,
|
||||
onSignOut,
|
||||
...permissions
|
||||
isDefaultOpen,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(isDefaultOpen);
|
||||
const hasSomePermission = Object.values(permissions).some((p) => p);
|
||||
|
||||
return (
|
||||
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||
@@ -87,10 +88,10 @@ export const MobileMenu: FC<MobileMenuProps> = ({
|
||||
<DropdownMenuSeparator />
|
||||
<ProxySettingsSub proxyContextValue={proxyContextValue} />
|
||||
|
||||
{hasSomePermission && (
|
||||
{canViewAdminSettings(adminPermissions) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<AdminSettingsSub {...permissions} />
|
||||
<AdminSettingsSub permissions={adminPermissions} />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
@@ -123,7 +124,7 @@ const ProxySettingsSub: FC<ProxySettingsSubProps> = ({ proxyContextValue }) => {
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className={cn(itemStyles.default, open ? itemStyles.open : "")}
|
||||
className={cn(itemStyles.default, open && itemStyles.open)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => !prev);
|
||||
@@ -191,7 +192,8 @@ const ProxySettingsSub: FC<ProxySettingsSubProps> = ({ proxyContextValue }) => {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn(itemStyles.default, itemStyles.sub)}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
proxyContextValue.refetchProxyLatencies();
|
||||
}}
|
||||
>
|
||||
@@ -202,15 +204,18 @@ const ProxySettingsSub: FC<ProxySettingsSubProps> = ({ proxyContextValue }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const AdminSettingsSub: FC<AdminSettingsPermissions> = (permissions) => {
|
||||
type AdminSettingsSubProps = {
|
||||
permissions: AdminSettingsPermissions;
|
||||
};
|
||||
|
||||
const AdminSettingsSub: FC<AdminSettingsSubProps> = ({ permissions }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const items = getAdminSettingsItems(permissions);
|
||||
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className={cn(itemStyles.default, open ? itemStyles.open : "")}
|
||||
className={cn(itemStyles.default, open && itemStyles.open)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => !prev);
|
||||
@@ -223,15 +228,10 @@ const AdminSettingsSub: FC<AdminSettingsPermissions> = (permissions) => {
|
||||
</DropdownMenuItem>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{items.map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={item.to}
|
||||
asChild
|
||||
className={cn(itemStyles.default, itemStyles.sub)}
|
||||
>
|
||||
<Link to={item.to}>{item.label}</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<AdminSettingsItems
|
||||
itemClassName={cn(itemStyles.default, itemStyles.sub)}
|
||||
permissions={permissions}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
@@ -254,7 +254,7 @@ const UserSettingsSub: FC<UserSettingsSubProps> = ({
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<DropdownMenuItem
|
||||
className={cn(itemStyles.default, open ? itemStyles.open : "")}
|
||||
className={cn(itemStyles.default, open && itemStyles.open)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => !prev);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { buildInfo } from "#/api/queries/buildInfo";
|
||||
import type { LinkConfig } from "#/api/typesGenerated";
|
||||
@@ -10,7 +9,7 @@ import { canViewDeploymentSettings } from "#/modules/permissions";
|
||||
import { useFeatureVisibility } from "../useFeatureVisibility";
|
||||
import { NavbarView } from "./NavbarView";
|
||||
|
||||
export const Navbar: FC = () => {
|
||||
export const Navbar: React.FC = () => {
|
||||
const { metadata } = useEmbeddedMetadata();
|
||||
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
|
||||
const { appearance, canViewOrganizationSettings } = useDashboard();
|
||||
@@ -45,13 +44,15 @@ export const Navbar: FC = () => {
|
||||
buildInfo={buildInfoQuery.data}
|
||||
supportLinks={Array.from(uniqueLinks.values())}
|
||||
onSignOut={signOut}
|
||||
canViewDeployment={canViewDeployment}
|
||||
canViewOrganizations={canViewOrganizations}
|
||||
canViewHealth={canViewHealth}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewConnectionLog={canViewConnectionLog}
|
||||
canViewAIBridge={canViewAIBridge}
|
||||
canViewAISettings={canViewAISettings}
|
||||
adminPermissions={{
|
||||
canViewDeployment,
|
||||
canViewOrganizations,
|
||||
canViewAISettings,
|
||||
canViewAuditLog,
|
||||
canViewConnectionLog,
|
||||
canViewAIBridge,
|
||||
canViewHealth,
|
||||
}}
|
||||
canCreateChat={canCreateChat}
|
||||
proxyContextValue={proxyContextValue}
|
||||
/>
|
||||
|
||||
@@ -30,11 +30,15 @@ const meta: Meta<typeof NavbarView> = {
|
||||
component: NavbarView,
|
||||
args: {
|
||||
user: MockUserOwner,
|
||||
canViewAuditLog: true,
|
||||
canViewDeployment: true,
|
||||
canViewHealth: true,
|
||||
canViewAISettings: true,
|
||||
canViewOrganizations: true,
|
||||
adminPermissions: {
|
||||
canViewDeployment: true,
|
||||
canViewOrganizations: true,
|
||||
canViewAISettings: true,
|
||||
canViewAuditLog: true,
|
||||
canViewConnectionLog: true,
|
||||
canViewAIBridge: true,
|
||||
canViewHealth: true,
|
||||
},
|
||||
canCreateChat: true,
|
||||
supportLinks: [],
|
||||
},
|
||||
@@ -56,11 +60,9 @@ export const ForAdmin: Story = {
|
||||
export const ForAuditor: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: true,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewAISettings: false,
|
||||
canViewOrganizations: false,
|
||||
adminPermissions: {
|
||||
canViewAuditLog: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -73,11 +75,10 @@ export const ForAuditor: Story = {
|
||||
export const ForOrgAdmin: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: true,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewAISettings: false,
|
||||
canViewOrganizations: true,
|
||||
adminPermissions: {
|
||||
canViewAuditLog: true,
|
||||
canViewOrganizations: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -89,11 +90,9 @@ export const ForOrgAdmin: Story = {
|
||||
|
||||
export const ForSingleOrgOSSAdmin: Story = {
|
||||
args: {
|
||||
canViewAuditLog: false,
|
||||
canViewOrganizations: false,
|
||||
canViewConnectionLog: false,
|
||||
canViewAIBridge: false,
|
||||
canViewAISettings: false,
|
||||
adminPermissions: {
|
||||
canViewDeployment: true,
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
@@ -106,11 +105,7 @@ export const ForSingleOrgOSSAdmin: Story = {
|
||||
export const ForMember: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewAISettings: false,
|
||||
canViewOrganizations: false,
|
||||
adminPermissions: {},
|
||||
canCreateChat: false,
|
||||
},
|
||||
};
|
||||
@@ -118,11 +113,7 @@ export const ForMember: Story = {
|
||||
export const ForMemberWithAgentsAccess: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewAISettings: false,
|
||||
canViewOrganizations: false,
|
||||
adminPermissions: {},
|
||||
canCreateChat: true,
|
||||
},
|
||||
};
|
||||
@@ -141,11 +132,7 @@ export const IdleTasks: Story = {
|
||||
export const SupportLinks: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewAISettings: false,
|
||||
canViewOrganizations: false,
|
||||
adminPermissions: {},
|
||||
supportLinks: [
|
||||
{
|
||||
name: "This is a bug",
|
||||
@@ -182,11 +169,7 @@ export const SupportLinks: Story = {
|
||||
export const DefaultSupportLinks: Story = {
|
||||
args: {
|
||||
user: MockUserMember,
|
||||
canViewAuditLog: false,
|
||||
canViewDeployment: false,
|
||||
canViewHealth: false,
|
||||
canViewAISettings: false,
|
||||
canViewOrganizations: false,
|
||||
adminPermissions: {},
|
||||
supportLinks: [
|
||||
{ icon: "docs", name: "Documentation", target: "" },
|
||||
{ icon: "bug", name: "Report a bug", target: "" },
|
||||
|
||||
@@ -16,7 +16,11 @@ import { useEmbeddedMetadata } from "#/hooks/useEmbeddedMetadata";
|
||||
import { NotificationsInbox } from "#/modules/notifications/NotificationsInbox/NotificationsInbox";
|
||||
import { getPrereleaseFlag } from "#/utils/buildInfo";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { DeploymentDropdown } from "./DeploymentDropdown";
|
||||
import {
|
||||
type AdminSettingsPermissions,
|
||||
canViewAdminSettings,
|
||||
} from "./AdminSettings";
|
||||
import { AdminSettingsDropdown } from "./DeploymentDropdown";
|
||||
import { MobileMenu } from "./MobileMenu";
|
||||
import { ProxyMenu } from "./ProxyMenu";
|
||||
import { SupportIcon } from "./SupportIcon";
|
||||
@@ -27,13 +31,7 @@ interface NavbarViewProps {
|
||||
buildInfo?: TypesGen.BuildInfoResponse;
|
||||
supportLinks: readonly TypesGen.LinkConfig[];
|
||||
onSignOut: () => void;
|
||||
canViewDeployment: boolean;
|
||||
canViewOrganizations: boolean;
|
||||
canViewAuditLog: boolean;
|
||||
canViewConnectionLog: boolean;
|
||||
canViewHealth: boolean;
|
||||
canViewAIBridge: boolean;
|
||||
canViewAISettings: boolean;
|
||||
adminPermissions: AdminSettingsPermissions;
|
||||
canCreateChat: boolean;
|
||||
proxyContextValue?: ProxyContextValue;
|
||||
}
|
||||
@@ -49,13 +47,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
buildInfo,
|
||||
supportLinks,
|
||||
onSignOut,
|
||||
canViewDeployment,
|
||||
canViewOrganizations,
|
||||
canViewHealth,
|
||||
canViewAuditLog,
|
||||
canViewConnectionLog,
|
||||
canViewAIBridge,
|
||||
canViewAISettings,
|
||||
adminPermissions,
|
||||
canCreateChat,
|
||||
proxyContextValue,
|
||||
}) => {
|
||||
@@ -125,17 +117,11 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="hidden md:block">
|
||||
<DeploymentDropdown
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewOrganizations={canViewOrganizations}
|
||||
canViewDeployment={canViewDeployment}
|
||||
canViewConnectionLog={canViewConnectionLog}
|
||||
canViewAIBridge={canViewAIBridge}
|
||||
canViewAISettings={canViewAISettings}
|
||||
canViewHealth={canViewHealth}
|
||||
/>
|
||||
</div>
|
||||
{canViewAdminSettings(adminPermissions) && (
|
||||
<div className="hidden md:block">
|
||||
<AdminSettingsDropdown permissions={adminPermissions} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NotificationsInbox
|
||||
fetchNotifications={API.getInboxNotifications}
|
||||
@@ -159,16 +145,10 @@ export const NavbarView: FC<NavbarViewProps> = ({
|
||||
<div className="md:hidden">
|
||||
<MobileMenu
|
||||
proxyContextValue={proxyContextValue}
|
||||
adminPermissions={adminPermissions}
|
||||
user={user}
|
||||
supportLinks={supportLinks}
|
||||
onSignOut={onSignOut}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewConnectionLog={canViewConnectionLog}
|
||||
canViewOrganizations={canViewOrganizations}
|
||||
canViewDeployment={canViewDeployment}
|
||||
canViewAIBridge={canViewAIBridge}
|
||||
canViewAISettings={canViewAISettings}
|
||||
canViewHealth={canViewHealth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -207,7 +187,16 @@ const NavItems: FC<NavItemsProps> = ({ className, user, canCreateChat }) => {
|
||||
Templates
|
||||
</NavLink>
|
||||
<TasksNavItem user={user} />
|
||||
<AgentsNavItem canCreateChat={canCreateChat} />
|
||||
{canCreateChat && (
|
||||
<NavLink
|
||||
className={({ isActive }) => {
|
||||
return cn(linkStyles.default, { [linkStyles.active]: isActive });
|
||||
}}
|
||||
to="/agents"
|
||||
>
|
||||
Agents
|
||||
</NavLink>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -272,23 +261,6 @@ function idleTasksLabel(count: number) {
|
||||
return `You have ${count} ${count === 1 ? "task" : "tasks"} waiting for input`;
|
||||
}
|
||||
|
||||
const AgentsNavItem: FC<{ canCreateChat: boolean }> = ({ canCreateChat }) => {
|
||||
if (!canCreateChat) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
className={({ isActive }) => {
|
||||
return cn(linkStyles.default, { [linkStyles.active]: isActive });
|
||||
}}
|
||||
to="/agents"
|
||||
>
|
||||
Agents
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
function isNavbarLink(link: TypesGen.LinkConfig): boolean {
|
||||
return link.location === "navbar";
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { linkToAuditing } from "#/modules/navigation";
|
||||
|
||||
/**
|
||||
* Permissions that determine which items appear in the Admin settings menu.
|
||||
* Shared by the desktop `DeploymentDropdown` and the mobile `MobileMenu` so
|
||||
* both surfaces render the same set of items from a single source of truth.
|
||||
*/
|
||||
export type AdminSettingsPermissions = {
|
||||
canViewDeployment: boolean;
|
||||
canViewOrganizations: boolean;
|
||||
canViewAuditLog: boolean;
|
||||
canViewConnectionLog: boolean;
|
||||
canViewAIBridge: boolean;
|
||||
canViewAISettings: boolean;
|
||||
canViewHealth: boolean;
|
||||
};
|
||||
|
||||
type AdminSettingsItem = {
|
||||
label: string;
|
||||
to: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds the ordered list of Admin settings menu items for the given
|
||||
* permissions. Organizations is always available; the rest are gated behind
|
||||
* their respective permissions.
|
||||
*/
|
||||
export const getAdminSettingsItems = ({
|
||||
canViewDeployment,
|
||||
canViewAuditLog,
|
||||
canViewConnectionLog,
|
||||
canViewAIBridge,
|
||||
canViewAISettings,
|
||||
canViewHealth,
|
||||
}: AdminSettingsPermissions): AdminSettingsItem[] => [
|
||||
...(canViewDeployment ? [{ label: "Deployment", to: "/deployment" }] : []),
|
||||
{ label: "Organizations", to: "/organizations" },
|
||||
...(canViewAISettings ? [{ label: "AI", to: "/ai/settings" }] : []),
|
||||
...(canViewAuditLog ? [{ label: "Audit logs", to: linkToAuditing }] : []),
|
||||
...(canViewConnectionLog
|
||||
? [{ label: "Connection logs", to: "/connectionlog" }]
|
||||
: []),
|
||||
...(canViewAIBridge
|
||||
? [{ label: "AI sessions", to: "/ai-gateway/sessions" }]
|
||||
: []),
|
||||
...(canViewHealth ? [{ label: "Healthcheck", to: "/health" }] : []),
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether the user has any permission that should surface the Admin settings
|
||||
* menu. Organizations alone does not gate visibility, matching prior behavior.
|
||||
*/
|
||||
export const canViewAdminSettings = (
|
||||
permissions: AdminSettingsPermissions,
|
||||
): boolean => Object.values(permissions).some((canView) => canView);
|
||||
@@ -17,14 +17,6 @@ export function useLinks() {
|
||||
return get;
|
||||
}
|
||||
|
||||
function withFilter(path: string, filter: string) {
|
||||
return path + (filter ? `?filter=${encodeURIComponent(filter)}` : "");
|
||||
}
|
||||
|
||||
export const linkToAuditing = "/audit";
|
||||
|
||||
const _linkToUsers = withFilter("/deployment/users", "status:active");
|
||||
|
||||
export const linkToTemplate =
|
||||
(organizationName: string, templateName: string): LinkThunk =>
|
||||
(dashboard) =>
|
||||
|
||||
@@ -40,8 +40,7 @@ export const canViewAnyOrganization = (
|
||||
): permissions is Permissions => {
|
||||
return (
|
||||
permissions !== undefined &&
|
||||
(permissions.viewAnyMembers ||
|
||||
permissions.editAnyGroups ||
|
||||
(permissions.editAnyGroups ||
|
||||
permissions.assignAnyRoles ||
|
||||
permissions.viewAnyIdpSyncSettings ||
|
||||
permissions.editAnySettings)
|
||||
|
||||
Reference in New Issue
Block a user