mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(ui): show team alias in key edit Team dropdown and allow clearing it (#45629)
* fix(ui): show team alias in key edit Team dropdown and allow clearing it Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(ui): show alias-less teams by ID once in the key edit Team dropdown * fix(ui): keep the key organization when its team is cleared * fix(ui): show a dash for teams without an alias, keeping the ID underneath --------- Co-authored-by: yuneng <yuneng@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
dafa9cbd4c
commit
07e315ebfb
2 changed files with 120 additions and 28 deletions
|
|
@ -1530,7 +1530,9 @@ describe("KeyEditView", () => {
|
|||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Organization")).toHaveValue("Engineering");
|
||||
});
|
||||
await userEvent.click(screen.getByRole("button", { name: "Clear" }));
|
||||
const clearButtons = screen.getAllByRole("button", { name: "Clear" });
|
||||
expect(clearButtons).toHaveLength(2);
|
||||
await userEvent.click(clearButtons[0]);
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
|
|
@ -1571,7 +1573,7 @@ describe("KeyEditView", () => {
|
|||
const view = renderWithProviders(renderEditor());
|
||||
await userEvent.click(await screen.findByRole("button", { name: "Detach from project" }));
|
||||
expect(screen.getByRole("combobox", { name: "Organization" })).toBeDisabled();
|
||||
expect(screen.getByRole("combobox", { name: "Team ID" })).toBeDisabled();
|
||||
expect(screen.getByRole("combobox", { name: "Team" })).toBeDisabled();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(onCancel).toHaveBeenCalledOnce();
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
|
|
@ -1582,9 +1584,9 @@ describe("KeyEditView", () => {
|
|||
await waitFor(() =>
|
||||
expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining(expectedDetach), expect.any(Array)),
|
||||
);
|
||||
expect(screen.getByRole("combobox", { name: "Team ID" })).toBeDisabled();
|
||||
expect(screen.getByRole("combobox", { name: "Team" })).toBeDisabled();
|
||||
view.rerender(renderEditor({ ...key, project_id: null }));
|
||||
expect(screen.getByRole("combobox", { name: "Team ID" })).toBeEnabled();
|
||||
expect(screen.getByRole("combobox", { name: "Team" })).toBeEnabled();
|
||||
expect(screen.queryByRole("button", { name: "Detach from project" })).not.toBeInTheDocument();
|
||||
view.rerender(renderEditor(key, "Internal User"));
|
||||
expect(screen.queryByRole("button", { name: "Detach from project" })).not.toBeInTheDocument();
|
||||
|
|
@ -1615,7 +1617,7 @@ describe("KeyEditView", () => {
|
|||
/>,
|
||||
);
|
||||
expect(await screen.findByRole("combobox", { name: "Organization" })).toBeDisabled();
|
||||
expect(screen.getByRole("combobox", { name: "Team ID" })).toBeDisabled();
|
||||
expect(screen.getByRole("combobox", { name: "Team" })).toBeDisabled();
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
|
||||
expect(onSubmit.mock.calls[0][0]).toMatchObject({ organization_id: "org-1", team_id: "group-maple" });
|
||||
|
|
@ -2435,7 +2437,7 @@ describe("KeyEditView", () => {
|
|||
);
|
||||
await screen.findByRole("button", { name: /save changes/i });
|
||||
|
||||
await userEvent.click(screen.getByLabelText("Team ID"));
|
||||
await userEvent.click(screen.getByRole("combobox", { name: "Team" }));
|
||||
await userEvent.click(await screen.findByRole("option", { name: /Team Nine/ }));
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
|
@ -2446,6 +2448,98 @@ describe("KeyEditView", () => {
|
|||
expect(onSubmitMock.mock.calls[0][0].team_id).toBe("team-9");
|
||||
});
|
||||
|
||||
it("shows team aliases and IDs in the Team combobox", async () => {
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={{ ...MOCK_KEY_DATA, team_id: "team-1" }}
|
||||
teams={[
|
||||
{ team_id: "team-1", team_alias: "Alpha" },
|
||||
{ team_id: "team-2", team_alias: "Beta" },
|
||||
]}
|
||||
onCancel={() => {}}
|
||||
onSubmit={async () => {}}
|
||||
accessToken=""
|
||||
userID=""
|
||||
userRole="Admin"
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const teamCombobox = screen.getByRole("combobox", { name: "Team" });
|
||||
expect(teamCombobox).toHaveValue("Alpha");
|
||||
await userEvent.click(teamCombobox);
|
||||
|
||||
const alphaOption = await screen.findByRole("option", { name: /Alpha/ });
|
||||
const betaOption = screen.getByRole("option", { name: /Beta/ });
|
||||
expect(alphaOption).toHaveTextContent("team-1");
|
||||
expect(betaOption).toHaveTextContent("team-2");
|
||||
});
|
||||
|
||||
it("shows a dash with the ID underneath for a team without an alias", async () => {
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={{ ...MOCK_KEY_DATA, team_id: null }}
|
||||
teams={[{ team_id: "team-3", team_alias: null }]}
|
||||
onCancel={() => {}}
|
||||
onSubmit={async () => {}}
|
||||
accessToken=""
|
||||
userID=""
|
||||
userRole="Admin"
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByRole("combobox", { name: "Team" }));
|
||||
|
||||
expect(await screen.findByRole("option")).toHaveTextContent(/^-team-3$/);
|
||||
});
|
||||
|
||||
it("clears the team in the update payload and keeps the organization", async () => {
|
||||
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={{ ...MOCK_KEY_DATA, organization_id: "org-1", team_id: "team-1" }}
|
||||
teams={[{ team_id: "team-1", team_alias: "Alpha", organization_id: "org-1" }]}
|
||||
onCancel={() => {}}
|
||||
onSubmit={onSubmitMock}
|
||||
accessToken=""
|
||||
userID=""
|
||||
userRole="Admin"
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
const teamCombobox = screen.getByRole("combobox", { name: "Team" });
|
||||
expect(teamCombobox).toHaveValue("Alpha");
|
||||
const [, teamClearButton] = screen.getAllByRole("button", { name: "Clear" });
|
||||
await userEvent.click(teamClearButton);
|
||||
await userEvent.click(screen.getByRole("button", { name: /save changes/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSubmitMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ team_id: null, organization_id: "org-1" }),
|
||||
expect.any(Array),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows the selected team alias when it is outside the visible teams", async () => {
|
||||
renderWithProviders(
|
||||
<KeyEditView
|
||||
keyData={{ ...MOCK_KEY_DATA, organization_id: "org-1", team_id: "team-9" }}
|
||||
teams={[{ team_id: "team-9", team_alias: "Gamma", organization_id: "org-2" }]}
|
||||
onCancel={() => {}}
|
||||
onSubmit={async () => {}}
|
||||
accessToken=""
|
||||
userID=""
|
||||
userRole="Admin"
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole("combobox", { name: "Team" })).toHaveValue("Gamma");
|
||||
});
|
||||
|
||||
it("carries a picked MCP server into the payload", async () => {
|
||||
const onSubmitMock = vi.fn().mockResolvedValue(undefined);
|
||||
renderForPayload(onSubmitMock);
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ 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";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
|
||||
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
||||
import { FormField } from "@/components/shared/form/FormField";
|
||||
import { SearchSelect, SearchSelectOption } from "@/components/shared/SearchSelect";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { hasCapability } from "../../utils/capabilities";
|
||||
import { isProxyAdminRole, rolesWithWriteAccess } from "../../utils/roles";
|
||||
|
|
@ -336,9 +336,6 @@ export function KeyEditView({
|
|||
if (selectedTeam?.organization_id) {
|
||||
setSelectedOrganizationId(selectedTeam.organization_id);
|
||||
form.setValue("organization_id", selectedTeam.organization_id);
|
||||
} else if (!teamId) {
|
||||
setSelectedOrganizationId(null);
|
||||
form.setValue("organization_id", null);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -360,6 +357,17 @@ export function KeyEditView({
|
|||
? teams?.filter((t) => t.organization_id === selectedOrganizationId)
|
||||
: teams;
|
||||
|
||||
const teamOptionsFor = (selectedTeamId: string | null): SearchSelectOption[] => {
|
||||
const selectedTeamOutsideFilter = teams?.find((t) => t.team_id === selectedTeamId && !visibleTeams?.includes(t));
|
||||
return [...(selectedTeamOutsideFilter ? [selectedTeamOutsideFilter] : []), ...(visibleTeams ?? [])].map(
|
||||
({ team_id, team_alias }: { team_id: string; team_alias?: string | null }) => ({
|
||||
label: team_alias || "-",
|
||||
value: team_id,
|
||||
sublabel: team_id,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<form
|
||||
|
|
@ -796,29 +804,19 @@ export function KeyEditView({
|
|||
<FormField
|
||||
control={form.control}
|
||||
name="team_id"
|
||||
label="Team ID"
|
||||
label="Team"
|
||||
description={hasProject ? "Team is locked because this key belongs to a project" : undefined}
|
||||
>
|
||||
{({ value, onChange, id }) => (
|
||||
<Select
|
||||
<SearchSelect
|
||||
options={teamOptionsFor((value as string | null) ?? null)}
|
||||
value={(value as string | null) ?? null}
|
||||
onValueChange={(teamId: string | null) => handleTeamChange(onChange, teamId)}
|
||||
onValueChange={(teamId) => handleTeamChange(onChange, teamId)}
|
||||
disabled={hasProject}
|
||||
items={Object.fromEntries(
|
||||
(visibleTeams ?? []).map((t) => [t.team_id, `${t.team_alias} (${t.team_id})`]),
|
||||
)}
|
||||
>
|
||||
<SelectTrigger id={id} className="w-full">
|
||||
<SelectValue placeholder="Select team" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{visibleTeams?.map((t) => (
|
||||
<SelectItem key={t.team_id} value={t.team_id}>
|
||||
{`${t.team_alias} (${t.team_id})`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
inputId={id}
|
||||
placeholder="Select team"
|
||||
emptyText="No teams found"
|
||||
/>
|
||||
)}
|
||||
</FormField>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue