feat: add network call badges to AI sessions table (#27341)

Surface the total and blocked Agent Firewall network calls on the AI
sessions list. Sessions that did not pass through Agent Firewall show as
"Disabled".

<img width="2842" height="1366" alt="image"
src="https://github.com/user-attachments/assets/2a68b4a9-4d93-454d-a06e-5f0d0b734a33"
/>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sas Swart
2026-07-22 14:16:31 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 507ce504f8
commit f17d488479
8 changed files with 197 additions and 1 deletions
+6 -1
View File
@@ -49,7 +49,12 @@ not just what was called.
The sessions page (`http://<deployment-url>/ai-gateway/sessions`) lists all sessions in
reverse-chronological order. Each row shows the last prompt, initiator, provider,
client, token usage, thread count, and timestamp.
client, token usage, network calls, thread count, and timestamp.
The network calls column reports the total and blocked
[Agent Firewall](../agent-firewall/index.md) calls for the session. It shows
`No activity` when the session made no calls, and `Disabled` when the session
did not pass through Agent Firewall, so no network calls were monitored.
Select one to view its full details.
@@ -106,6 +106,13 @@ export const MultipleSessions: Story = {
cache_read_input_tokens: 800 * (i + 1),
cache_write_input_tokens: 50 * (i + 1),
},
network_calls: [
{ total: 23, blocked: 2 },
{ total: 5, blocked: 1 },
{ total: 0, blocked: 0 },
undefined,
{ total: 150, blocked: 0 },
][i],
})),
},
};
@@ -86,6 +86,7 @@ export const ListSessionsPageView: FC<ListSessionsPageViewProps> = ({
<TableHead className="text-nowrap">Provider</TableHead>
<TableHead className="text-nowrap">Client</TableHead>
<TableHead className="text-nowrap">In/Out Tokens</TableHead>
<TableHead className="text-nowrap">Network Calls</TableHead>
<TableHead className="flex items-center flex-nowrap gap-1">
Threads
<ThreadTooltip>
@@ -107,3 +107,27 @@ export const LargeTokenCounts: Story = {
},
},
};
export const NetworkCallsBlocked: Story = {
args: {
session: {
...MockSession,
network_calls: { total: 23, blocked: 2 },
},
},
};
export const NoNetworkActivity: Story = {
args: {
session: {
...MockSession,
network_calls: { total: 0, blocked: 0 },
},
},
};
export const NetworkDisabled: Story = {
args: {
session: { ...MockSession, network_calls: undefined },
},
};
@@ -13,6 +13,7 @@ import {
import { AIBridgeClientIcon } from "#/pages/AIBridgePage/icons/AIBridgeClientIcon";
import { AIBridgeProviderIcon } from "#/pages/AIBridgePage/icons/AIBridgeProviderIcon";
import { DATE_FORMAT, formatDateTime } from "#/utils/time";
import { NetworkCallBadges } from "../NetworkCallBadges";
import { TokenBadges } from "../TokenBadges";
import { getProviderDisplayName } from "../utils";
@@ -105,6 +106,9 @@ export const ListSessionsRow: FC<ListSessionsRowProps> = ({
/>
</div>
</TableCell>
<TableCell className="w-40">
<NetworkCallBadges summary={session.network_calls} />
</TableCell>
<TableCell className="w-32">
<Badge className="bg-surface-secondary align-end">
{session.threads}
@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, screen, userEvent, waitFor } from "storybook/test";
import { NetworkCallBadges } from "./NetworkCallBadges";
const meta: Meta<typeof NetworkCallBadges> = {
title: "pages/AIBridgePage/NetworkCallBadges",
component: NetworkCallBadges,
};
export default meta;
type Story = StoryObj<typeof NetworkCallBadges>;
export const TotalAndBlocked: Story = {
args: {
summary: { total: 23, blocked: 2 },
},
};
export const NoBlocked: Story = {
args: {
summary: { total: 23, blocked: 0 },
},
};
export const NoActivity: Story = {
args: {
summary: { total: 0, blocked: 0 },
},
};
export const Disabled: Story = {
args: {
summary: undefined,
},
};
export const LargeCounts: Story = {
args: {
summary: { total: 12_480, blocked: 320 },
},
};
// Tabbing to the badges reveals the breakdown tooltip without a mouse.
export const TotalAndBlockedKeyboard: Story = {
args: {
summary: { total: 23, blocked: 2 },
},
play: async () => {
await userEvent.tab();
await waitFor(() => {
const tooltip = screen.getByRole("tooltip");
expect(tooltip).toHaveTextContent("Total calls");
expect(tooltip).toHaveTextContent("Blocked");
});
},
};
// Tabbing to the disabled indicator's info button and pressing Enter reveals
// the reason popover without a mouse.
export const DisabledKeyboard: Story = {
args: {
summary: undefined,
},
play: async () => {
await userEvent.tab();
await userEvent.keyboard("{Enter}");
await waitFor(() =>
expect(screen.getByRole("dialog")).toHaveTextContent(
"Network call monitoring was not active for this session.",
),
);
},
};
@@ -0,0 +1,78 @@
import { BanIcon } from "lucide-react";
import type { FC } from "react";
import type { AIBridgeSessionNetworkCallSummary } from "#/api/typesGenerated";
import { Badge } from "#/components/Badge/Badge";
import { InfoTooltip } from "#/components/InfoTooltip/InfoTooltip";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
interface NetworkCallBadgesProps {
// summary is undefined when network call monitoring was not active for the
// session, which renders as "Disabled".
summary: AIBridgeSessionNetworkCallSummary | undefined;
}
export const NetworkCallBadges: FC<NetworkCallBadgesProps> = ({ summary }) => {
if (!summary) {
return (
<span className="inline-flex items-center gap-1 whitespace-nowrap text-content-secondary">
Disabled
<InfoTooltip message="Network call monitoring was not active for this session." />
</span>
);
}
if (summary.total === 0) {
return (
<span className="whitespace-nowrap text-content-secondary">
No activity
</span>
);
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
aria-label="More info"
className="flex items-center whitespace-nowrap border-0 bg-transparent p-0 text-inherit"
>
<Badge size="sm" className="rounded-e-none">
{summary.total.toLocaleString("en-US")}
</Badge>
<Badge
size="sm"
svgSize="xs"
className="gap-0 bg-surface-tertiary rounded-s-none text-content-warning"
>
<BanIcon className="flex-shrink-0" />
{summary.blocked.toLocaleString("en-US")}
</Badge>
</button>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="text-sm font-normal"
>
<div className="flex flex-col gap-1">
<div className="flex items-center justify-between gap-4">
<span className="text-content-secondary">Total calls</span>
<span>{summary.total.toLocaleString("en-US")}</span>
</div>
<div className="flex items-center justify-between gap-4">
<span className="text-content-secondary">Blocked</span>
<span>{summary.blocked.toLocaleString("en-US")}</span>
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
+4
View File
@@ -5523,6 +5523,10 @@ export const MockSession: TypesGen.AIBridgeSession = {
cache_read_input_tokens: 980,
cache_write_input_tokens: 120,
},
network_calls: {
total: 23,
blocked: 2,
},
last_prompt: "But *can* I really fix it?",
last_active_at: "2026-03-09T10:28:15.03152Z",
};