mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(ui): name the granting access group on hover instead of an Inherited tag
`/team/info` access_group_details now carries mcp_server_ids and agent_ids per group next to models, so the dashboard can say which group granted a server or agent. The Object Permissions rows drop the Inherited badge and the row tooltip reads "Granted via access group <name>. Full ID: <id>", listing every group when more than one grants the same id and falling back to "an access group" when the proxy did not say. Claude-Session: https://claude.ai/code/session_01QvQzYztinxj8ZuD5YxbVdL
This commit is contained in:
parent
944d018c72
commit
2fd6e19051
13 changed files with 204 additions and 67 deletions
|
|
@ -4172,6 +4172,8 @@ class TeamAccessGroupModelGrant(LiteLLMPydanticObjectBase):
|
|||
access_group_id: str
|
||||
access_group_name: str
|
||||
models: tuple[str, ...]
|
||||
mcp_server_ids: tuple[str, ...] = ()
|
||||
agent_ids: tuple[str, ...] = ()
|
||||
|
||||
|
||||
class TeamInfoResponseObjectTeamTable(LiteLLM_TeamTable):
|
||||
|
|
|
|||
|
|
@ -4318,6 +4318,8 @@ async def _resolve_team_access_group_resources(
|
|||
access_group_id=group.access_group_id,
|
||||
access_group_name=group.access_group_name,
|
||||
models=tuple(group.access_model_names or ()),
|
||||
mcp_server_ids=tuple(group.access_mcp_server_ids or ()),
|
||||
agent_ids=tuple(group.access_agent_ids or ()),
|
||||
)
|
||||
for group in resolved_groups
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9897,11 +9897,11 @@ class TestResolveTeamAccessGroupResources:
|
|||
assert resolved.access_group_mcp_server_ids == ["mcp-1"]
|
||||
assert resolved.access_group_agent_ids == ["agent-1"]
|
||||
assert [
|
||||
(d.access_group_id, d.access_group_name, d.models)
|
||||
(d.access_group_id, d.access_group_name, d.models, d.mcp_server_ids, d.agent_ids)
|
||||
for d in (resolved.access_group_details or [])
|
||||
] == [
|
||||
("ag-1", "shared-models", ("gpt-4", "claude-3")),
|
||||
("ag-2", "extra-models", ("claude-3", "gemini")),
|
||||
("ag-1", "shared-models", ("gpt-4", "claude-3"), ("mcp-1",), ()),
|
||||
("ag-2", "extra-models", ("claude-3", "gemini"), (), ("agent-1",)),
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -3,11 +3,12 @@ import VectorStorePermissions from "./permissions/VectorStorePermissions";
|
|||
import MCPServerPermissions from "./permissions/MCPServerPermissions";
|
||||
import AgentPermissions from "./permissions/AgentPermissions";
|
||||
import type { ObjectPermission } from "./object_permission_types";
|
||||
import type { InheritedGrant } from "./permissions/inheritedGrants";
|
||||
|
||||
interface ObjectPermissionsViewProps {
|
||||
objectPermission?: ObjectPermission | null;
|
||||
inheritedMcpServerIds?: string[];
|
||||
inheritedAgentIds?: string[];
|
||||
inheritedMcpServers?: InheritedGrant[];
|
||||
inheritedAgents?: InheritedGrant[];
|
||||
variant?: "card" | "inline";
|
||||
className?: string;
|
||||
accessToken?: string | null;
|
||||
|
|
@ -15,8 +16,8 @@ interface ObjectPermissionsViewProps {
|
|||
|
||||
export function ObjectPermissionsView({
|
||||
objectPermission,
|
||||
inheritedMcpServerIds = [],
|
||||
inheritedAgentIds = [],
|
||||
inheritedMcpServers = [],
|
||||
inheritedAgents = [],
|
||||
variant = "card",
|
||||
className = "",
|
||||
accessToken,
|
||||
|
|
@ -38,13 +39,13 @@ export function ObjectPermissionsView({
|
|||
mcpAccessGroups={mcpAccessGroups}
|
||||
mcpToolPermissions={mcpToolPermissions}
|
||||
mcpToolsets={mcpToolsets}
|
||||
inheritedMcpServers={inheritedMcpServerIds}
|
||||
inheritedMcpServers={inheritedMcpServers}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
<AgentPermissions
|
||||
agents={agents}
|
||||
agentAccessGroups={agentAccessGroups}
|
||||
inheritedAgents={inheritedAgentIds}
|
||||
inheritedAgents={inheritedAgents}
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
<div className="min-w-0 rounded-md border border-border p-4">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import AgentPermissions from "./AgentPermissions";
|
||||
import * as networking from "../networking";
|
||||
|
||||
|
|
@ -13,31 +14,51 @@ describe("AgentPermissions", () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("lists agents inherited from access groups with an Inherited tag and counts them", async () => {
|
||||
it("lists agents inherited from access groups, counts them, and names the groups on hover", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.getAgentsList).mockResolvedValue({
|
||||
agents: [{ agent_id: agentId, agent_name: "support_agent" }],
|
||||
});
|
||||
|
||||
render(<AgentPermissions agents={[]} inheritedAgents={[agentId]} accessToken={accessToken} />);
|
||||
render(
|
||||
<AgentPermissions
|
||||
agents={[]}
|
||||
inheritedAgents={[{ id: agentId, accessGroupNames: ["platform-tools", "support"] }]}
|
||||
accessToken={accessToken}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/support_agent/)).toBeInTheDocument();
|
||||
expect(screen.getByText("Inherited")).toBeInTheDocument();
|
||||
const row = await screen.findByText(/support_agent/);
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("No agents or access groups configured")).not.toBeInTheDocument();
|
||||
expect(networking.getAgentsList).toHaveBeenCalledWith(accessToken);
|
||||
|
||||
await user.hover(row);
|
||||
expect(
|
||||
await screen.findByText(`Granted via access groups platform-tools, support. Full ID: ${agentId}`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not double-list an agent that is both granted directly and inherited", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.getAgentsList).mockResolvedValue({
|
||||
agents: [{ agent_id: agentId, agent_name: "support_agent" }],
|
||||
});
|
||||
|
||||
render(<AgentPermissions agents={[agentId]} inheritedAgents={[agentId]} accessToken={accessToken} />);
|
||||
render(
|
||||
<AgentPermissions
|
||||
agents={[agentId]}
|
||||
inheritedAgents={[{ id: agentId, accessGroupNames: ["support"] }]}
|
||||
accessToken={accessToken}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/support_agent/)).toBeInTheDocument();
|
||||
const row = await screen.findByText(/support_agent/);
|
||||
expect(screen.getAllByText(/support_agent/)).toHaveLength(1);
|
||||
expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
|
||||
await user.hover(row);
|
||||
expect(await screen.findByText(`Full ID: ${agentId}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the empty state when nothing is granted directly or inherited", () => {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { UserGroupIcon } from "@heroicons/react/outline";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { getAgentsList } from "../networking";
|
||||
import { InheritedGrant, inheritedGrantTooltip } from "./inheritedGrants";
|
||||
|
||||
interface Agent {
|
||||
agent_id: string;
|
||||
|
|
@ -14,12 +15,10 @@ interface Agent {
|
|||
interface AgentPermissionsProps {
|
||||
agents: string[];
|
||||
agentAccessGroups?: string[];
|
||||
inheritedAgents?: string[];
|
||||
inheritedAgents?: InheritedGrant[];
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
const INHERITED_AGENT_TOOLTIP = "Granted through one of the team's access groups";
|
||||
|
||||
export function AgentPermissions({
|
||||
agents,
|
||||
agentAccessGroups = [],
|
||||
|
|
@ -27,7 +26,7 @@ export function AgentPermissions({
|
|||
accessToken,
|
||||
}: AgentPermissionsProps) {
|
||||
const [agentDetails, setAgentDetails] = useState<Agent[]>([]);
|
||||
const inheritedOnlyAgents = inheritedAgents.filter((agent) => !agents.includes(agent));
|
||||
const inheritedOnlyAgents = inheritedAgents.filter((grant) => !agents.includes(grant.id));
|
||||
const agentIdCount = agents.length + inheritedOnlyAgents.length;
|
||||
|
||||
// Fetch agent details when component mounts
|
||||
|
|
@ -58,9 +57,9 @@ export function AgentPermissions({
|
|||
};
|
||||
|
||||
const mergedItems = [
|
||||
...agents.map((agent) => ({ type: "agent", value: agent, inherited: false })),
|
||||
...inheritedOnlyAgents.map((agent) => ({ type: "agent", value: agent, inherited: true })),
|
||||
...agentAccessGroups.map((group) => ({ type: "accessGroup", value: group, inherited: false })),
|
||||
...agents.map((agent) => ({ type: "agent", value: agent, tooltip: `Full ID: ${agent}` })),
|
||||
...inheritedOnlyAgents.map((grant) => ({ type: "agent", value: grant.id, tooltip: inheritedGrantTooltip(grant) })),
|
||||
...agentAccessGroups.map((group) => ({ type: "accessGroup", value: group, tooltip: "" })),
|
||||
];
|
||||
const totalCount = mergedItems.length;
|
||||
|
||||
|
|
@ -86,17 +85,8 @@ export function AgentPermissions({
|
|||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{getAgentDisplayName(item.value)}
|
||||
</span>
|
||||
{item.inherited && (
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-purple-600 bg-purple-50 border border-purple-200 rounded-sm uppercase tracking-wide shrink-0 dark:text-purple-300 dark:bg-purple-950 dark:border-purple-800">
|
||||
Inherited
|
||||
</span>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{item.inherited
|
||||
? `${INHERITED_AGENT_TOOLTIP}. Full ID: ${item.value}`
|
||||
: `Full ID: ${item.value}`}
|
||||
</TooltipContent>
|
||||
<TooltipContent>{item.tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -407,7 +407,8 @@ describe("MCPServerPermissions", () => {
|
|||
await waitFor(() => expect(screen.getByText("Blocked")).toHaveAttribute("data-variant", "destructive"));
|
||||
});
|
||||
|
||||
it("lists servers inherited from access groups with an Inherited tag and counts them", async () => {
|
||||
it("lists servers inherited from access groups, counts them, and names the group on hover", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([
|
||||
{ server_id: mockServerId1, server_name: mockServerName1, alias: mockServerName1 },
|
||||
]);
|
||||
|
|
@ -417,19 +418,24 @@ describe("MCPServerPermissions", () => {
|
|||
mcpServers={[]}
|
||||
mcpAccessGroups={[]}
|
||||
mcpToolPermissions={{}}
|
||||
inheritedMcpServers={[mockServerId1]}
|
||||
inheritedMcpServers={[{ id: mockServerId1, accessGroupNames: ["platform-tools"] }]}
|
||||
accessToken={mockAccessToken}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/DW_MCP/)).toBeInTheDocument();
|
||||
expect(screen.getByText("Inherited")).toBeInTheDocument();
|
||||
const row = await screen.findByText(/DW_MCP/);
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("No MCP servers, access groups, or toolsets configured")).not.toBeInTheDocument();
|
||||
expect(networking.fetchMCPServers).toHaveBeenCalledWith(mockAccessToken);
|
||||
|
||||
await user.hover(row);
|
||||
expect(
|
||||
await screen.findByText(`Granted via access group platform-tools. Full ID: ${mockServerId1}`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not double-list a server that is both granted directly and inherited", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([
|
||||
{ server_id: mockServerId2, server_name: mockServerName2, alias: mockServerName2 },
|
||||
]);
|
||||
|
|
@ -439,14 +445,16 @@ describe("MCPServerPermissions", () => {
|
|||
mcpServers={[mockServerId2]}
|
||||
mcpAccessGroups={[]}
|
||||
mcpToolPermissions={{}}
|
||||
inheritedMcpServers={[mockServerId2]}
|
||||
inheritedMcpServers={[{ id: mockServerId2, accessGroupNames: ["platform-tools"] }]}
|
||||
accessToken={mockAccessToken}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText(/Test Server/)).toBeInTheDocument();
|
||||
const row = await screen.findByText(/Test Server/);
|
||||
expect(screen.getAllByText(/Test Server/)).toHaveLength(1);
|
||||
expect(screen.queryByText("Inherited")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("1")).toBeInTheDocument();
|
||||
|
||||
await user.hover(row);
|
||||
expect(await screen.findByText(`Full ID: ${mockServerId2}`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,18 +5,17 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
|
|||
import { fetchMCPServers, fetchMCPToolsets } from "../networking";
|
||||
import { MCPServer, MCPToolset } from "../mcp_tools/types";
|
||||
import { ALL_PROXY_MCP_SERVERS_SENTINEL, NO_MCP_SERVERS_SENTINEL } from "../mcp_tools/constants";
|
||||
import { InheritedGrant, inheritedGrantTooltip } from "./inheritedGrants";
|
||||
|
||||
interface MCPServerPermissionsProps {
|
||||
mcpServers: string[];
|
||||
mcpAccessGroups?: string[];
|
||||
mcpToolPermissions?: Record<string, string[]>;
|
||||
mcpToolsets?: string[];
|
||||
inheritedMcpServers?: string[];
|
||||
inheritedMcpServers?: InheritedGrant[];
|
||||
accessToken?: string | null;
|
||||
}
|
||||
|
||||
const INHERITED_MCP_SERVER_TOOLTIP = "Granted through one of the team's access groups";
|
||||
|
||||
export function MCPServerPermissions({
|
||||
mcpServers,
|
||||
mcpAccessGroups = [],
|
||||
|
|
@ -57,8 +56,8 @@ export function MCPServerPermissions({
|
|||
const directServerIds = mcpServers.filter(
|
||||
(server) => server !== NO_MCP_SERVERS_SENTINEL && server !== ALL_PROXY_MCP_SERVERS_SENTINEL,
|
||||
);
|
||||
const inheritedOnlyServerIds = inheritedMcpServers.filter((server) => !mcpServers.includes(server));
|
||||
const serverIdCount = directServerIds.length + inheritedOnlyServerIds.length;
|
||||
const inheritedOnlyServers = inheritedMcpServers.filter((grant) => !mcpServers.includes(grant.id));
|
||||
const serverIdCount = directServerIds.length + inheritedOnlyServers.length;
|
||||
|
||||
// Fetch MCP server details when component mounts
|
||||
useEffect(() => {
|
||||
|
|
@ -109,9 +108,13 @@ export function MCPServerPermissions({
|
|||
const grantsAllProxyMcpServers = mcpServers.includes(ALL_PROXY_MCP_SERVERS_SENTINEL);
|
||||
|
||||
const mergedItems = [
|
||||
...directServerIds.map((server) => ({ type: "server", value: server, inherited: false })),
|
||||
...inheritedOnlyServerIds.map((server) => ({ type: "server", value: server, inherited: true })),
|
||||
...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group, inherited: false })),
|
||||
...directServerIds.map((server) => ({ type: "server", value: server, tooltip: `Full ID: ${server}` })),
|
||||
...inheritedOnlyServers.map((grant) => ({
|
||||
type: "server",
|
||||
value: grant.id,
|
||||
tooltip: inheritedGrantTooltip(grant),
|
||||
})),
|
||||
...mcpAccessGroups.map((group) => ({ type: "accessGroup", value: group, tooltip: "" })),
|
||||
];
|
||||
const totalCount = mergedItems.length + mcpToolsets.length;
|
||||
|
||||
|
|
@ -160,17 +163,8 @@ export function MCPServerPermissions({
|
|||
<span className="text-sm font-medium text-foreground truncate">
|
||||
{getMCPServerDisplayName(item.value)}
|
||||
</span>
|
||||
{item.inherited && (
|
||||
<span className="ml-1 px-1.5 py-0.5 text-[9px] font-semibold text-info bg-info/10 border border-info/20 rounded-sm uppercase tracking-wide shrink-0">
|
||||
Inherited
|
||||
</span>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{item.inherited
|
||||
? `${INHERITED_MCP_SERVER_TOOLTIP}. Full ID: ${item.value}`
|
||||
: `Full ID: ${item.value}`}
|
||||
</TooltipContent>
|
||||
<TooltipContent>{item.tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 min-w-0">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { computeInheritedGrants, inheritedGrantTooltip } from "./inheritedGrants";
|
||||
import { TeamAccessGroupModelGrant } from "../team/teamModelAccess";
|
||||
|
||||
const GRANTS: TeamAccessGroupModelGrant[] = [
|
||||
{ access_group_id: "ag-1", access_group_name: "platform-tools", models: [], mcp_server_ids: ["mcp-1", "mcp-2"] },
|
||||
{
|
||||
access_group_id: "ag-2",
|
||||
access_group_name: "support",
|
||||
models: [],
|
||||
mcp_server_ids: ["mcp-2"],
|
||||
agent_ids: ["agent-1"],
|
||||
},
|
||||
];
|
||||
|
||||
describe("computeInheritedGrants", () => {
|
||||
it("attributes each id to every group that grants it, in group order", () => {
|
||||
expect(computeInheritedGrants(["mcp-1", "mcp-2"], GRANTS, (g) => g.mcp_server_ids)).toEqual([
|
||||
{ id: "mcp-1", accessGroupNames: ["platform-tools"] },
|
||||
{ id: "mcp-2", accessGroupNames: ["platform-tools", "support"] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps ids the flat list carries but no group detail explains, with no group names", () => {
|
||||
expect(computeInheritedGrants(["agent-1", "agent-legacy"], GRANTS, (g) => g.agent_ids)).toEqual([
|
||||
{ id: "agent-1", accessGroupNames: ["support"] },
|
||||
{ id: "agent-legacy", accessGroupNames: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to the group details when the flat list is missing, without duplicates", () => {
|
||||
expect(computeInheritedGrants(undefined, GRANTS, (g) => g.mcp_server_ids).map((g) => g.id)).toEqual([
|
||||
"mcp-1",
|
||||
"mcp-2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns nothing when neither source has ids", () => {
|
||||
expect(computeInheritedGrants(undefined, undefined, (g) => g.agent_ids)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("inheritedGrantTooltip", () => {
|
||||
it("names a single group", () => {
|
||||
expect(inheritedGrantTooltip({ id: "mcp-1", accessGroupNames: ["platform-tools"] })).toBe(
|
||||
"Granted via access group platform-tools. Full ID: mcp-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("lists several groups", () => {
|
||||
expect(inheritedGrantTooltip({ id: "mcp-2", accessGroupNames: ["platform-tools", "support"] })).toBe(
|
||||
"Granted via access groups platform-tools, support. Full ID: mcp-2",
|
||||
);
|
||||
});
|
||||
|
||||
it("stays generic when the proxy did not say which group granted it", () => {
|
||||
expect(inheritedGrantTooltip({ id: "agent-legacy", accessGroupNames: [] })).toBe(
|
||||
"Granted via an access group. Full ID: agent-legacy",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { describeGroups, TeamAccessGroupModelGrant } from "../team/teamModelAccess";
|
||||
|
||||
export interface InheritedGrant {
|
||||
id: string;
|
||||
accessGroupNames: string[];
|
||||
}
|
||||
|
||||
export function computeInheritedGrants(
|
||||
ids: string[] | undefined,
|
||||
grants: TeamAccessGroupModelGrant[] | undefined,
|
||||
idsOf: (grant: TeamAccessGroupModelGrant) => string[] | undefined,
|
||||
): InheritedGrant[] {
|
||||
const known = grants ?? [];
|
||||
const allIds = [...new Set([...(ids ?? []), ...known.flatMap((grant) => idsOf(grant) ?? [])])];
|
||||
return allIds.map((id) => ({
|
||||
id,
|
||||
accessGroupNames: known
|
||||
.filter((grant) => (idsOf(grant) ?? []).includes(id))
|
||||
.map((grant) => grant.access_group_name),
|
||||
}));
|
||||
}
|
||||
|
||||
export const inheritedGrantTooltip = (grant: InheritedGrant): string => {
|
||||
const source = grant.accessGroupNames.length > 0 ? describeGroups(grant.accessGroupNames) : "an access group";
|
||||
return `Granted via ${source}. Full ID: ${grant.id}`;
|
||||
};
|
||||
|
|
@ -305,7 +305,8 @@ describe("TeamInfoView", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("shows MCP servers and agents inherited from access groups in the Object Permissions card", async () => {
|
||||
it("shows MCP servers and agents inherited from access groups in the Object Permissions card, naming the group on hover", async () => {
|
||||
const user = userEvent.setup();
|
||||
vi.mocked(networking.fetchMCPServers).mockResolvedValue([
|
||||
{ server_id: "mcp-github-1234", server_name: "github", alias: "github" },
|
||||
]);
|
||||
|
|
@ -318,16 +319,33 @@ describe("TeamInfoView", () => {
|
|||
access_group_ids: ["ag-1"],
|
||||
access_group_mcp_server_ids: ["mcp-github-1234"],
|
||||
access_group_agent_ids: ["agent-support-5678"],
|
||||
access_group_details: [
|
||||
{
|
||||
access_group_id: "ag-1",
|
||||
access_group_name: "platform-tools",
|
||||
models: [],
|
||||
mcp_server_ids: ["mcp-github-1234"],
|
||||
agent_ids: ["agent-support-5678"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
renderWithProviders(<TeamInfoView {...defaultProps} />);
|
||||
|
||||
expect(await screen.findByText(/github \(mcp\.\.\.1234\)/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/support_agent \(age\.\.\.5678\)/)).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Inherited")).toHaveLength(2);
|
||||
const serverRow = await screen.findByText(/github \(mcp\.\.\.1234\)/);
|
||||
const agentRow = await screen.findByText(/support_agent \(age\.\.\.5678\)/);
|
||||
expect(screen.queryByText("No MCP servers, access groups, or toolsets configured")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("No agents or access groups configured")).not.toBeInTheDocument();
|
||||
|
||||
await user.hover(serverRow);
|
||||
expect(
|
||||
await screen.findByText("Granted via access group platform-tools. Full ID: mcp-github-1234"),
|
||||
).toBeInTheDocument();
|
||||
await user.hover(agentRow);
|
||||
expect(
|
||||
await screen.findByText("Granted via access group platform-tools. Full ID: agent-support-5678"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the all-proxy-models badge non-clickable", async () => {
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ import {
|
|||
TeamModelBadge,
|
||||
TeamModelBadgeKind,
|
||||
} from "./teamModelAccess";
|
||||
import { computeInheritedGrants } from "../permissions/inheritedGrants";
|
||||
import MetadataKeyValueFields, {
|
||||
metadataObjectToPairs,
|
||||
metadataPairsSchema,
|
||||
|
|
@ -936,6 +937,17 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
|
||||
const { team_info: info } = teamData;
|
||||
|
||||
const inheritedMcpServers = computeInheritedGrants(
|
||||
info.access_group_mcp_server_ids,
|
||||
info.access_group_details,
|
||||
(grant) => grant.mcp_server_ids,
|
||||
);
|
||||
const inheritedAgents = computeInheritedGrants(
|
||||
info.access_group_agent_ids,
|
||||
info.access_group_details,
|
||||
(grant) => grant.agent_ids,
|
||||
);
|
||||
|
||||
const initialKillSwitchOn = info.metadata?.disable_global_guardrails === true;
|
||||
|
||||
const allGuardrails: GuardrailListItem[] = guardrailsData?.guardrails ?? [];
|
||||
|
|
@ -1035,8 +1047,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
|
||||
<ObjectPermissionsView
|
||||
objectPermission={info.object_permission}
|
||||
inheritedMcpServerIds={info.access_group_mcp_server_ids}
|
||||
inheritedAgentIds={info.access_group_agent_ids}
|
||||
inheritedMcpServers={inheritedMcpServers}
|
||||
inheritedAgents={inheritedAgents}
|
||||
variant="card"
|
||||
accessToken={accessToken}
|
||||
/>
|
||||
|
|
@ -1889,8 +1901,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
|
||||
<ObjectPermissionsView
|
||||
objectPermission={info.object_permission}
|
||||
inheritedMcpServerIds={info.access_group_mcp_server_ids}
|
||||
inheritedAgentIds={info.access_group_agent_ids}
|
||||
inheritedMcpServers={inheritedMcpServers}
|
||||
inheritedAgents={inheritedAgents}
|
||||
variant="inline"
|
||||
className="pt-4 border-t border-border"
|
||||
accessToken={accessToken}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ export interface TeamAccessGroupModelGrant {
|
|||
access_group_id: string;
|
||||
access_group_name: string;
|
||||
models: string[];
|
||||
mcp_server_ids?: string[];
|
||||
agent_ids?: string[];
|
||||
}
|
||||
|
||||
export type TeamModelBadgeKind = "all-proxy" | "no-default" | "direct" | "access-group";
|
||||
|
|
@ -19,7 +21,7 @@ export function normalizeTeamModelSelection(models: string[] | undefined): strin
|
|||
return models && models.length > 0 ? models : [NO_DEFAULT_MODELS];
|
||||
}
|
||||
|
||||
const describeGroups = (names: string[]): string =>
|
||||
export const describeGroups = (names: string[]): string =>
|
||||
names.length > 1 ? `access groups ${names.join(", ")}` : `access group ${names[0]}`;
|
||||
|
||||
export function computeTeamModelBadges(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue