feat(ui): add an Internal User filter to the request logs page

The logs page could filter by end user but never by the users the proxy knows about, so admins had no way to narrow logs to a person by email.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
shivam 2026-08-08 20:45:44 +00:00
parent 97a59c8c90
commit 1eee8ce382
5 changed files with 130 additions and 6 deletions

View file

@ -18,9 +18,14 @@ vi.mock("@/app/(dashboard)/hooks/spendLogs/useSpendLogEndUsers", () => ({
useInfiniteSpendLogEndUsers: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/users/useUsers", () => ({
useInfiniteUsers: vi.fn(),
}));
import { useInfiniteSpendLogEndUsers } from "@/app/(dashboard)/hooks/spendLogs/useSpendLogEndUsers";
import { useInfiniteKeyAliases } from "@/app/(dashboard)/hooks/keys/useKeyAliases";
import { useInfiniteModelInfo } from "@/app/(dashboard)/hooks/models/useModels";
import { useInfiniteUsers } from "@/app/(dashboard)/hooks/users/useUsers";
const emptyInfiniteQuery = {
data: { pages: [], pageParams: [] },
@ -32,10 +37,16 @@ const emptyInfiniteQuery = {
const LOGS_WINDOW = { start_date: "2026-07-23 00:00:00", end_date: "2026-07-24 00:00:00" };
function renderFilters(filters: Record<string, string> = {}) {
function renderFilters(filters: Record<string, string> = {}, canFilterByInternalUser = true) {
const set = vi.fn();
renderWithProviders(
<RequestLogsFilters get={(id: string) => filters[id]} set={set} teams={[]} logsWindow={LOGS_WINDOW} />,
<RequestLogsFilters
get={(id: string) => filters[id]}
set={set}
teams={[]}
logsWindow={LOGS_WINDOW}
canFilterByInternalUser={canFilterByInternalUser}
/>,
);
return { set };
}
@ -53,6 +64,7 @@ describe("RequestLogsFilters", () => {
vi.mocked(useInfiniteSpendLogEndUsers).mockReturnValue(
emptyInfiniteQuery as unknown as ReturnType<typeof useInfiniteSpendLogEndUsers>,
);
vi.mocked(useInfiniteUsers).mockReturnValue(emptyInfiniteQuery as unknown as ReturnType<typeof useInfiniteUsers>);
});
it("renders every backend-supported filter field", async () => {
@ -62,6 +74,7 @@ describe("RequestLogsFilters", () => {
"Team ID",
"Status",
"Key Alias",
"Internal User",
"End User",
"Error Code",
"Error Message",
@ -164,8 +177,57 @@ describe("RequestLogsFilters", () => {
it("scopes the End User lookup to the window the logs table is showing", async () => {
const otherWindow = { start_date: "2026-01-01 00:00:00", end_date: "2026-01-02 00:00:00" };
renderWithProviders(<RequestLogsFilters get={() => undefined} set={vi.fn()} teams={[]} logsWindow={otherWindow} />);
renderWithProviders(
<RequestLogsFilters
get={() => undefined}
set={vi.fn()}
teams={[]}
logsWindow={otherWindow}
canFilterByInternalUser
/>,
);
await waitFor(() => expect(useInfiniteSpendLogEndUsers).toHaveBeenCalledWith(otherWindow, 50, undefined));
});
it("offers internal users by email and filters on their user id", async () => {
vi.mocked(useInfiniteUsers).mockReturnValue({
...emptyInfiniteQuery,
data: {
pages: [
{
users: [{ user_id: "u-1", user_email: "bob@acme.com", user_alias: null }],
page: 1,
page_size: 50,
total: 1,
total_pages: 1,
},
],
pageParams: [1],
},
} as unknown as ReturnType<typeof useInfiniteUsers>);
const user = userEvent.setup();
const { set } = renderFilters();
await user.click(await screen.findByPlaceholderText("Search a user by email"));
await user.click(await screen.findByText("bob@acme.com"));
expect(set).toHaveBeenCalledWith(LOG_FILTER_IDS.USER_ID, "u-1");
});
it("pushes the Internal User query to the server rather than filtering a preloaded list", async () => {
const user = userEvent.setup();
renderFilters();
await user.type(await screen.findByPlaceholderText("Search a user by email"), "bob");
await waitFor(() => expect(useInfiniteUsers).toHaveBeenCalledWith(50, "bob"));
});
it("hides the Internal User filter from callers the proxy scopes to their own logs", async () => {
renderFilters({}, false);
expect(await screen.findByText("End User")).toBeInTheDocument();
expect(screen.queryByText("Internal User")).not.toBeInTheDocument();
});
});

View file

@ -5,6 +5,7 @@ import { useMemo, useState } from "react";
import { useInfiniteSpendLogEndUsers } from "@/app/(dashboard)/hooks/spendLogs/useSpendLogEndUsers";
import { useInfiniteKeyAliases } from "@/app/(dashboard)/hooks/keys/useKeyAliases";
import { useInfiniteModelInfo } from "@/app/(dashboard)/hooks/models/useModels";
import { useInfiniteUsers } from "@/app/(dashboard)/hooks/users/useUsers";
import { DataTableFilterField } from "@/components/shared/DataTable";
import { PaginatedSearchSelect } from "@/components/shared/PaginatedSearchSelect";
import { SearchSelect, type SearchSelectOption } from "@/components/shared/SearchSelect";
@ -189,6 +190,49 @@ function EndUserFilterField({
);
}
function InternalUserFilterField({
value,
onChange,
}: {
value: string;
onChange: (value: string | undefined) => void;
}) {
const [search, setSearch] = useState("");
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useInfiniteUsers(
PAGE_SIZE,
emptyToUndefined(search),
);
const options = useMemo<SearchSelectOption[]>(() => {
const seen = new Set<string>();
return (data?.pages ?? []).flatMap((page) =>
page.users.flatMap((user) => {
if (!user.user_id || seen.has(user.user_id)) return [];
seen.add(user.user_id);
const name = user.user_email || user.user_alias || "";
return [{ label: name || user.user_id, value: user.user_id, sublabel: name === "" ? undefined : user.user_id }];
}),
);
}, [data]);
return (
<DataTableFilterField label="Internal User">
<PaginatedSearchSelect
options={options}
value={value}
onValueChange={(next) => onChange(emptyToUndefined(next))}
onSearchChange={setSearch}
onLoadMore={() => void fetchNextPage()}
hasNextPage={hasNextPage}
isLoading={isLoading}
isFetchingNextPage={isFetchingNextPage}
placeholder="Search a user by email"
emptyText="No users found"
/>
</DataTableFilterField>
);
}
function ErrorCodeFilterField({ value, onChange }: { value: string; onChange: (value: string | undefined) => void }) {
const [query, setQuery] = useState("");
@ -243,9 +287,10 @@ interface RequestLogsFiltersProps {
set: (columnId: string, value: unknown) => void;
teams: Team[];
logsWindow: LogsWindow;
canFilterByInternalUser: boolean;
}
export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsFiltersProps) {
export function RequestLogsFilters({ get, set, teams, logsWindow, canFilterByInternalUser }: RequestLogsFiltersProps) {
const valueOf = (id: string): string => asString(get(id));
const setter = (id: string) => (next: string | undefined) => set(id, next);
@ -279,6 +324,10 @@ export function RequestLogsFilters({ get, set, teams, logsWindow }: RequestLogsF
teamId={valueOf(LOG_FILTER_IDS.TEAM_ID)}
/>
{canFilterByInternalUser && (
<InternalUserFilterField value={valueOf(LOG_FILTER_IDS.USER_ID)} onChange={setter(LOG_FILTER_IDS.USER_ID)} />
)}
<EndUserFilterField
value={valueOf(LOG_FILTER_IDS.END_USER)}
onChange={setter(LOG_FILTER_IDS.END_USER)}

View file

@ -6,7 +6,7 @@ import moment from "moment";
import { useCallback, useEffect, useMemo, useState } from "react";
import { AutoRouterModelGroupsProvider } from "@/components/shared/table_cells";
import { internalUserRoles } from "../../utils/roles";
import { all_admin_roles, internalUserRoles } from "../../utils/roles";
import type { KeyResponse } from "../key_team_helpers/key_list";
import { keyInfoV1Call, uiSpendLogsCall } from "../networking";
import KeyInfoView from "../templates/key_info_view";
@ -74,6 +74,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
}, [isLiveTail]);
const filterByCurrentUser = internalUserRoles.includes(userRole);
const canFilterByInternalUser = all_admin_roles.includes(userRole);
const { logsQuery, filteredLogs, allTeams } = useLogFilterLogic({
accessToken,
@ -305,6 +306,7 @@ export default function RequestLogsPanel({ accessToken, token, userRole, userID,
onSessionClick={handleSessionClick}
teams={allTeams ?? []}
logsWindow={logsWindow}
canFilterByInternalUser={canFilterByInternalUser}
toolbarChildren={
<LogsTableToolbar
startTime={startTime}

View file

@ -31,6 +31,7 @@ interface RequestLogsTableProps {
onSessionClick: (sessionId: string) => void;
teams: Team[];
logsWindow: LogsWindow;
canFilterByInternalUser: boolean;
toolbarChildren?: ReactNode;
}
@ -69,6 +70,7 @@ export function RequestLogsTable({
onSessionClick,
teams,
logsWindow,
canFilterByInternalUser,
toolbarChildren,
}: RequestLogsTableProps) {
const [filtersOpen, setFiltersOpen] = useState(false);
@ -122,7 +124,15 @@ export function RequestLogsTable({
title="Filters"
description="Narrow down request logs"
>
{({ get, set }) => <RequestLogsFilters get={get} set={set} teams={teams} logsWindow={logsWindow} />}
{({ get, set }) => (
<RequestLogsFilters
get={get}
set={set}
teams={teams}
logsWindow={logsWindow}
canFilterByInternalUser={canFilterByInternalUser}
/>
)}
</DataTableFilterDrawer>
</>
)}

View file

@ -42,6 +42,7 @@ export const LOG_FILTER_LABELS: Record<string, string> = {
[LOG_FILTER_IDS.SESSION_ID]: "Session ID",
[LOG_FILTER_IDS.MODEL_ID]: "Model",
[LOG_FILTER_IDS.PUBLIC_MODEL_OR_SEARCH_TOOL]: "Public model / search tool",
[LOG_FILTER_IDS.USER_ID]: "Internal User",
};
export interface LogsWindow {