Skip to content

Commit 3bb7975

Browse files
authored
feat: add page for ai-bridge interception logs (#20331)
Relates #20287 This pull-request introduces a basic routing for `AI Governance`'s Request Logs feature. Currently we're just pulling back the basics from the database and rendering it into the table. Nothing exciting. The idea is to extend further upon the `/aigovernance` route so it has been appropriately wrapped with a `<AIGovernanceLayout />` to introduce a navigation later.
1 parent dc21699 commit 3bb7975

File tree

17 files changed

+718
-0
lines changed

17 files changed

+718
-0
lines changed

site/src/api/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2716,6 +2716,16 @@ class ExperimentalApiMethods {
27162716
setTimeout(() => res(), 500);
27172717
});
27182718
};
2719+
2720+
getAIBridgeInterceptions = async (options: SearchParamOptions) => {
2721+
const url = getURLWithSearchParams(
2722+
"/api/experimental/aibridge/interceptions",
2723+
options,
2724+
);
2725+
const response =
2726+
await this.axios.get<TypesGen.AIBridgeListInterceptionsResponse>(url);
2727+
return response.data;
2728+
};
27192729
}
27202730

27212731
// This is a hard coded CSRF token/cookie pair for local development. In prod,

site/src/api/queries/aiBridge.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { API } from "api/api";
2+
import type { AIBridgeListInterceptionsResponse } from "api/typesGenerated";
3+
import { useFilterParamsKey } from "components/Filter/Filter";
4+
import type { UsePaginatedQueryOptions } from "hooks/usePaginatedQuery";
5+
6+
export const paginatedInterceptions = (
7+
searchParams: URLSearchParams,
8+
): UsePaginatedQueryOptions<AIBridgeListInterceptionsResponse, string> => {
9+
return {
10+
queryPayload: () => searchParams.get(useFilterParamsKey) ?? "",
11+
queryKey: ({ payload, pageNumber }) => {
12+
return ["aiBridgeInterceptions", payload, pageNumber] as const;
13+
},
14+
queryFn: ({ limit, offset, payload }) =>
15+
API.experimental.getAIBridgeInterceptions({
16+
offset,
17+
limit,
18+
q: payload,
19+
}),
20+
};
21+
};

site/src/modules/dashboard/Navbar/DeploymentDropdown.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface DeploymentDropdownProps {
1818
canViewAuditLog: boolean;
1919
canViewConnectionLog: boolean;
2020
canViewHealth: boolean;
21+
canViewAIGovernance: boolean;
2122
}
2223

