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>
This commit is contained in:
ryan 2026-10-31 03:33:37 +00:00
parent e6b4774ac6
commit 0e71513813
18 changed files with 71 additions and 222 deletions

View file

@ -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)

View file

@ -1,3 +0,0 @@
from . import ui_settings_extensions # side-effect: registers extra UI settings fields
__all__ = ["ui_settings_extensions"]

View file

@ -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."
),
),
)

View file

@ -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}

View file

@ -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];

View file

@ -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,

View file

@ -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<typeof Sidebar>) => (
<nav>{enableProjectsUI && <a href="/projects">Projects</a>}</nav>
default: ({ allowAgentsForTeamAdmins }: ComponentProps<typeof Sidebar>) => (
<nav>{allowAgentsForTeamAdmins && <a href="/agents">Agents</a>}</nav>
),
}));
@ -31,13 +31,13 @@ it("reuses loaded navigation settings immediately when the mobile sidebar mounts
</QueryClientProvider>
);
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();
}
}

View file

@ -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)}

View file

@ -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),

View file

@ -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", () => {

View file

@ -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 && (
<SettingRow
checked={Boolean(values.enable_projects_ui)}
disabled={isUpdating}
onCheckedChange={handleToggleEnableProjectsUI}
ariaLabel={enableProjectsUIProperty.description ?? "Enable Projects UI"}
label="[BETA] Enable Projects (page will refresh)"
description={
enableProjectsUIProperty.description ??
"If enabled, shows the Projects feature in the UI sidebar and the project field in key management."
}
/>
)}
<SettingRow
checked={Boolean(values.enable_chat_ui)}
disabled={isUpdating}

View file

