From 0e715138134cc69f00d0db4b6b017f26b7cc15cc Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 31 Oct 2026 03:33:37 +0000 Subject: [PATCH] feat(ui): make Projects generally available and remove enable_projects_ui flag Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../proxy/enterprise_routes.py | 3 +- .../proxy/ui_crud_endpoints/__init__.py | 3 -- .../ui_settings_extensions.py | 25 ---------- .../proxy_setting_endpoints.py | 20 -------- tests/e2e/ui/globalSetup.ts | 14 ------ .../projectDetachment.spec.ts | 16 ------ .../components/SidebarProvider.test.tsx | 10 ++-- .../components/SidebarProvider.tsx | 1 - .../CommandPalette/useVisibleMenuGroups.ts | 1 - .../components/CommandPalette/utils.test.ts | 11 ++--- .../AdminSettings/UISettings/UISettings.tsx | 29 ----------- .../src/components/leftnav.test.tsx | 43 +++++++--------- .../src/components/leftnav.tsx | 12 +---- .../create_key_button.integration.test.tsx | 11 +---- .../organisms/create_key_button.tsx | 49 +++++++++---------- .../key_edit_view.integration.test.tsx | 7 ++- .../components/templates/key_edit_view.tsx | 7 +-- .../components/templates/key_info_view.tsx | 31 +++++------- 18 files changed, 71 insertions(+), 222 deletions(-) delete mode 100644 enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py delete mode 100644 enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py diff --git a/enterprise/litellm_enterprise/proxy/enterprise_routes.py b/enterprise/litellm_enterprise/proxy/enterprise_routes.py index df3e5470d46..e45c3329787 100644 --- a/enterprise/litellm_enterprise/proxy/enterprise_routes.py +++ b/enterprise/litellm_enterprise/proxy/enterprise_routes.py @@ -4,13 +4,12 @@ from litellm_enterprise.enterprise_callbacks.send_emails.endpoints import ( router as email_events_router, ) -from . import ui_crud_endpoints # side-effect: registers extra UI settings from .audit_logging_endpoints import router as audit_logging_router from .liteadmin import router as liteadmin_router from .management_endpoints import management_endpoints_router from .utils import should_block_robots -__all__ = ["router", "ui_crud_endpoints"] +__all__ = ["router"] router = APIRouter() router.include_router(email_events_router) diff --git a/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py b/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py deleted file mode 100644 index 296d964f85b..00000000000 --- a/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import ui_settings_extensions # side-effect: registers extra UI settings fields - -__all__ = ["ui_settings_extensions"] diff --git a/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py b/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py deleted file mode 100644 index e61611ae595..00000000000 --- a/enterprise/litellm_enterprise/proxy/ui_crud_endpoints/ui_settings_extensions.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Enterprise-only UI settings fields. - -Registers additional fields onto the OSS ``UISettings`` model at import time. -Importing this module has the side effect of extending both the GET schema -and the PATCH allowlist served by ``/get/ui_settings`` and -``/update/ui_settings``. -""" - -from pydantic.fields import FieldInfo - -from litellm.proxy.ui_crud_endpoints.proxy_setting_endpoints import ( - register_extra_ui_setting, -) - -register_extra_ui_setting( - "enable_projects_ui", - bool, - FieldInfo( - default=False, - description=( - "If enabled, shows the Projects feature in the UI sidebar and " - "the project field in key management." - ), - ), -) diff --git a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py index 0efd4e4c619..e3bd21bd681 100644 --- a/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py +++ b/litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py @@ -477,12 +477,6 @@ _RUNTIME_GENERAL_SETTINGS_FLAGS: Final = [ # valid inputs. _EXTRA_UI_SETTINGS_FIELDS: Final[dict[str, tuple[object, FieldInfo]]] = {} -# Settings OSS knows about as enterprise-gated. If a caller sends one of -# these keys and no extension package has registered it, the PATCH -# endpoint returns 403 instead of silently dropping the value, so the -# client gets a clear signal that the feature requires LiteLLM Enterprise. -_ENTERPRISE_ONLY_UI_SETTINGS: Final[set[str]] = {"enable_projects_ui"} - # Memoized effective class; invalidated on registration. _EFFECTIVE_UI_SETTINGS_CLASS: type[UISettings] | None = None @@ -1977,20 +1971,6 @@ async def update_ui_settings( # Only include fields the caller actually sent (not Pydantic defaults). settings_dict: Final = _json_settings(settings.model_dump(exclude_unset=True)) - # Reject enterprise-only settings up front so the caller gets a clear - # signal instead of a silent drop. - blocked_enterprise_keys = sorted((settings_dict.keys() & _ENTERPRISE_ONLY_UI_SETTINGS) - ALLOWED_UI_SETTINGS_FIELDS) - if blocked_enterprise_keys: - raise HTTPException( - status_code=403, - detail={ - "error": ( - f"Setting(s) {blocked_enterprise_keys} are a LiteLLM " - "Enterprise feature and are not available on this build." - ) - }, - ) - # Enforce allowlist and drop anything unexpected incoming: Final = {k: v for k, v in settings_dict.items() if k in ALLOWED_UI_SETTINGS_FIELDS} diff --git a/tests/e2e/ui/globalSetup.ts b/tests/e2e/ui/globalSetup.ts index e18d1be390c..a0453e6992d 100644 --- a/tests/e2e/ui/globalSetup.ts +++ b/tests/e2e/ui/globalSetup.ts @@ -19,20 +19,6 @@ async function globalSetup() { // keeps the default "." a no-op. fs.mkdirSync(ARTIFACT_DIR, { recursive: true }); - // The Projects sidebar item is hidden unless the enterprise-gated - // enable_projects_ui setting is on, and the seeded DB starts with it off. - // The proxy runs with LITELLM_LICENSE in CI, so enable it the same way - // the admin UI toggle does; the projects migration smoke needs the link. - const masterKey = getMasterKey(); - const api = await request.newContext(); - const settingsRes = await api.patch(`${UI_BASE_URL}${rootPath}/update/ui_settings`, { - headers: { Authorization: `Bearer ${masterKey}` }, - data: { enable_projects_ui: true }, - }); - if (!settingsRes.ok()) { - throw new Error(`Enabling enable_projects_ui failed (${settingsRes.status()}): ${await settingsRes.text()}`); - } - const roles = [Role.ProxyAdmin, ...Object.values(Role).filter((role) => role !== Role.ProxyAdmin)]; for (const role of roles) { const { email, password, seedApiRole } = users[role]; diff --git a/tests/e2e/ui/tests/integrationCritical/projectDetachment.spec.ts b/tests/e2e/ui/tests/integrationCritical/projectDetachment.spec.ts index 497c2a6914f..2cee6040813 100644 --- a/tests/e2e/ui/tests/integrationCritical/projectDetachment.spec.ts +++ b/tests/e2e/ui/tests/integrationCritical/projectDetachment.spec.ts @@ -39,22 +39,6 @@ test("project creation and explicit detachment preserve saved scope and restore ), ); try { - const previous = await request.get("/get/ui_settings", { headers }); - expect(previous.ok(), await previous.text()).toBe(true); - const priorEnabled = - (await previous.json()).values.enable_projects_ui ?? false; - resources.push(async () => { - const response = await request.patch("/update/ui_settings", { - headers, - data: { enable_projects_ui: priorEnabled }, - }); - expect(response.ok(), await response.text()).toBe(true); - }); - const settings = await request.patch("/update/ui_settings", { - headers, - data: { enable_projects_ui: true }, - }); - expect(settings.ok(), await settings.text()).toBe(true); for (const alias of [prefix, `${prefix}-outside`]) { const model = await post("/model/new", { model_name: alias, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.test.tsx index a3d75df9606..c82b14c42e5 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.test.tsx @@ -6,13 +6,13 @@ import Sidebar from "@/components/leftnav"; import SidebarProvider from "./SidebarProvider"; const { getSettings } = vi.hoisted(() => ({ - getSettings: vi.fn().mockResolvedValue({ values: { enable_projects_ui: true } }), + getSettings: vi.fn().mockResolvedValue({ values: { allow_agents_for_team_admins: true } }), })); vi.mock("@/components/networking", () => ({ getUiSettings: getSettings, getUISettings: getSettings })); vi.mock("@/components/leftnav", () => ({ - default: ({ enableProjectsUI }: ComponentProps) => ( - + default: ({ allowAgentsForTeamAdmins }: ComponentProps) => ( + ), })); @@ -31,13 +31,13 @@ it("reuses loaded navigation settings immediately when the mobile sidebar mounts ); const { rerender } = render(shell(false)); - expect(await screen.findByRole("link", { name: "Projects" })).toBeVisible(); + expect(await screen.findByRole("link", { name: "Agents" })).toBeVisible(); for (const open of [true, false, true]) { rerender(shell(open)); if (open) { expect( - within(screen.getByRole("region", { name: "Mobile navigation" })).getByRole("link", { name: "Projects" }), + within(screen.getByRole("region", { name: "Mobile navigation" })).getByRole("link", { name: "Agents" }), ).toBeVisible(); } } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx index c7794233ded..ba7808b9cb6 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/components/SidebarProvider.tsx @@ -17,7 +17,6 @@ const SidebarProvider = ({ sidebarCollapsed, onToggleCollapsed }: SidebarProvide collapsed={sidebarCollapsed} onToggleCollapsed={onToggleCollapsed} enabledPagesInternalUsers={values?.enabled_ui_pages_internal_users ?? null} - enableProjectsUI={Boolean(values?.enable_projects_ui)} disableAgentsForInternalUsers={Boolean(values?.disable_agents_for_internal_users)} allowAgentsForTeamAdmins={Boolean(values?.allow_agents_for_team_admins)} disableVectorStoresForInternalUsers={Boolean(values?.disable_vector_stores_for_internal_users)} diff --git a/ui/litellm-dashboard/src/components/CommandPalette/useVisibleMenuGroups.ts b/ui/litellm-dashboard/src/components/CommandPalette/useVisibleMenuGroups.ts index 8c3b9382934..9e4e056e9d8 100644 --- a/ui/litellm-dashboard/src/components/CommandPalette/useVisibleMenuGroups.ts +++ b/ui/litellm-dashboard/src/components/CommandPalette/useVisibleMenuGroups.ts @@ -23,7 +23,6 @@ export const useVisibleMenuGroups = () => { isOrgAdmin, isTeamAdmin, enabledPagesInternalUsers: values?.enabled_ui_pages_internal_users ?? null, - enableProjectsUI: Boolean(values?.enable_projects_ui), disableAgentsForInternalUsers: Boolean(values?.disable_agents_for_internal_users), allowAgentsForTeamAdmins: Boolean(values?.allow_agents_for_team_admins), disableVectorStoresForInternalUsers: Boolean(values?.disable_vector_stores_for_internal_users), diff --git a/ui/litellm-dashboard/src/components/CommandPalette/utils.test.ts b/ui/litellm-dashboard/src/components/CommandPalette/utils.test.ts index 9fda2335acf..7ca301fa07d 100644 --- a/ui/litellm-dashboard/src/components/CommandPalette/utils.test.ts +++ b/ui/litellm-dashboard/src/components/CommandPalette/utils.test.ts @@ -57,13 +57,12 @@ describe("visibleMenuGroups", () => { expect(flattenNavItems(visible).map(({ route }) => route)).toEqual(["api-keys"]); }); - it("hides Projects when its UI setting is disabled", () => { - const projectAdminContext = { ...context, userRole: "Admin", isTeamAdmin: true }; - const visibleWithFlag = visibleMenuGroups(menuGroups, { ...projectAdminContext, enableProjectsUI: true }); - const visibleWithoutFlag = visibleMenuGroups(menuGroups, { ...projectAdminContext, enableProjectsUI: false }); + it("shows Projects to admins and hides it from internal users who administer no team", () => { + const visibleToAdmin = visibleMenuGroups(menuGroups, { ...context, userRole: "Admin" }); + const visibleToMember = visibleMenuGroups(menuGroups, context); - expect(flattenNavItems(visibleWithFlag).some(({ route }) => route === "projects")).toBe(true); - expect(flattenNavItems(visibleWithoutFlag).some(({ route }) => route === "projects")).toBe(false); + expect(flattenNavItems(visibleToAdmin).some(({ route }) => route === "projects")).toBe(true); + expect(flattenNavItems(visibleToMember).some(({ route }) => route === "projects")).toBe(false); }); it("keeps admin pages visible despite the internal-user page allowlist", () => { diff --git a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx index 3103851f280..75ff98be5b3 100644 --- a/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx +++ b/ui/litellm-dashboard/src/components/Settings/AdminSettings/UISettings/UISettings.tsx @@ -63,7 +63,6 @@ export default function UISettings() { const requireAuthForPublicAIHubProperty = schema?.properties?.require_auth_for_public_ai_hub; const forwardClientHeadersProperty = schema?.properties?.forward_client_headers_to_llm_api; const forwardLLMProviderAuthHeadersProperty = schema?.properties?.forward_llm_provider_auth_headers; - const enableProjectsUIProperty = schema?.properties?.enable_projects_ui; const enableChatUIProperty = schema?.properties?.enable_chat_ui; const enabledPagesProperty = schema?.properties?.enabled_ui_pages_internal_users; const disableAgentsProperty = schema?.properties?.disable_agents_for_internal_users; @@ -150,21 +149,6 @@ export default function UISettings() { ); }; - const handleToggleEnableProjectsUI = (checked: boolean) => { - updateSettings( - { enable_projects_ui: checked }, - { - onSuccess: () => { - toast.success("UI settings updated successfully. Refreshing page..."); - setTimeout(() => window.location.reload(), 1000); - }, - onError: (error) => { - toast.fromError(error); - }, - }, - ); - }; - const handleToggleEnableChatUI = (checked: boolean) => { updateSettings( { enable_chat_ui: checked }, @@ -411,19 +395,6 @@ export default function UISettings() { "Forwards provider auth headers (x-api-key, x-goog-api-key, api-key, ocp-apim-subscription-key) to the upstream LLM, overriding any deployment-configured key for that request. Enable for Claude Code BYOK (clients bring their own API key). Independent of the client-headers toggle — enable only the one(s) you need." } /> - {enableProjectsUIProperty && ( - - )} { error: null, }); - renderWithProviders(); + renderWithProviders(); expect(screen.getByRole("link", { name: /Projects/ })).toBeInTheDocument(); }); - it("hides Projects when the feature flag is disabled for a team admin", () => { - mockUseAuthorized.mockReturnValue(teamAdminAuthorization); - mockUseTeams.mockReturnValue({ - data: [ - { - members_with_roles: [{ user_id: "team-admin-user-id", role: "admin" }], - }, - ], - isLoading: false, - error: null, - }); - - renderWithProviders(); - - expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument(); - }); - it("hides Projects from an internal user who is not a team admin", () => { mockUseAuthorized.mockReturnValue(teamMemberAuthorization); - renderWithProviders(); + renderWithProviders(); expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument(); }); @@ -634,7 +617,7 @@ describe("Sidebar (leftnav)", () => { error: null, }); - renderWithProviders(); + renderWithProviders(); expect(screen.queryByRole("link", { name: /Projects/ }) !== null).toBe(visible); }); @@ -642,7 +625,7 @@ describe("Sidebar (leftnav)", () => { it("shows Projects to a user whose global role is org admin", () => { mockUseAuthorized.mockReturnValue({ ...teamMemberAuthorization, userRole: "Org Admin" }); - renderWithProviders(); + renderWithProviders(); expect(screen.getByRole("link", { name: /Projects/ })).toHaveAttribute("href", "/ui/projects"); }); @@ -683,7 +666,7 @@ describe("Sidebar (leftnav)", () => { }); it("never links a leaf to the legacy ?page= switch", () => { - renderWithProviders(); + renderWithProviders(); for (const group of ["Agentic", "Tools", "Experimental", "Settings"]) { act(() => { fireEvent.click(screen.getByText(group)); @@ -707,19 +690,27 @@ describe("Sidebar (leftnav)", () => { expect(label).toHaveClass("group-data-[collapsed=true]/sidebar:hidden"); }); - it("shows Cost Optimization without a Beta badge and no feature-flag gate", () => { - const { container } = renderWithProviders(); + it("shows Cost Optimization without a Beta badge", () => { + const { container } = renderWithProviders(); const costOptimization = container.querySelector('a[href*="cost-optimization"]'); expect(costOptimization).not.toBeNull(); expect(costOptimization!).toHaveTextContent("Cost Optimization"); expect(costOptimization!).not.toHaveTextContent("Beta"); + }); - expect(container.querySelector('a[href*="projects"]')).toBeNull(); + it("shows Projects to an org admin without a Beta badge", () => { + mockUseAuthorized.mockReturnValue({ ...teamMemberAuthorization, userRole: "Org Admin" }); + + renderWithProviders(); + + const projects = screen.getByRole("link", { name: "Projects" }); + expect(projects).toHaveAttribute("href", "/ui/projects"); + expect(projects).not.toHaveTextContent("Beta"); }); it("keeps a readable collapsed-rail tooltip for items whose label carries a badge", () => { - const { container } = renderWithProviders(); + const { container } = renderWithProviders(); expect(container.querySelector('a[href*="cost-optimization"]')).toHaveAttribute("title", "Cost Optimization"); expect(container.querySelector('a[href*="projects"]')).toHaveAttribute("title", "Projects"); diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index 2cf6f8219db..258f1cf808f 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -99,7 +99,6 @@ interface SidebarProps { collapsed?: boolean; onToggleCollapsed?: () => void; enabledPagesInternalUsers?: string[] | null; - enableProjectsUI?: boolean; disableAgentsForInternalUsers?: boolean; allowAgentsForTeamAdmins?: boolean; disableVectorStoresForInternalUsers?: boolean; @@ -129,7 +128,6 @@ export interface MenuVisibilityContext { isOrgAdmin: boolean; isTeamAdmin: boolean; enabledPagesInternalUsers?: string[] | null; - enableProjectsUI?: boolean; disableAgentsForInternalUsers?: boolean; allowAgentsForTeamAdmins?: boolean; disableVectorStoresForInternalUsers?: boolean; @@ -148,7 +146,6 @@ const adminPageVisibility = (item: MenuItem, context: MenuVisibilityContext, isA const projectPageIsVisible = (item: MenuItem, context: MenuVisibilityContext): boolean => { if (item.key !== "projects") return true; - if (!context.enableProjectsUI) return false; return canViewProjectsPage({ userRole: context.userRole, isOrgAdmin: context.isOrgAdmin, @@ -342,11 +339,7 @@ const menuGroups: MenuGroup[] = [ { key: "projects", page: "projects", - label: ( - - Projects - - ), + label: "Projects", icon: , roles: [...projectReaderRoles], }, @@ -533,7 +526,6 @@ const Sidebar_: React.FC = ({ collapsed = false, onToggleCollapsed, enabledPagesInternalUsers, - enableProjectsUI, disableAgentsForInternalUsers, allowAgentsForTeamAdmins, disableVectorStoresForInternalUsers, @@ -580,7 +572,6 @@ const Sidebar_: React.FC = ({ isOrgAdmin, isTeamAdmin, enabledPagesInternalUsers, - enableProjectsUI, disableAgentsForInternalUsers, allowAgentsForTeamAdmins, disableVectorStoresForInternalUsers, @@ -592,7 +583,6 @@ const Sidebar_: React.FC = ({ allowVectorStoresForTeamAdmins, disableAgentsForInternalUsers, disableVectorStoresForInternalUsers, - enableProjectsUI, enabledPagesInternalUsers, isOrgAdmin, isTeamAdmin, diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx index 0fbe043c0d1..34b013916f3 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.integration.test.tsx @@ -131,6 +131,7 @@ const SECTIONS = { const ALL_CLOSED_PAYLOAD = { organization_id: undefined, + project_id: undefined, team_id: null, key_alias: "contract-key", models: [], @@ -538,15 +539,6 @@ describe("CreateKey", () => { expect(payload).toHaveProperty("guardrails"); }); - it("adds project_id only when the projects UI is enabled", async () => { - state.uiSettings = { enable_projects_ui: true }; - await openModal(); - await nameTheKey(); - await submit(); - - expect(await createdPayload()).toStrictEqual({ ...ALL_CLOSED_PAYLOAD, project_id: undefined }); - }); - it("keeps disable_global_guardrails out of the payload while the switch is off", async () => { await openModal(); await nameTheKey(); @@ -923,7 +915,6 @@ describe("CreateKey", () => { }); it("discards the old project and team when the organization changes", async () => { - state.uiSettings = { enable_projects_ui: true }; state.organizations = [ { organization_id: "scope-silver", organization_alias: "Silver" }, { organization_id: "scope-copper", organization_alias: "Copper" }, diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx index b096954722a..bfbffcc0e6a 100644 --- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx @@ -241,7 +241,6 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp const { data: projects, isLoading: isProjectsLoading } = useProjects(); const { data: uiSettingsData } = useUISettings(); const { data: tagsData } = useTags(); - const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui); const disableCustomApiKeys = Boolean(uiSettingsData?.values?.disable_custom_api_keys); const tagOptions = tagsData ? Object.values(tagsData).map((tag) => ({ value: tag.name, label: tag.name })) : []; const queryClient = useQueryClient(); @@ -884,31 +883,29 @@ const CreateKey: React.FC = ({ team, teams, data, addKey, autoOp )} {ownerLimitedToTeamMembers && ownerField} - {enableProjectsUI && ( - - Project{" "} - - - - - } - name="project_id" - className="mt-4" - > - {(control) => ( - - )} - - )} + + Project{" "} + + + + + } + name="project_id" + className="mt-4" + > + {(control) => ( + + )} + {/* Show message when team selection is required */} diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx index dce14adc2e5..5c64137727d 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.integration.test.tsx @@ -25,7 +25,7 @@ vi.mock("../networking", async () => { const actual = await vi.importActual("../networking"); return { ...actual, - getUiSettings: vi.fn().mockResolvedValue({ values: { enable_projects_ui: false } }), + getUiSettings: vi.fn().mockResolvedValue({ values: {} }), getPromptsList: vi.fn().mockResolvedValue({ prompts: [{ prompt_id: "prompt-1" }, { prompt_id: "prompt-2" }], }), @@ -396,7 +396,7 @@ describe("KeyEditView", () => { beforeEach(() => { vi.clearAllMocks(); can.mockReturnValue(true); - vi.mocked(getUiSettings).mockResolvedValue({ values: { enable_projects_ui: false } }); + vi.mocked(getUiSettings).mockResolvedValue({ values: {} }); testQueryClient.removeQueries({ queryKey: ["uiSettings"] }); }); @@ -2006,7 +2006,6 @@ describe("KeyEditView", () => { }); it("should save an explicit project detach while keeping parents locked until reopening the editor", async () => { - vi.mocked(getUiSettings).mockResolvedValue({ values: { enable_projects_ui: true } }); const onSubmit = vi.fn().mockResolvedValue(undefined); const onCancel = vi.fn(); const key = { ...MOCK_KEY_DATA, organization_id: "org-1", team_id: "group-maple", project_id: "project-orbit" }; @@ -2067,7 +2066,7 @@ describe("KeyEditView", () => { expect(await screen.findByRole("button", { name: "Detach from project" })).toBeInTheDocument(); }); - it("keeps project key relationships locked and omits project updates when the project UI is disabled", async () => { + it("keeps project key relationships locked and omits project_id when the project is not detached", async () => { const onSubmit = vi.fn().mockResolvedValue(undefined); renderWithProviders( - {enableProjectsUI && hasProject && ( + {hasProject && ( - {enableProjectsUI && ( -
-

Project

-

- {currentKeyData.project_id - ? (() => { - const project = projects?.find((p) => p.project_id === currentKeyData.project_id); - return project?.project_alias - ? `${project.project_alias} (${currentKeyData.project_id})` - : currentKeyData.project_id; - })() - : "Not Set"} -

-
- )} +
+

Project

+

+ {currentKeyData.project_id + ? (() => { + const project = projects?.find((p) => p.project_id === currentKeyData.project_id); + return project?.project_alias + ? `${project.project_alias} (${currentKeyData.project_id})` + : currentKeyData.project_id; + })() + : "Not Set"} +

+

Organization