2324
export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
@@ -26,6 +27,7 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
2627
canViewAuditLog,
2728
canViewConnectionLog,
2829
canViewHealth,
30+
canViewAIGovernance,
2931
}) => {
3032
if (
3133
!canViewAuditLog &&
@@ -56,6 +58,7 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
5658
canViewAuditLog={canViewAuditLog}
5759
canViewConnectionLog={canViewConnectionLog}
5860
canViewHealth={canViewHealth}
61+
canViewAIGovernance={canViewAIGovernance}
5962
/>
6063
</PopoverContent>
6164
</Popover>
@@ -68,6 +71,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
6871
canViewAuditLog,
6972
canViewHealth,
7073
canViewConnectionLog,
74+
canViewAIGovernance,
7175
}) => {
7276
return (
7377
<nav>
@@ -111,6 +115,17 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
111115
</MenuItem>
112116
</PopoverClose>
113117
)}
118+
{canViewAIGovernance && (
119+
<PopoverClose asChild>
120+
<MenuItem
121+
component={NavLink}
122+
to="/aigovernance"
123+
css={styles.menuItem}
124+
>
125+
AI Governance
126+
</MenuItem>
127+
</PopoverClose>
128+
)}
114129
{canViewHealth && (
115130
<PopoverClose asChild>
116131
<MenuItem component={NavLink} to="/health" css={styles.menuItem}>

site/src/modules/dashboard/Navbar/Navbar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ export const Navbar: FC = () => {
2525
featureVisibility.audit_log && permissions.viewAnyAuditLog;
2626
const canViewConnectionLog =
2727
featureVisibility.connection_log && permissions.viewAnyConnectionLog;
28+
const canViewAIGovernance =
29+
featureVisibility.aibridge && permissions.viewAnyAIBridgeInterception;
2830

2931
const uniqueLinks = new Map<string, LinkConfig>();
3032
for (const link of appearance.support_links ?? []) {
@@ -44,6 +46,7 @@ export const Navbar: FC = () => {
4446
canViewHealth={canViewHealth}
4547
canViewAuditLog={canViewAuditLog}
4648
canViewConnectionLog={canViewConnectionLog}
49+
canViewAIGovernance={canViewAIGovernance}
4750
proxyContextValue={proxyContextValue}
4851
/>
4952
);

site/src/modules/dashboard/Navbar/NavbarView.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ interface NavbarViewProps {
3535
canViewAuditLog: boolean;
3636
canViewConnectionLog: boolean;
3737
canViewHealth: boolean;
38+
canViewAIGovernance: boolean;
3839
proxyContextValue?: ProxyContextValue;
3940
}
4041

@@ -55,6 +56,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
5556
canViewHealth,
5657
canViewAuditLog,
5758
canViewConnectionLog,
59+
canViewAIGovernance,
5860
proxyContextValue,
5961
}) => {
6062
const webPush = useWebpushNotifications();
@@ -95,6 +97,7 @@ export const NavbarView: FC<NavbarViewProps> = ({
9597
canViewDeployment={canViewDeployment}
9698
canViewHealth={canViewHealth}
9799
canViewConnectionLog={canViewConnectionLog}
100+
canViewAIGovernance={canViewAIGovernance}
98101
/>
99102
</div>
100103

site/src/modules/permissions/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,13 @@ export const permissionChecks = {
169169
},
170170
action: "read",
171171
},
172+
viewAnyAIBridgeInterception: {
173+
object: {
174+
resource_type: "aibridge_interception",
175+
any_org: true,
176+
},
177+
action: "read",
178+
},
172179
} as const satisfies Record<string, AuthorizationCheck>;
173180

174181
export const canViewDeploymentSettings = (
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import {
2+
HelpTooltip,
3+
HelpTooltipContent,
4+
HelpTooltipIconTrigger,
5+
HelpTooltipLink,
6+
HelpTooltipLinksGroup,
7+
HelpTooltipText,
8+
HelpTooltipTitle,
9+
} from "components/HelpTooltip/HelpTooltip";
10+
import type { FC } from "react";
11+
import { docs } from "utils/docs";
12+
13+
export const AIGovernanceHelpTooltip: FC = () => {
14+
return (
15+
<HelpTooltip>
16+
<HelpTooltipIconTrigger />
17+
18+
<HelpTooltipContent>
19+
<HelpTooltipTitle>What is AI Governance?</HelpTooltipTitle>
20+
<HelpTooltipText>
21+
AI Governance is a proxy that unifies and audits LLM usage across your
22+
organization.
23+
</HelpTooltipText>
24+
<HelpTooltipLinksGroup>
25+
<HelpTooltipLink href={docs("/ai-coder/ai-bridge")}>
26+
What we track
27+
</HelpTooltipLink>
28+
</HelpTooltipLinksGroup>
29+
</HelpTooltipContent>
30+
</HelpTooltip>
31+
);
32+
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Margins } from "components/Margins/Margins";
2+
import {
3+
PageHeader,
4+
PageHeaderSubtitle,
5+
PageHeaderTitle,
6+
} from "components/PageHeader/PageHeader";
7+
import type { FC, PropsWithChildren } from "react";
8+
import { Outlet } from "react-router";
9+
import { AIGovernanceHelpTooltip } from "./AIGovernanceHelpTooltip";
10+
11+
const AIGovernanceLayout: FC<PropsWithChildren> = () => {
12+
return (
13+
<Margins className="pb-12">
14+
<PageHeader>
15+
<PageHeaderTitle>
16+
<div className="flex items-center gap-2">
17+
<span>AI Governance</span>
18+
<AIGovernanceHelpTooltip />
19+
</div>
20+
</PageHeaderTitle>
21+
<PageHeaderSubtitle>
22+
Manage usage for your organization.
23+
</PageHeaderSubtitle>
24+
</PageHeader>
25+
<Outlet />
26+
</Margins>
27+
);
28+
};
29+
30+
export default AIGovernanceLayout;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { paginatedInterceptions } from "api/queries/aiBridge";
2+
import { useFilter } from "components/Filter/Filter";
3+
import { useUserFilterMenu } from "components/Filter/UserFilter";
4+
import { usePaginatedQuery } from "hooks/usePaginatedQuery";
5+
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
6+
import type { FC } from "react";
7+
import { useSearchParams } from "react-router";
8+
import { pageTitle } from "utils/page";
9+
import { useProviderFilterMenu } from "./filter/filter";
10+
import { RequestLogsPageView } from "./RequestLogsPageView";
11+
12+
const RequestLogsPage: FC = () => {
13+
const feats = useFeatureVisibility();
14+
const isRequestLogsVisible = Boolean(feats.aibridge);
15+
16+
const [searchParams, setSearchParams] = useSearchParams();
17+
const interceptionsQuery = usePaginatedQuery(
18+
paginatedInterceptions(searchParams),
19+
);
20+
const filter = useFilter({
21+
searchParams,
22+
onSearchParamsChange: setSearchParams,
23+
onUpdate: interceptionsQuery.goToFirstPage,
24+
});
25+
26+
const userMenu = useUserFilterMenu({
27+
value: filter.values.initiator,
28+
onChange: (option) =>
29+
filter.update({
30+
...filter.values,
31+
initiator: option?.value,
32+
}),
33+
});
34+
35+
const providerMenu = useProviderFilterMenu({
36+
value: filter.values.provider,
37+
onChange: (option) =>
38+
filter.update({
39+
...filter.values,
40+
provider: option?.value,
41+
}),
42+
});
43+
44+
return (
45+
<>
46+
<title>{pageTitle("Request Logs", "AI Governance")}</title>
47+
48+
<RequestLogsPageView
49+
isLoading={interceptionsQuery.isLoading}
50+
isRequestLogsVisible={isRequestLogsVisible}
51+
interceptions={interceptionsQuery.data?.results}
52+
interceptionsQuery={interceptionsQuery}
53+
filterProps={{
54+
filter,
55+
error: interceptionsQuery.error,
56+
menus: {
57+
user: userMenu,
58+
provider: providerMenu,
59+
},
60+
}}
61+
/>
62+
</>
63+
);
64+
};
65+
66+
export default RequestLogsPage;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { MockInterception } from "testHelpers/entities";
2+
import type { Meta, StoryObj } from "@storybook/react-vite";
3+
import {
4+
getDefaultFilterProps,
5+
MockMenu,
6+
} from "components/Filter/storyHelpers";
7+
import {
8+
mockInitialRenderResult,
9+
mockSuccessResult,
10+
} from "components/PaginationWidget/PaginationContainer.mocks";
11+
import type { ComponentProps } from "react";
12+
import { RequestLogsPageView } from "./RequestLogsPageView";
13+
14+
type FilterProps = ComponentProps<typeof RequestLogsPageView>["filterProps"];
15+
16+
const defaultFilterProps = getDefaultFilterProps<FilterProps>({
17+
query: "owner:me",
18+
values: {
19+
username: undefined,
20+
provider: undefined,
21+
},
22+
menus: {
23+
user: MockMenu,
24+
provider: MockMenu,
25+
},
26+
});
27+
28+
const interceptions = [MockInterception, MockInterception, MockInterception];
29+
30+
const meta: Meta<typeof RequestLogsPageView> = {
31+
title: "pages/AIGovernancePage/RequestLogsPageView",
32+
component: RequestLogsPageView,
33+
args: {},
34+
};
35+
36+
export default meta;
37+
type Story = StoryObj<typeof RequestLogsPageView>;
38+
39+
export const Paywall: Story = {
40+
args: {
41+
isRequestLogsVisible: false,
42+
},
43+
};
44+
45+
export const Loaded: Story = {
46+
args: {
47+
isRequestLogsVisible: true,
48+
interceptions,
49+
filterProps: {
50+
...defaultFilterProps,
51+
},
52+
interceptionsQuery: mockSuccessResult,
53+
},
54+
};
55+
56+
export const Empty: Story = {
57+
args: {
58+
isRequestLogsVisible: true,
59+
interceptions: [],
60+
filterProps: {
61+
...defaultFilterProps,
62+
},
63+
interceptionsQuery: mockSuccessResult,
64+
},
65+
};
66+
67+
export const Loading: Story = {
68+
args: {
69+
isLoading: true,
70+
isRequestLogsVisible: true,
71+
interceptions: [],
72+
filterProps: {
73+
...defaultFilterProps,
74+
},
75+
interceptionsQuery: mockInitialRenderResult,
76+
},
77+
};

0 commit comments

Comments
 (0)