@ -589,32 +589,15 @@ describe("Sidebar (leftnav)", () => {
error: null,
});
renderWithProviders(<Sidebar {...defaultProps} enableProjectsUI />);
renderWithProviders(<Sidebar {...defaultProps} />);
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(<Sidebar {...defaultProps} enableProjectsUI={false} />);
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(<Sidebar {...defaultProps} enableProjectsUI />);
renderWithProviders(<Sidebar {...defaultProps} />);
expect(screen.queryByRole("link", { name: /Projects/ })).not.toBeInTheDocument();
});
@ -634,7 +617,7 @@ describe("Sidebar (leftnav)", () => {
error: null,
});
renderWithProviders(<Sidebar {...defaultProps} enableProjectsUI enabledPagesInternalUsers={allowlist} />);
renderWithProviders(<Sidebar {...defaultProps} enabledPagesInternalUsers={allowlist} />);
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(<Sidebar {...defaultProps} enableProjectsUI />);
renderWithProviders(<Sidebar {...defaultProps} />);
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(<Sidebar {...defaultProps} enableProjectsUI />);
renderWithProviders(<Sidebar {...defaultProps} />);
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(<Sidebar {...defaultProps} enableProjectsUI={false} />);
it("shows Cost Optimization without a Beta badge", () => {
const { container } = renderWithProviders(<Sidebar {...defaultProps} />);
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(<Sidebar {...defaultProps} />);
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(<Sidebar {...defaultProps} enableProjectsUI collapsed />);
const { container } = renderWithProviders(<Sidebar {...defaultProps} collapsed />);
expect(container.querySelector('a[href*="cost-optimization"]')).toHaveAttribute("title", "Cost Optimization");
expect(container.querySelector('a[href*="projects"]')).toHaveAttribute("title", "Projects");

View file

@ -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: (
<span className="flex items-center gap-2">
Projects <BetaBadge />
</span>
),
label: "Projects",
icon: <Folder {...ICON} />,
roles: [...projectReaderRoles],
},
@ -533,7 +526,6 @@ const Sidebar_: React.FC<SidebarProps> = ({
collapsed = false,
onToggleCollapsed,
enabledPagesInternalUsers,
enableProjectsUI,
disableAgentsForInternalUsers,
allowAgentsForTeamAdmins,
disableVectorStoresForInternalUsers,
@ -580,7 +572,6 @@ const Sidebar_: React.FC<SidebarProps> = ({
isOrgAdmin,
isTeamAdmin,
enabledPagesInternalUsers,
enableProjectsUI,
disableAgentsForInternalUsers,
allowAgentsForTeamAdmins,
disableVectorStoresForInternalUsers,
@ -592,7 +583,6 @@ const Sidebar_: React.FC<SidebarProps> = ({
allowVectorStoresForTeamAdmins,
disableAgentsForInternalUsers,
disableVectorStoresForInternalUsers,
enableProjectsUI,
enabledPagesInternalUsers,
isOrgAdmin,
isTeamAdmin,

View file

@ -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" },

View file

@ -241,7 +241,6 @@ const CreateKey: React.FC<CreateKeyProps> = ({ 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<CreateKeyProps> = ({ team, teams, data, addKey, autoOp
)}
</MountedFormField>
{ownerLimitedToTeamMembers && ownerField}
{enableProjectsUI && (
<MountedFormField
label={
<span>
Project{" "}
<SimpleTooltip content="Assign this key to a project. Selecting a project will lock the team to the project's team.">
<Info className="ml-1 inline size-3.5 align-text-bottom" />
</SimpleTooltip>
</span>
}
name="project_id"
className="mt-4"
>
{(control) => (
<ProjectDropdown
id={control.id}
value={typeof control.value === "string" ? control.value : null}
projects={projects}
teamId={selectedCreateKeyTeam?.team_id}
loading={isProjectsLoading || !teams}
onChange={changeProject(control.onChange)}
/>
)}
</MountedFormField>
)}
<MountedFormField
label={
<span>
Project{" "}
<SimpleTooltip content="Assign this key to a project. Selecting a project will lock the team to the project's team.">
<Info className="ml-1 inline size-3.5 align-text-bottom" />
</SimpleTooltip>
</span>
}
name="project_id"
className="mt-4"
>
{(control) => (
<ProjectDropdown
id={control.id}
value={typeof control.value === "string" ? control.value : null}
projects={projects}
teamId={selectedCreateKeyTeam?.team_id}
loading={isProjectsLoading || !teams}
onChange={changeProject(control.onChange)}
/>
)}
</MountedFormField>
</div>
{/* Show message when team selection is required */}

View file

@ -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(
<KeyEditView

View file

@ -1,7 +1,6 @@
import { canDetachKeyProject, KeyProjectField } from "./KeyProjectField";
import GuardrailSelector from "@/components/guardrails/GuardrailSelector";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import PolicySelector from "@/components/policies/PolicySelector";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -143,8 +142,6 @@ function KeyEditSession({
const keyTypeFieldId = React.useId();
const endUserBudgetFieldId = React.useId();
const { data: organizations, isLoading: isOrganizationsLoading } = useOrganizations();
const { data: uiSettingsData } = useUISettings();
const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui);
const hasProject = Boolean(keyData.project_id);
const canChangeOrganization = userRole === "Admin" && !hasProject;
const detachProject = hasProject && form.watch("project_id") === null;
@ -317,7 +314,7 @@ function KeyEditSession({
await onSubmit(
withNormalizedEstimates({
...values,
...(detachProject && enableProjectsUI && canDetachProject ? { project_id: null } : {}),
...(detachProject && canDetachProject ? { project_id: null } : {}),
}),
[...Object.keys(form.formState.dirtyFields), ...(budgetLimitsUnchanged ? [] : ["budget_limits"])],
);
@ -822,7 +819,7 @@ function KeyEditSession({
)}
</FormField>
{enableProjectsUI && hasProject && (
{hasProject && (
<KeyProjectField
projectId={keyData.project_id}
canDetach={canDetachProject}

View file

@ -1,7 +1,6 @@
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useModelAccessGroupNames } from "@/app/(dashboard)/hooks/models/useModels";
import { useProjects } from "@/app/(dashboard)/hooks/projects/useProjects";
import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings";
import { useApplyUserBudgetToTeamKeys } from "@/app/(dashboard)/hooks/uiSettings/useApplyUserBudgetToTeamKeys";
import useTeams from "@/app/(dashboard)/hooks/useTeams";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
@ -111,11 +110,9 @@ export default function KeyInfoView({
const { teams: teamsData } = useTeams();
const { data: organizations } = useOrganizations();
const { data: projects } = useProjects();
const { data: uiSettingsData } = useUISettings();
const applyUserBudgetToTeamKeys = useApplyUserBudgetToTeamKeys();
const { data: allMcpServers } = useMCPServers();
const { data: allMcpToolsets } = useMCPToolsets();
const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui);
const [isEditing, setIsEditing] = useState(false);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
@ -968,21 +965,19 @@ export default function KeyInfoView({
</p>
</div>
{enableProjectsUI && (
<div>
<p className="text-sm font-medium">Project</p>
<p className="text-sm">
{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"}
</p>
</div>
)}
<div>
<p className="text-sm font-medium">Project</p>
<p className="text-sm">
{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"}
</p>
</div>
<div>
<p className="text-sm font-medium">Organization</p>