Org UI Improvements (#8436)

* feat(team_endpoints.py): support returning teams filtered by organization_id

allows user to just get teams they belong to, within the org

Enables org admin to see filtered list of teams on UI

* fix(teams.tsx): simple filter for team on ui - just filter team based on selected org id

* feat(ui/organizations): show 'default org' in switcher, filter teams based on selected org

* feat(user_dashboard.tsx): update team in switcher when org changes

* feat(schema.prisma): add new 'organization_id' value to key table

allow org admin to directly issue keys to a user within their org

* fix(view_key_table.tsx): fix regression where admin couldn't see keys

caused by bad console log statement

* fix(team_endpoints.py): handle default org value in /team/list

* fix(key_management_endpoints.py): allow proxy admin to create keys for team they're not in

* fix(team_endpoints.py): fix team endpoint to handle org id not being passed in

* build(config.yml): investigate what pkg is installing posthog in ci/cd

* ci(config.yml): uninstall posthog

prevent it from being added in ci/cd

* ci: auto-install ci
This commit is contained in:
Krish Dholakia 2025-02-10 19:13:32 -08:00 committed by GitHub
parent e26d7df91b
commit 13a3e8630e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 195 additions and 127 deletions

View file

@ -72,6 +72,7 @@ jobs:
pip install "jsonschema==4.22.0" pip install "jsonschema==4.22.0"
pip install "pytest-xdist==3.6.1" pip install "pytest-xdist==3.6.1"
pip install "websockets==10.4" pip install "websockets==10.4"
pip uninstall posthog -y
- save_cache: - save_cache:
paths: paths:
- ./venv - ./venv

View file

@ -1042,6 +1042,9 @@ class LiteLLM_TeamTable(TeamBase):
"model_aliases", "model_aliases",
] ]
if isinstance(values, BaseModel):
values = values.model_dump()
if ( if (
isinstance(values.get("members_with_roles"), dict) isinstance(values.get("members_with_roles"), dict)
and not values["members_with_roles"] and not values["members_with_roles"]
@ -1489,6 +1492,7 @@ class LiteLLM_OrganizationTable(LiteLLMPydanticObjectBase):
class LiteLLM_OrganizationTableWithMembers(LiteLLM_OrganizationTable): class LiteLLM_OrganizationTableWithMembers(LiteLLM_OrganizationTable):
members: List[LiteLLM_OrganizationMembershipTable] members: List[LiteLLM_OrganizationMembershipTable]
teams: List[LiteLLM_TeamTable]
class NewOrganizationResponse(LiteLLM_OrganizationTable): class NewOrganizationResponse(LiteLLM_OrganizationTable):
@ -2424,3 +2428,7 @@ class PrismaCompatibleUpdateDBModel(TypedDict, total=False):
model_info: str model_info: str
updated_at: str updated_at: str
updated_by: str updated_by: str
class SpecialManagementEndpointEnums(enum.Enum):
DEFAULT_ORGANIZATION = "default_organization"

View file

@ -168,6 +168,8 @@ def _team_key_generation_check(
user_api_key_dict: UserAPIKeyAuth, user_api_key_dict: UserAPIKeyAuth,
data: GenerateKeyRequest, data: GenerateKeyRequest,
): ):
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value:
return True
if ( if (
litellm.key_generation_settings is not None litellm.key_generation_settings is not None
and "team_key_generation" in litellm.key_generation_settings and "team_key_generation" in litellm.key_generation_settings

View file

@ -227,7 +227,7 @@ async def list_organization(
# if proxy admin - get all orgs # if proxy admin - get all orgs
if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN: if user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN:
response = await prisma_client.db.litellm_organizationtable.find_many( response = await prisma_client.db.litellm_organizationtable.find_many(
include={"members": True} include={"members": True, "teams": True}
) )
# if internal user - get orgs they are a member of # if internal user - get orgs they are a member of
else: else:
@ -242,7 +242,7 @@ async def list_organization(
"in": [membership.organization_id for membership in org_memberships] "in": [membership.organization_id for membership in org_memberships]
} }
}, },
include={"members": True}, include={"members": True, "teams": True},
) )
response = org_objects response = org_objects

View file

