Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions site/e2e/tests/roles.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -88,7 +84,6 @@ test.describe("roles admin settings access", () => {

await hasAccessToAdminSettings(page, [
"Deployment",
"Organizations",
"Healthcheck",
"Audit Logs",
]);
Expand All @@ -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({
Expand Down
80 changes: 80 additions & 0 deletions site/src/modules/dashboard/Navbar/AdminSettings.tsx
Original file line number Diff line number Diff line change
@@ -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);
25 changes: 6 additions & 19 deletions site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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>
Expand Down
45 changes: 17 additions & 28 deletions site/src/modules/dashboard/Navbar/MobileMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
};
Expand All @@ -67,41 +69,28 @@ 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,
};

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,
};

export const Member: Story = {
args: {
user: MockUserMember,
canViewAuditLog: false,
canViewConnectionLog: false,
canViewDeployment: false,
canViewHealth: false,
canViewOrganizations: false,
canViewAIBridge: false,
canViewAISettings: false,
adminPermissions: {},
},
};

Expand Down
46 changes: 23 additions & 23 deletions site/src/modules/dashboard/Navbar/MobileMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -38,24 +39,24 @@ const itemStyles = {
open: "text-content-primary",
};

type MobileMenuProps = AdminSettingsPermissions & {
type MobileMenuProps = {
proxyContextValue?: ProxyContextValue;
adminPermissions: AdminSettingsPermissions;
user?: TypesGen.User;
supportLinks?: readonly TypesGen.LinkConfig[];
onSignOut: () => void;
isDefaultOpen?: boolean; // Useful for storybook
};

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}>
Expand Down Expand Up @@ -87,10 +88,10 @@ export const MobileMenu: FC<MobileMenuProps> = ({
<DropdownMenuSeparator />
<ProxySettingsSub proxyContextValue={proxyContextValue} />

{hasSomePermission && (
{canViewAdminSettings(adminPermissions) && (
<>
<DropdownMenuSeparator />
<AdminSettingsSub {...permissions} />
<AdminSettingsSub permissions={adminPermissions} />
</>
)}
<DropdownMenuSeparator />
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -191,7 +192,8 @@ const ProxySettingsSub: FC<ProxySettingsSubProps> = ({ proxyContextValue }) => {
</DropdownMenuItem>
<DropdownMenuItem
className={cn(itemStyles.default, itemStyles.sub)}
onClick={() => {
onClick={(event) => {
event.stopPropagation();
proxyContextValue.refetchProxyLatencies();
}}
>
Expand All @@ -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);
Expand All @@ -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>
);
Expand All @@ -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);
Expand Down
Loading
Loading