init ui for internal user controsl

This commit is contained in:
Ishaan Jaffer 2026-01-27 17:01:16 -08:00
parent 5baca5592d
commit fed0f3748e
5 changed files with 226 additions and 26 deletions

View file

@ -1,4 +1,9 @@
"use client";
import Sidebar from "@/components/leftnav";
import { getUISettings } from "@/components/networking";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useEffect, useState } from "react";
interface SidebarProviderProps {
setPage: (page: string) => void;
@ -7,7 +12,34 @@ interface SidebarProviderProps {
}
const SidebarProvider = ({ setPage, defaultSelectedKey, sidebarCollapsed }: SidebarProviderProps) => {
return <Sidebar setPage={setPage} defaultSelectedKey={defaultSelectedKey} collapsed={sidebarCollapsed} />;
const { accessToken } = useAuthorized();
const [enabledPagesInternalUsers, setEnabledPagesInternalUsers] = useState<string[] | null>(null);
useEffect(() => {
const fetchUISettings = async () => {
if (!accessToken) return;
try {
const settings = await getUISettings(accessToken);
if (settings?.settings?.enabled_ui_pages_internal_users !== undefined) {
setEnabledPagesInternalUsers(settings.settings.enabled_ui_pages_internal_users);
}
} catch (error) {
console.error("Failed to fetch UI settings:", error);
}
};
fetchUISettings();
}, [accessToken]);
return (
<Sidebar
setPage={setPage}
defaultSelectedKey={defaultSelectedKey}
collapsed={sidebarCollapsed}
enabledPagesInternalUsers={enabledPagesInternalUsers}
/>
);
};
export default SidebarProvider;

View file

@ -39,6 +39,7 @@ interface SidebarProps {
setPage: (page: string) => void;
defaultSelectedKey: string;
collapsed?: boolean;
enabledPagesInternalUsers?: string[] | null;
}
// Menu item configuration
@ -59,28 +60,8 @@ interface MenuGroup {
roles?: string[];
}
const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapsed = false }) => {
const { userId, accessToken, userRole } = useAuthorized();
const { data: organizations } = useOrganizations();
// Check if user is an org_admin
const isOrgAdmin = useMemo(() => {
if (!userId || !organizations) return false;
return organizations.some((org: Organization) =>
org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin"),
);
}, [userId, organizations]);
// Navigate to page helper
const navigateToPage = (page: string) => {
const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.set("page", page);
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
setPage(page);
};
// Menu groups organized by category
const menuGroups: MenuGroup[] = [
// Menu groups organized by category - defined outside component for export
const menuGroups: MenuGroup[] = [
{
groupLabel: "AI GATEWAY",
items: [
@ -337,15 +318,53 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
},
];
// Filter items based on user role
const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapsed = false, enabledPagesInternalUsers }) => {
const { userId, accessToken, userRole } = useAuthorized();
const { data: organizations } = useOrganizations();
// Check if user is an org_admin
const isOrgAdmin = useMemo(() => {
if (!userId || !organizations) return false;
return organizations.some((org: Organization) =>
org.members?.some((member) => member.user_id === userId && member.user_role === "org_admin"),
);
}, [userId, organizations]);
// Navigate to page helper
const navigateToPage = (page: string) => {
const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.set("page", page);
window.history.pushState(null, "", `?${newSearchParams.toString()}`);
setPage(page);
};
// Filter items based on user role and enabled pages for internal users
const filterItemsByRole = (items: MenuItem[]): MenuItem[] => {
const isAdmin = isAdminRole(userRole);
return items
.filter((item) => {
// Special handling for organizations menu item - allow org_admins
if (item.key === "organizations") {
return !item.roles || item.roles.includes(userRole) || isOrgAdmin;
const hasRoleAccess = !item.roles || item.roles.includes(userRole) || isOrgAdmin;
if (!hasRoleAccess) return false;
// Check enabled pages for internal users (non-admins)
if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) {
return enabledPagesInternalUsers.includes(item.page);
}
return true;
}
return !item.roles || item.roles.includes(userRole);
// Existing role check
if (item.roles && !item.roles.includes(userRole)) return false;
// Check enabled pages for internal users (non-admins)
if (!isAdmin && enabledPagesInternalUsers !== null && enabledPagesInternalUsers !== undefined) {
return enabledPagesInternalUsers.includes(item.page);
}
return true;
})
.map((item) => ({
...item,
@ -485,3 +504,6 @@ const Sidebar: React.FC<SidebarProps> = ({ setPage, defaultSelectedKey, collapse
};
export default Sidebar;
// Also export menuGroups for advanced use cases
export { menuGroups };

View file

@ -5330,6 +5330,66 @@ export const getProxyUISettings = async (accessToken: string) => {
}
};
export const getUISettings = async (accessToken: string) => {
/**
* Get UI-specific configuration flags from the database
*/
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/get/ui_settings` : `/get/ui_settings`;
const response = await fetch(url, {
method: "GET",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
console.error("Failed to get UI settings:", errorMessage);
return null;
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to get UI settings:", error);
return null;
}
};
export const updateUISettings = async (accessToken: string, settings: any) => {
/**
* Update UI-specific configuration flags in the database
* Only proxy admins can update these settings
*/
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/update/ui_settings` : `/update/ui_settings`;
const response = await fetch(url, {
method: "PATCH",
headers: {
[globalLitellmHeaderName]: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(settings),
});
if (!response.ok) {
const errorData = await response.json();
const errorMessage = deriveErrorMessage(errorData);
handleError(errorMessage);
throw new Error(errorMessage);
}
const data = await response.json();
return data;
} catch (error) {
console.error("Failed to update UI settings:", error);
throw error;
}
};
export const getGuardrailsList = async (accessToken: string) => {
try {
const url = proxyBaseUrl ? `${proxyBaseUrl}/v2/guardrails/list` : `/v2/guardrails/list`;

View file

@ -0,0 +1,41 @@
/**
* Page metadata for UI Settings configuration
* This file contains descriptions and metadata for all navigation pages
*/
// Page descriptions for UI Settings configuration
export const pageDescriptions: Record<string, string> = {
"api-keys": "Manage virtual keys for API access and authentication",
playground: "Interactive playground for testing LLM requests",
"models-and-endpoints": "Configure and manage LLM models and endpoints",
agents: "Create and manage AI agents",
"mcp-servers": "Configure Model Context Protocol servers",
guardrails: "Set up content moderation and safety guardrails",
policies: "Define access control and usage policies",
"search-tools": "Configure RAG search and retrieval tools",
"vector-stores": "Manage vector databases for embeddings",
usage: "View usage analytics and metrics",
logs: "Access request and response logs",
"internal-users": "Manage internal user accounts and permissions",
teams: "Create and manage teams for access control",
organizations: "Manage organizations and their members",
budgets: "Set and monitor spending budgets",
"api-reference": "Browse API documentation and endpoints",
"ai-hub": "Explore available AI models and providers",
"learning-resources": "Access tutorials and documentation",
caching: "Configure response caching settings",
"transform-request": "Set up request transformation rules",
"pass-through-endpoints": "Configure pass-through API endpoints",
"cost-tracking": "Track and analyze API costs",
"ui-themes": "Customize dashboard appearance",
"tag-management": "Organize resources with tags",
prompts: "Manage and version prompt templates",
"claude-code-plugins": "Configure Claude Code plugins",
};
export interface PageMetadata {
page: string;
label: string;
group: string;
description: string;
}

View file

@ -0,0 +1,45 @@
/**
* Utility functions for working with navigation pages
*/
import { menuGroups } from "./leftnav";
import { pageDescriptions, PageMetadata } from "./page_metadata";
/**
* Get all available pages from the navigation menu configuration
* Used by UI Settings to display available pages for visibility control
*/
export const getAvailablePages = (): PageMetadata[] => {
const pages: PageMetadata[] = [];
menuGroups.forEach((group) => {
group.items.forEach((item) => {
// Add top-level items (skip parent containers like 'tools', 'experimental', 'settings')
if (item.page && item.page !== "tools" && item.page !== "experimental" && item.page !== "settings") {
const label = typeof item.label === "string" ? item.label : item.key;
pages.push({
page: item.page,
label: label,
group: group.groupLabel,
description: pageDescriptions[item.page] || "No description available",
});
}
// Add children items
if (item.children) {
const parentLabel = typeof item.label === "string" ? item.label : item.key;
item.children.forEach((child) => {
const childLabel = typeof child.label === "string" ? child.label : child.key;
pages.push({
page: child.page,
label: childLabel,
group: `${group.groupLabel} > ${parentLabel}`,
description: pageDescriptions[child.page] || "No description available",
});
});
}
});
});
return pages;
};