= ({
+ value,
+ onChange,
+ placeholder = "Select access groups",
+ disabled = false,
+ style,
+ className,
+ showLabel = false,
+ labelText = "Access Group",
+ allowClear = true,
+}) => {
+ const { data: accessGroups, isLoading, isError } = useAccessGroups();
+
+ // ── Loading skeleton ─────────────────────────────────────────────────────
+ if (isLoading) {
+ return (
+
+ {showLabel && (
+
+ {labelText}
+
+ )}
+
+
+ );
+ }
+
+ // ── Build options ────────────────────────────────────────────────────────
+ const options = (accessGroups ?? []).map((group: AccessGroupResponse) => ({
+ label: (
+
+ {group.access_group_name}{" "}
+ ({group.access_group_id})
+
+ ),
+ value: group.access_group_id,
+ selectedLabel: group.access_group_name,
+ searchText: `${group.access_group_name} ${group.access_group_id}`,
+ }));
+
+ // ── Render ───────────────────────────────────────────────────────────────
+ return (
+
+ {showLabel && (
+
+ {labelText}
+
+ )}
+
+ );
+};
+
+export default AccessGroupSelector;
diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
index 5511a671db7..b54eb21a0ae 100644
--- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
+++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx
@@ -87,6 +87,7 @@ export interface KeyResponse {
agents?: string[];
agent_access_groups?: string[];
};
+ access_group_ids?: string[];
auto_rotate?: boolean;
rotation_interval?: string;
last_rotation_at?: string;
diff --git a/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx b/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx
index bd5e0ee1cbc..b5198e4f861 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx
@@ -1,15 +1,19 @@
+import { act, fireEvent, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders, screen } from "../../../tests/test-utils";
import CreateKey from "./create_key_button";
-const mockKeyCreateCall = vi.fn().mockResolvedValue({
- key: "test-api-key",
- soft_budget: null,
+const { mockKeyCreateCall } = vi.hoisted(() => {
+ const fn = vi.fn().mockResolvedValue({
+ key: "test-api-key",
+ soft_budget: null,
+ });
+ return { mockKeyCreateCall: fn };
});
-vi.mock("./networking", () => ({
+vi.mock("../networking", () => ({
keyCreateCall: mockKeyCreateCall,
- modelAvailableCall: vi.fn().mockResolvedValue({ data: [] }),
+ modelAvailableCall: vi.fn().mockResolvedValue({ data: [{ id: "gpt-4" }, { id: "gpt-3.5-turbo" }] }),
getGuardrailsList: vi.fn().mockResolvedValue({ guardrails: [] }),
getPromptsList: vi.fn().mockResolvedValue({ prompts: [] }),
proxyBaseUrl: "http://localhost:4000",
@@ -25,7 +29,7 @@ vi.mock("./networking", () => ({
fetchMCPAccessGroups: vi.fn().mockResolvedValue([]),
}));
-vi.mock("./molecules/notifications_manager", () => ({
+vi.mock("../molecules/notifications_manager", () => ({
default: {
success: vi.fn(),
fromBackend: vi.fn(),
@@ -36,16 +40,22 @@ vi.mock("./molecules/notifications_manager", () => ({
},
}));
+vi.mock("../common_components/AccessGroupSelector", () => ({
+ default: ({ value = [], onChange }: { value?: string[]; onChange?: (v: string[]) => void }) => (
+ onChange?.(e.target.value ? e.target.value.split(",").map((s) => s.trim()) : [])}
+ />
+ ),
+}));
+
describe("CreateKey", () => {
const defaultProps = {
- userID: "test-user-id",
team: null,
- userRole: "Admin",
- accessToken: "test-token",
data: [],
teams: [],
addKey: vi.fn(),
- premiumUser: false,
};
beforeEach(() => {
@@ -61,4 +71,54 @@ describe("CreateKey", () => {
renderWithProviders();
expect(screen.getByRole("button", { name: /create new key/i })).toBeInTheDocument();
});
+
+ it("should include access_group_ids in keyCreateCall payload when access groups are selected", async () => {
+ renderWithProviders();
+
+ act(() => {
+ fireEvent.click(screen.getByRole("button", { name: /create new key/i }));
+ });
+
+ await waitFor(() => {
+ expect(screen.getByLabelText(/key name/i)).toBeInTheDocument();
+ });
+
+ fireEvent.change(screen.getByLabelText(/key name/i), { target: { value: "Test Key" } });
+
+ const optionalSettingsAccordion = screen.getByText("Optional Settings");
+ act(() => {
+ fireEvent.click(optionalSettingsAccordion);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByTestId("access-group-selector")).toBeInTheDocument();
+ });
+
+ fireEvent.change(screen.getByTestId("access-group-selector"), { target: { value: "ag-1,ag-2" } });
+
+ const modelsCombobox = screen.getAllByRole("combobox").find((el) => el.closest('[class*="ant-form-item"]')?.textContent?.includes("Models")) ||
+ screen.getAllByRole("combobox")[1];
+ if (modelsCombobox) {
+ act(() => fireEvent.mouseDown(modelsCombobox));
+ await waitFor(() => {
+ const allTeamModels = [...document.body.querySelectorAll(".ant-select-item")].find(
+ (el) => el.textContent?.includes("All Team Models"),
+ );
+ if (allTeamModels) fireEvent.click(allTeamModels);
+ });
+ }
+
+ const createButton = screen.getByRole("button", { name: /create key/i });
+ act(() => fireEvent.click(createButton));
+
+ await waitFor(
+ () => {
+ expect(mockKeyCreateCall).toHaveBeenCalled();
+ const formValues = mockKeyCreateCall.mock.calls[0][2];
+ expect(formValues).toHaveProperty("access_group_ids");
+ expect(formValues.access_group_ids).toEqual(["ag-1", "ag-2"]);
+ },
+ { timeout: 5000 },
+ );
+ });
});
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 abadbe10590..7937ebef666 100644
--- a/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
+++ b/ui/litellm-dashboard/src/components/organisms/create_key_button.tsx
@@ -20,6 +20,7 @@ import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSel
import PremiumLoggingSettings from "../common_components/PremiumLoggingSettings";
import RateLimitTypeFormItem from "../common_components/RateLimitTypeFormItem";
import RouterSettingsAccordion, { RouterSettingsAccordionValue } from "../common_components/RouterSettingsAccordion";
+import AccessGroupSelector from "../common_components/AccessGroupSelector";
import TeamDropdown from "../common_components/team_dropdown";
import { CreateUserButton } from "../CreateUserButton";
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
@@ -998,6 +999,23 @@ const CreateKey: React.FC = ({ team, teams, data, addKey }) => {
options={promptsList.map((name) => ({ value: name, label: name }))}
/>