Make model select required for team, add checks for all-proxy-models

This commit is contained in:
yuneng-jiang 2025-11-25 16:57:38 -08:00
parent 5cb5c2a7b7
commit 5ec3f19a53
4 changed files with 225 additions and 70 deletions

View file

@ -1,5 +1,6 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key";
import { teamCreateCall } from "./networking";
import OldTeams from "./OldTeams";
@ -23,6 +24,28 @@ vi.mock("./molecules/notifications_manager", () => ({
},
}));
vi.mock("./key_team_helpers/fetch_available_models_team_key", () => ({
fetchAvailableModelsForTeamOrKey: vi.fn(),
getModelDisplayName: vi.fn((model: string) => model),
unfurlWildcardModelsInList: vi.fn((teamModels: string[], allModels: string[]) => {
const wildcardDisplayNames: string[] = [];
const expandedModels: string[] = [];
teamModels.forEach((teamModel) => {
if (teamModel.endsWith("/*")) {
const provider = teamModel.replace("/*", "");
const matchingModels = allModels.filter((model) => model.startsWith(provider + "/"));
expandedModels.push(...matchingModels);
wildcardDisplayNames.push(teamModel);
} else {
expandedModels.push(teamModel);
}
});
return [...wildcardDisplayNames, ...expandedModels].filter((item, index, array) => array.indexOf(item) === index);
}),
}));
describe("OldTeams - handleCreate organization handling", () => {
beforeEach(() => {
vi.clearAllMocks();
@ -236,7 +259,7 @@ describe("OldTeams - handleCreate organization handling", () => {
});
it("should clear the delete modal when the cancel button is clicked", async () => {
const { getByRole, getByTestId } = render(
render(
<OldTeams
teams={[
{
@ -261,7 +284,7 @@ describe("OldTeams - handleCreate organization handling", () => {
organizations={[]}
/>,
);
const deleteTeamButton = getByTestId("delete-team-button");
const deleteTeamButton = screen.getByTestId("delete-team-button");
act(() => {
fireEvent.click(deleteTeamButton);
});
@ -275,7 +298,7 @@ describe("OldTeams - empty state", () => {
});
it("should display empty state message when teams array is empty", () => {
const { getByText } = render(
render(
<OldTeams
teams={[]}
searchParams={{}}
@ -287,12 +310,12 @@ describe("OldTeams - empty state", () => {
/>,
);
expect(getByText("No teams found")).toBeInTheDocument();
expect(getByText("Adjust your filters or create a new team")).toBeInTheDocument();
expect(screen.getByText("No teams found")).toBeInTheDocument();
expect(screen.getByText("Adjust your filters or create a new team")).toBeInTheDocument();
});
it("should display empty state message when teams is null", () => {
const { getByText } = render(
render(
<OldTeams
teams={null}
searchParams={{}}
@ -304,12 +327,12 @@ describe("OldTeams - empty state", () => {
/>,
);
expect(getByText("No teams found")).toBeInTheDocument();
expect(getByText("Adjust your filters or create a new team")).toBeInTheDocument();
expect(screen.getByText("No teams found")).toBeInTheDocument();
expect(screen.getByText("Adjust your filters or create a new team")).toBeInTheDocument();
});
it("should not display empty state when teams array has items", () => {
const { queryByText, getByText } = render(
render(
<OldTeams
teams={[
{
@ -335,9 +358,9 @@ describe("OldTeams - empty state", () => {
/>,
);
expect(queryByText("No teams found")).not.toBeInTheDocument();
expect(queryByText("Adjust your filters or create a new team")).not.toBeInTheDocument();
expect(getByText("Test Team")).toBeInTheDocument();
expect(screen.queryByText("No teams found")).not.toBeInTheDocument();
expect(screen.queryByText("Adjust your filters or create a new team")).not.toBeInTheDocument();
expect(screen.getByText("Test Team")).toBeInTheDocument();
});
});
@ -473,7 +496,7 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
});
it("should show Default Team Settings tab for Admin role", () => {
const { getByRole } = render(
render(
<OldTeams
teams={[
{
@ -499,11 +522,11 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
/>,
);
expect(getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
});
it("should show Default Team Settings tab for proxy_admin role", () => {
const { getByRole } = render(
render(
<OldTeams
teams={[
{
@ -529,11 +552,11 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
/>,
);
expect(getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
});
it("should not show Default Team Settings tab for proxy_admin_viewer role", () => {
const { queryByRole } = render(
render(
<OldTeams
teams={[
{
@ -559,11 +582,11 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
/>,
);
expect(queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
});
it("should not show Default Team Settings tab for Admin Viewer role", () => {
const { queryByRole } = render(
render(
<OldTeams
teams={[
{
@ -589,6 +612,44 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
/>,
);
expect(queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
});
});
describe("OldTeams - all-proxy-models dropdown visibility", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
});
it("should not show all-proxy-models option when user has no access to it", async () => {
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
render(
<OldTeams
teams={[]}
searchParams={{}}
accessToken="test-token"
setTeams={vi.fn()}
userID="user-123"
userRole="Admin"
organizations={[]}
/>,
);
await waitFor(() => {
expect(fetchAvailableModelsForTeamOrKey).toHaveBeenCalled();
});
const createButton = screen.getByRole("button", { name: /create new team/i });
act(() => {
fireEvent.click(createButton);
});
await waitFor(() => {
expect(screen.getByLabelText(/models/i)).toBeInTheDocument();
});
const allProxyModelsOption = screen.queryByText("All Proxy Models");
expect(allProxyModelsOption).not.toBeInTheDocument();
});
});

View file

@ -1139,12 +1139,20 @@ const Teams: React.FC<TeamProps> = ({
</Tooltip>
</span>
}
rules={[
{
required: true,
message: "Please select at least one model",
},
]}
name="models"
>
<Select2 mode="multiple" placeholder="Select models" style={{ width: "100%" }}>
<Select2.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select2.Option>
{(isProxyAdminRole(userRole || "") || userModels.includes("all-proxy-models")) && (
<Select2.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select2.Option>
)}
<Select2.Option key="no-default-models" value="no-default-models">
No Default Models
</Select2.Option>

View file

@ -1,7 +1,7 @@
import * as networking from "@/components/networking";
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import TeamInfoView from "./team_info";
import { render, waitFor } from "@testing-library/react";
import * as networking from "@/components/networking";
// Mock the networking module
vi.mock("@/components/networking", () => ({
@ -61,7 +61,7 @@ describe("TeamInfoView", () => {
vi.mocked(networking.getGuardrailsList).mockResolvedValue([]);
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
const { getByText } = render(
render(
<TeamInfoView
teamId="123"
onUpdate={() => {}}
@ -75,7 +75,87 @@ describe("TeamInfoView", () => {
/>,
);
await waitFor(() => {
expect(getByText("User ID")).toBeInTheDocument();
expect(screen.queryByText("User ID")).not.toBeNull();
});
});
it("should not show all-proxy-models option when user has no access to it", async () => {
vi.mocked(networking.teamInfoCall).mockResolvedValue({
team_id: "123",
team_info: {
team_alias: "Test Team",
team_id: "123",
organization_id: null,
admins: ["admin@test.com"],
members: ["user1@test.com", "user2@test.com"],
members_with_roles: [
{
user_id: "user1@test.com",
user_email: "user1@test.com",
role: "member",
spend: 0,
budget_id: "budget1",
},
],
metadata: {},
tpm_limit: null,
rpm_limit: null,
max_budget: null,
budget_duration: null,
models: ["gpt-4"],
blocked: false,
spend: 0,
max_parallel_requests: null,
budget_reset_at: null,
model_id: null,
litellm_model_table: null,
created_at: "2024-01-01T00:00:00Z",
team_member_budget_table: null,
},
keys: [],
team_memberships: [],
});
vi.mocked(networking.getGuardrailsList).mockResolvedValue([]);
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
render(
<TeamInfoView
teamId="123"
onUpdate={() => {}}
onClose={() => {}}
accessToken="123"
is_team_admin={true}
is_proxy_admin={true}
userModels={["gpt-4", "gpt-3.5-turbo"]}
editTeam={false}
premiumUser={false}
/>,
);
await waitFor(() => {
expect(screen.getAllByText("Test Team")).not.toBeNull();
});
const settingsTab = screen.getByRole("tab", { name: "Settings" });
act(() => {
fireEvent.click(settingsTab);
});
await waitFor(() => {
expect(screen.getByText("Team Settings")).toBeInTheDocument();
});
const editButton = screen.getByRole("button", { name: "Edit Settings" });
act(() => {
fireEvent.click(editButton);
});
await waitFor(() => {
expect(screen.getByLabelText("Models")).toBeInTheDocument();
});
const allProxyModelsOption = screen.queryByText("All Proxy Models");
expect(allProxyModelsOption).not.toBeInTheDocument();
});
});

View file

@ -1,50 +1,50 @@
import React, { useState, useEffect } from "react";
import NumericalInput from "../shared/numerical_input";
import UserSearchModal from "@/components/common_components/user_search_modal";
import {
Card,
Title,
Text,
Tab,
TabList,
TabGroup,
TabPanel,
TabPanels,
Grid,
Badge,
Button as TremorButton,
TextInput,
} from "@tremor/react";
import TeamMembersComponent from "./team_member_view";
import MemberPermissions from "./member_permissions";
import {
teamInfoCall,
teamMemberDeleteCall,
teamMemberAddCall,
teamMemberUpdateCall,
Member,
teamUpdateCall,
getGuardrailsList,
Member,
teamInfoCall,
teamMemberAddCall,
teamMemberDeleteCall,
teamMemberUpdateCall,
teamUpdateCall,
} from "@/components/networking";
import { Button, Form, Input, Select, Switch, message, Tooltip } from "antd";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import { InfoCircleOutlined } from "@ant-design/icons";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import MemberModal from "./edit_membership";
import UserSearchModal from "@/components/common_components/user_search_modal";
import {
Badge,
Card,
Grid,
Tab,
TabGroup,
TabList,
TabPanel,
TabPanels,
Text,
TextInput,
Title,
Button as TremorButton,
} from "@tremor/react";
import { Button, Form, Input, message, Select, Switch, Tooltip } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
import ObjectPermissionsView from "../object_permissions_view";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
import LoggingSettingsView from "../logging_settings_view";
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import EditLoggingSettings from "./EditLoggingSettings";
import LoggingSettingsView from "../logging_settings_view";
import { fetchMCPAccessGroups } from "../networking";
import { CheckIcon, CopyIcon } from "lucide-react";
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
import NotificationsManager from "../molecules/notifications_manager";
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import { fetchMCPAccessGroups } from "../networking";
import ObjectPermissionsView from "../object_permissions_view";
import NumericalInput from "../shared/numerical_input";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
import MemberModal from "./edit_membership";
import EditLoggingSettings from "./EditLoggingSettings";
import MemberPermissions from "./member_permissions";
import TeamMembersComponent from "./team_member_view";
export interface TeamMembership {
user_id: string;
@ -586,11 +586,17 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Input type="" />
</Form.Item>
<Form.Item label="Models" name="models">
<Form.Item
label="Models"
name="models"
rules={[{ required: true, message: "Please select at least one model" }]}
>
<Select mode="multiple" placeholder="Select models">
<Select.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select.Option>
{(is_proxy_admin || userModels.includes("all-proxy-models")) && (
<Select.Option key="all-proxy-models" value="all-proxy-models">
All Proxy Models
</Select.Option>
)}
<Select.Option key="no-default-models" value="no-default-models">
No Default Models
</Select.Option>