@ -39,6 +39,7 @@ from litellm.proxy._types import (
NewTeamRequest, NewTeamRequest,
ProxyErrorTypes, ProxyErrorTypes,
ProxyException, ProxyException,
SpecialManagementEndpointEnums,
TeamAddMemberResponse, TeamAddMemberResponse,
TeamInfoResponseObject, TeamInfoResponseObject,
TeamListResponseObject, TeamListResponseObject,
@ -1482,6 +1483,7 @@ async def list_team(
user_id: Optional[str] = fastapi.Query( user_id: Optional[str] = fastapi.Query(
default=None, description="Only return teams which this 'user_id' belongs to" default=None, description="Only return teams which this 'user_id' belongs to"
), ),
organization_id: Optional[str] = None,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
): ):
""" """
@ -1492,6 +1494,7 @@ async def list_team(
Parameters: Parameters:
- user_id: str - Optional. If passed will only return teams that the user_id is a member of. - user_id: str - Optional. If passed will only return teams that the user_id is a member of.
- organization_id: str - Optional. If passed will only return teams that belong to the organization_id. Pass 'default_organization' to get all teams without organization_id.
""" """
from litellm.proxy.proxy_server import prisma_client from litellm.proxy.proxy_server import prisma_client
@ -1565,6 +1568,19 @@ async def list_team(
continue continue
# Sort the responses by team_alias # Sort the responses by team_alias
returned_responses.sort(key=lambda x: (getattr(x, "team_alias", "") or "")) returned_responses.sort(key=lambda x: (getattr(x, "team_alias", "") or ""))
if organization_id is not None:
if organization_id == SpecialManagementEndpointEnums.DEFAULT_ORGANIZATION.value:
returned_responses = [
team for team in returned_responses if team.organization_id is None
]
else:
returned_responses = [
team
for team in returned_responses
if team.organization_id == organization_id
]
return returned_responses return returned_responses

View file

@ -56,6 +56,7 @@ model LiteLLM_OrganizationTable {
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
teams LiteLLM_TeamTable[] teams LiteLLM_TeamTable[]
users LiteLLM_UserTable[] users LiteLLM_UserTable[]
keys LiteLLM_VerificationToken[]
members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership") members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership")
} }
@ -158,9 +159,11 @@ model LiteLLM_VerificationToken {
model_spend Json @default("{}") model_spend Json @default("{}")
model_max_budget Json @default("{}") model_max_budget Json @default("{}")
budget_id String? budget_id String?
organization_id String?
created_at DateTime? @default(now()) @map("created_at") created_at DateTime? @default(now()) @map("created_at")
updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
} }
model LiteLLM_EndUserTable { model LiteLLM_EndUserTable {

View file

@ -56,6 +56,7 @@ model LiteLLM_OrganizationTable {
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
teams LiteLLM_TeamTable[] teams LiteLLM_TeamTable[]
users LiteLLM_UserTable[] users LiteLLM_UserTable[]
keys LiteLLM_VerificationToken[]
members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership") members LiteLLM_OrganizationMembership[] @relation("OrganizationToMembership")
} }
@ -158,9 +159,11 @@ model LiteLLM_VerificationToken {
model_spend Json @default("{}") model_spend Json @default("{}")
model_max_budget Json @default("{}") model_max_budget Json @default("{}")
budget_id String? budget_id String?
organization_id String?
created_at DateTime? @default(now()) @map("created_at") created_at DateTime? @default(now()) @map("created_at")
updated_at DateTime? @default(now()) @updatedAt @map("updated_at") updated_at DateTime? @default(now()) @updatedAt @map("updated_at")
litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id]) litellm_budget_table LiteLLM_BudgetTable? @relation(fields: [budget_id], references: [budget_id])
litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id])
} }
model LiteLLM_EndUserTable { model LiteLLM_EndUserTable {

View file

@ -183,6 +183,7 @@ export default function CreateKeyPage() {
} }
} }
} }
setTeams(null);
} }
return ( return (
@ -201,6 +202,7 @@ export default function CreateKeyPage() {
setTeams={setTeams} setTeams={setTeams}
setKeys={setKeys} setKeys={setKeys}
setOrganizations={setOrganizations} setOrganizations={setOrganizations}
currentOrg={currentOrg}
/> />
) : ( ) : (
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
@ -237,6 +239,7 @@ export default function CreateKeyPage() {
setTeams={setTeams} setTeams={setTeams}
setKeys={setKeys} setKeys={setKeys}
setOrganizations={setOrganizations} setOrganizations={setOrganizations}
currentOrg={currentOrg}
/> />
) : page == "models" ? ( ) : page == "models" ? (
<ModelDashboard <ModelDashboard

View file

@ -0,0 +1,14 @@
import { teamListCall, DEFAULT_ORGANIZATION, Organization } from "../networking";
export const fetchTeams = async (accessToken: string, userID: string | null, userRole: string | null, currentOrg: Organization | null, setTeams: (teams: any[]) => void) => {
let givenTeams;
if (userRole != "Admin" && userRole != "Admin Viewer") {
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || DEFAULT_ORGANIZATION, userID)
} else {
givenTeams = await teamListCall(accessToken, currentOrg?.organization_id || DEFAULT_ORGANIZATION)
}
console.log(`givenTeams: ${givenTeams}`)
setTeams(givenTeams)
}

View file

@ -44,7 +44,6 @@ export const fetchAvailableModelsForTeamOrKey = async (
}; };
export const getModelDisplayName = (model: string) => { export const getModelDisplayName = (model: string) => {
console.log("getModelDisplayName", model);
if (model.endsWith('/*')) { if (model.endsWith('/*')) {
const provider = model.replace('/*', ''); const provider = model.replace('/*', '');
return `All ${provider} models`; return `All ${provider} models`;

View file

@ -59,46 +59,45 @@ const Navbar: React.FC<NavbarProps> = ({
]; ];
const orgMenuItems: MenuProps["items"] = [ const orgMenuItems: MenuProps["items"] = [
...(userRole === "Admin" {
? [{ key: 'header',
key: 'global', label: 'Organizations',
label: ( type: 'group' as const,
<div className="flex items-center justify-between py-1"> style: {
<span className="text-sm">Global View</span> color: '#6B7280',
</div> fontSize: '0.875rem'
), }
onClick: () => onOrgChange({ organization_id: "global", organization_alias: "Global View" } as Organization) },
}] {
: [ key: "default",
{ label: (
key: 'header', <div className="flex items-center justify-between py-1">
label: 'Organizations', <span className="text-sm">Default Organization</span>
type: 'group' as const, </div>
style: { ),
color: '#6B7280', onClick: () => onOrgChange({
fontSize: '0.875rem' organization_id: null,
} organization_alias: "Default Organization"
}, } as Organization)
...organizations.map(org => ({ },
key: org.organization_id, ...organizations.filter(org => org.organization_id !== null).map(org => ({
label: ( key: org.organization_id ?? "default",
<div className="flex items-center justify-between py-1"> label: (
<span className="text-sm">{org.organization_alias}</span> <div className="flex items-center justify-between py-1">
</div> <span className="text-sm">{org.organization_alias}</span>
), </div>
onClick: () => onOrgChange(org) ),
})), onClick: () => onOrgChange(org)
{ })),
key: "note", {
label: ( key: "note",
<div className="flex items-center justify-between py-1 px-2 bg-gray-50 text-gray-500 text-xs italic"> label: (
<span>Switching between organizations on the UI is currently in beta.</span> <div className="flex items-center justify-between py-1 px-2 bg-gray-50 text-gray-500 text-xs italic">
</div> <span>Switching between organizations on the UI is currently in beta.</span>
), </div>
disabled: true ),
} disabled: true
] }
)
]; ];
return ( return (

View file

@ -9,6 +9,8 @@ if (isLocal != true) {
console.log = function() {}; console.log = function() {};
} }
export const DEFAULT_ORGANIZATION = "default_organization";
export interface Model { export interface Model {
model_name: string; model_name: string;
litellm_params: Object; litellm_params: Object;
@ -16,7 +18,7 @@ export interface Model {
} }
export interface Organization { export interface Organization {
organization_id: string; organization_id: string | null;
organization_alias: string; organization_alias: string;
budget_id: string; budget_id: string;
metadata: Record<string, any>; metadata: Record<string, any>;
@ -724,7 +726,8 @@ export const teamInfoCall = async (
export const teamListCall = async ( export const teamListCall = async (
accessToken: String, accessToken: String,
userID: String | null = null organizationID: string | null,
userID: String | null = null,
) => { ) => {
/** /**
* Get all available teams on proxy * Get all available teams on proxy
@ -732,9 +735,21 @@ export const teamListCall = async (
try { try {
let url = proxyBaseUrl ? `${proxyBaseUrl}/team/list` : `/team/list`; let url = proxyBaseUrl ? `${proxyBaseUrl}/team/list` : `/team/list`;
console.log("in teamInfoCall"); console.log("in teamInfoCall");
const queryParams = new URLSearchParams();
if (userID) { if (userID) {
url += `?user_id=${userID}`; queryParams.append('user_id', userID.toString());
} }
if (organizationID) {
queryParams.append('organization_id', organizationID.toString());
}
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;
}
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {

View file

@ -1,8 +1,9 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
import { Typography } from "antd"; import { Typography } from "antd";
import { teamDeleteCall, teamUpdateCall, teamInfoCall, Organization } from "./networking"; import { teamDeleteCall, teamUpdateCall, teamInfoCall, Organization, DEFAULT_ORGANIZATION } from "./networking";
import TeamMemberModal from "@/components/team/edit_membership"; import TeamMemberModal from "@/components/team/edit_membership";
import { fetchTeams } from "./common_components/fetch_teams";
import { import {
InformationCircleIcon, InformationCircleIcon,
PencilAltIcon, PencilAltIcon,
@ -95,23 +96,12 @@ const Team: React.FC<TeamProps> = ({
}) => { }) => {
const [lastRefreshed, setLastRefreshed] = useState(""); const [lastRefreshed, setLastRefreshed] = useState("");
const fetchTeams = async (accessToken: string, userID: string | null, userRole: string | null) => {
let givenTeams;
if (userRole != "Admin" && userRole != "Admin Viewer") {
givenTeams = await teamListCall(accessToken, userID)
} else {
givenTeams = await teamListCall(accessToken)
}
console.log(`givenTeams: ${givenTeams}`)
setTeams(givenTeams)
}
useEffect(() => { useEffect(() => {
console.log(`inside useeffect - ${teams}`) console.log(`inside useeffect - ${teams}`)
if (teams === null && accessToken) { if (teams === null && accessToken) {
// Call your function here // Call your function here
fetchTeams(accessToken, userID, userRole) fetchTeams(accessToken, userID, userRole, currentOrg, setTeams)
} }
}, [teams]); }, [teams]);
@ -119,7 +109,7 @@ const Team: React.FC<TeamProps> = ({
console.log(`inside useeffect - ${lastRefreshed}`) console.log(`inside useeffect - ${lastRefreshed}`)
if (accessToken) { if (accessToken) {
// Call your function here // Call your function here
fetchTeams(accessToken, userID, userRole) fetchTeams(accessToken, userID, userRole, currentOrg, setTeams)
} }
handleRefreshClick() handleRefreshClick()
}, [lastRefreshed]); }, [lastRefreshed]);
@ -257,9 +247,9 @@ const Team: React.FC<TeamProps> = ({
let _team_id_to_info: Record<string, any> = {}; let _team_id_to_info: Record<string, any> = {};
let teamList; let teamList;
if (userRole != "Admin" && userRole != "Admin Viewer") { if (userRole != "Admin" && userRole != "Admin Viewer") {
teamList = await teamListCall(accessToken, userID) teamList = await teamListCall(accessToken, currentOrg?.organization_id || DEFAULT_ORGANIZATION, userID)
} else { } else {
teamList = await teamListCall(accessToken) teamList = await teamListCall(accessToken, currentOrg?.organization_id || DEFAULT_ORGANIZATION)
} }
for (let i = 0; i < teamList.length; i++) { for (let i = 0; i < teamList.length; i++) {
@ -405,6 +395,10 @@ const Team: React.FC<TeamProps> = ({
<TableBody> <TableBody>
{teams && teams.length > 0 {teams && teams.length > 0
? teams ? teams
.filter((team) => {
const targetOrgId = currentOrg ? currentOrg.organization_id : null;
return team.organization_id === targetOrgId;
})
.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
.map((team: any) => ( .map((team: any) => (
<TableRow key={team.team_id}> <TableRow key={team.team_id}>

View file

@ -7,8 +7,10 @@ import {
getProxyUISettings, getProxyUISettings,
teamListCall, teamListCall,
Organization, Organization,
organizationListCall organizationListCall,
DEFAULT_ORGANIZATION
} from "./networking"; } from "./networking";
import { fetchTeams } from "./common_components/fetch_teams";
import { Grid, Col, Card, Text, Title } from "@tremor/react"; import { Grid, Col, Card, Text, Title } from "@tremor/react";
import CreateKey from "./create_key_button"; import CreateKey from "./create_key_button";
import ViewKeyTable from "./view_key_table"; import ViewKeyTable from "./view_key_table";
@ -62,6 +64,7 @@ interface UserDashboardProps {
setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>; setKeys: React.Dispatch<React.SetStateAction<Object[] | null>>;
setOrganizations: React.Dispatch<React.SetStateAction<Organization[]>>; setOrganizations: React.Dispatch<React.SetStateAction<Organization[]>>;
premiumUser: boolean; premiumUser: boolean;
currentOrg: Organization | null;
} }
type TeamInterface = { type TeamInterface = {
@ -82,15 +85,15 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
setKeys, setKeys,
setOrganizations, setOrganizations,
premiumUser, premiumUser,
currentOrg
}) => { }) => {
console.log(`currentOrg in user dashboard: ${JSON.stringify(currentOrg)}`)
const [userSpendData, setUserSpendData] = useState<UserInfo | null>( const [userSpendData, setUserSpendData] = useState<UserInfo | null>(
null null
); );
// Assuming useSearchParams() hook exists and works in your setup // Assuming useSearchParams() hook exists and works in your setup
const searchParams = useSearchParams()!; const searchParams = useSearchParams()!;
const viewSpend = searchParams.get("viewSpend");
const router = useRouter();
const token = getCookie('token'); const token = getCookie('token');
@ -173,22 +176,11 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
} }
} }
if (userID && accessToken && userRole && !keys && !userSpendData) { if (userID && accessToken && userRole && !keys && !userSpendData) {
// const cachedUserModels = sessionStorage.getItem("userModels" + userID); const cachedUserModels = sessionStorage.getItem("userModels" + userID);
// if (cachedUserModels) { if (cachedUserModels) {
// setUserModels(JSON.parse(cachedUserModels)); setUserModels(JSON.parse(cachedUserModels));
// } else { } else {
const fetchTeams = async () => { console.log(`currentOrg: ${JSON.stringify(currentOrg)}`)
let givenTeams;
if (userRole != "Admin" && userRole != "Admin Viewer") {
givenTeams = await teamListCall(accessToken, userID)
} else {
givenTeams = await teamListCall(accessToken)
}
console.log(`givenTeams: ${givenTeams}`)
setTeams(givenTeams)
}
const fetchData = async () => { const fetchData = async () => {
try { try {
const proxy_settings: ProxySettings = await getProxyUISettings(accessToken); const proxy_settings: ProxySettings = await getProxyUISettings(accessToken);
@ -206,11 +198,9 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
setUserSpendData(response["user_info"]); setUserSpendData(response["user_info"]);
console.log(`userSpendData: ${JSON.stringify(userSpendData)}`) console.log(`userSpendData: ${JSON.stringify(userSpendData)}`)
console.log(`response["user_info"]["organization_memberships"]: ${JSON.stringify(response["user_info"]["organization_memberships"])}`)
// set keys for admin and users // set keys for admin and users
if (!response?.teams?.[0]?.keys) { if (!response?.teams[0].keys) {
setKeys(response["keys"]); setKeys(response["keys"]);
} else { } else {
setKeys( setKeys(
@ -270,12 +260,20 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
setOrganizations(organizations); setOrganizations(organizations);
} }
fetchData(); fetchData();
fetchTeams(); fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
fetchOrganizations(); fetchOrganizations();
} }
// } }
}, [userID, token, accessToken, keys, userRole]); }, [userID, token, accessToken, keys, userRole]);
useEffect(() => {
console.log(`currentOrg: ${JSON.stringify(currentOrg)}, accessToken: ${accessToken}, userID: ${userID}, userRole: ${userRole}`)
if (accessToken) {
console.log(`fetching teams`)
fetchTeams(accessToken, userID, userRole, currentOrg, setTeams);
}
}, [currentOrg]);
useEffect(() => { useEffect(() => {
// This code will run every time selectedTeam changes // This code will run every time selectedTeam changes
if ( if (
@ -375,6 +373,7 @@ const UserDashboard: React.FC<UserDashboardProps> = ({
setData={setKeys} setData={setKeys}
premiumUser={premiumUser} premiumUser={premiumUser}
teams={teams} teams={teams}
currentOrg={currentOrg}
/> />
<CreateKey <CreateKey
key={selectedTeam ? selectedTeam.team_id : null} key={selectedTeam ? selectedTeam.team_id : null}

View file

@ -4,6 +4,7 @@ import {
keyDeleteCall, keyDeleteCall,
modelAvailableCall, modelAvailableCall,
getGuardrailsList, getGuardrailsList,
Organization,
} from "./networking"; } from "./networking";
import { add } from "date-fns"; import { add } from "date-fns";
import { import {
@ -101,6 +102,7 @@ interface ViewKeyTableProps {
setData: React.Dispatch<React.SetStateAction<any[] | null>>; setData: React.Dispatch<React.SetStateAction<any[] | null>>;
teams: any[] | null; teams: any[] | null;
premiumUser: boolean; premiumUser: boolean;
currentOrg: Organization | null;
} }
interface ItemData { interface ItemData {
@ -132,6 +134,7 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
setData, setData,
teams, teams,
premiumUser, premiumUser,
currentOrg
}) => { }) => {
const [isButtonClicked, setIsButtonClicked] = useState(false); const [isButtonClicked, setIsButtonClicked] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -167,55 +170,64 @@ const ViewKeyTable: React.FC<ViewKeyTableProps> = ({
// Combine all keys that user should have access to // Combine all keys that user should have access to
const all_keys_to_display = React.useMemo(() => { const all_keys_to_display = React.useMemo(() => {
if (!data) return [];
// Helper function for default team org check
const matchesDefaultTeamOrg = (key: any) => {
console.log(`Checking if key matches default team org: ${JSON.stringify(key)}, currentOrg: ${JSON.stringify(currentOrg)}`)
if (!currentOrg || currentOrg.organization_id === null) {
return !('organization_id' in key) || key.organization_id === null;
}
return key.organization_id === currentOrg.organization_id;
};
let allKeys: any[] = []; let allKeys: any[] = [];
// If no teams, return personal keys // Handle no team selected or Default Team case
if (!teams || teams.length === 0) { if (!selectedTeam || selectedTeam.team_alias === "Default Team") {
return data;
}
teams.forEach((team) => { console.log(`inside personal keys`)
// For default team or when user is not admin, use personal keys (data) // Get personal keys (with org check)
if (team.team_id === "default-team" || !isUserTeamAdmin(team)) { const personalKeys = data.filter(key =>
if (selectedTeam && selectedTeam.team_id === team.team_id && data) { key.team_id == null &&
allKeys = [ matchesDefaultTeamOrg(key)
...allKeys,
...data.filter((key) => key.team_id === team.team_id),
];
}
}
// For teams where user is admin, use team keys
else if (isUserTeamAdmin(team)) {
if (selectedTeam && selectedTeam.team_id === team.team_id) {
allKeys = [
...allKeys,
...(data?.filter((key) => key?.team_id === team?.team_id) || []),
];
}
}
});
// If no team is selected, show all accessible keys
if ((!selectedTeam || selectedTeam.team_alias === "Default Team") && data) {
const personalKeys = data.filter(
(key) => !key.team_id || key.team_id === "default-team"
); );
const adminTeamKeys = teams
.filter((team) => isUserTeamAdmin(team)) console.log(`personalKeys: ${JSON.stringify(personalKeys)}`)
.flatMap((team) => team.keys || []);
// Get admin team keys (no org check)
const adminTeamKeys = data.filter(key => {
const keyTeam = teams?.find(team => team.team_id === key.team_id);
return keyTeam && isUserTeamAdmin(keyTeam) && key.team_id !== "default-team";
});
console.log(`adminTeamKeys: ${JSON.stringify(adminTeamKeys)}`)
allKeys = [...personalKeys, ...adminTeamKeys]; allKeys = [...personalKeys, ...adminTeamKeys];
} }
// Handle specific team selected
else {
const selectedTeamData = teams?.find(t => t.team_id === selectedTeam.team_id);
if (selectedTeamData) {
const teamKeys = data.filter(key => {
if (selectedTeamData.team_id === "default-team") {
return key.team_id == null && matchesDefaultTeamOrg(key);
}
return key.team_id === selectedTeamData.team_id;
});
allKeys = teamKeys;
}
}
// Filter out litellm-dashboard keys // Final filtering and deduplication
allKeys = allKeys.filter((key) => key.team_id !== "litellm-dashboard"); return Array.from(
new Map(
// Remove duplicates based on token allKeys
const uniqueKeys = Array.from( .filter(key => key.team_id !== "litellm-dashboard")
new Map(allKeys.map((key) => [key.token, key])).values() .map(key => [key.token, key])
).values()
); );
}, [data, teams, selectedTeam, currentOrg]);
return uniqueKeys;
}, [data, teams, selectedTeam, userID]);
useEffect(() => { useEffect(() => {
const calculateNewExpiryTime = (duration: string | undefined) => { const calculateNewExpiryTime = (duration: string | undefined) => {