Display tag values
This commit is contained in:
@@ -1,24 +1,16 @@
|
|||||||
import { Button, HStack, Icon, Text } from "@chakra-ui/react";
|
import { Button, HStack, Icon, Text } from "@chakra-ui/react";
|
||||||
import { BsPlus } from "react-icons/bs";
|
import { BsPlus } from "react-icons/bs";
|
||||||
import { comparators } from "~/state/logFiltersSlice";
|
import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { useFilterableFields } from "~/utils/hooks";
|
|
||||||
|
|
||||||
const AddFilterButton = () => {
|
const AddFilterButton = () => {
|
||||||
const filterableFields = useFilterableFields().data;
|
|
||||||
|
|
||||||
const addFilter = useAppStore((s) => s.logFilters.addFilter);
|
const addFilter = useAppStore((s) => s.logFilters.addFilter);
|
||||||
|
|
||||||
if (!filterableFields || !filterableFields.length || !comparators || !comparators.length)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack
|
<HStack
|
||||||
as={Button}
|
as={Button}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() =>
|
onClick={() => addFilter({ field: defaultFilterableFields[0], comparator: comparators[0] })}
|
||||||
addFilter({ field: filterableFields[0] as string, comparator: comparators[0] })
|
|
||||||
}
|
|
||||||
spacing={0}
|
spacing={0}
|
||||||
fontSize="sm"
|
fontSize="sm"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { type LogFilter } from "~/state/logFiltersSlice";
|
import { defaultFilterableFields, type LogFilter } from "~/state/logFiltersSlice";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { useFilterableFields } from "~/utils/hooks";
|
import { useTagNames } from "~/utils/hooks";
|
||||||
import InputDropdown from "~/components/InputDropdown";
|
import InputDropdown from "~/components/InputDropdown";
|
||||||
|
|
||||||
const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: number }) => {
|
const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: number }) => {
|
||||||
const filterableFields = useFilterableFields().data;
|
const tagNames = useTagNames().data;
|
||||||
|
|
||||||
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
const updateFilter = useAppStore((s) => s.logFilters.updateFilter);
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ const SelectFieldDropdown = ({ filter, index }: { filter: LogFilter; index: numb
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InputDropdown
|
<InputDropdown
|
||||||
options={filterableFields || []}
|
options={[...defaultFilterableFields, ...(tagNames || [])]}
|
||||||
selectedOption={field}
|
selectedOption={field}
|
||||||
onSelect={(option) => updateFilter(index, { ...filter, field: option })}
|
onSelect={(option) => updateFilter(index, { ...filter, field: option })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import Link from "next/link";
|
|||||||
import { type RouterOutputs } from "~/utils/api";
|
import { type RouterOutputs } from "~/utils/api";
|
||||||
import { FormattedJson } from "./FormattedJson";
|
import { FormattedJson } from "./FormattedJson";
|
||||||
import { useAppStore } from "~/state/store";
|
import { useAppStore } from "~/state/store";
|
||||||
import { useLoggedCalls } from "~/utils/hooks";
|
import { useLoggedCalls, useTagNames } from "~/utils/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
@@ -37,6 +37,7 @@ export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
|
|||||||
if (!matchingLogIds || !matchingLogIds.length) return false;
|
if (!matchingLogIds || !matchingLogIds.length) return false;
|
||||||
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
||||||
}, [selectedLogIds, matchingLogIds]);
|
}, [selectedLogIds, matchingLogIds]);
|
||||||
|
const tagNames = useTagNames().data;
|
||||||
return (
|
return (
|
||||||
<Thead>
|
<Thead>
|
||||||
<Tr>
|
<Tr>
|
||||||
@@ -58,6 +59,7 @@ export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
|
|||||||
)}
|
)}
|
||||||
<Th>Sent At</Th>
|
<Th>Sent At</Th>
|
||||||
<Th>Model</Th>
|
<Th>Model</Th>
|
||||||
|
{tagNames?.map((tagName) => <Th key={tagName}>{tagName}</Th>)}
|
||||||
<Th isNumeric>Duration</Th>
|
<Th isNumeric>Duration</Th>
|
||||||
<Th isNumeric>Input tokens</Th>
|
<Th isNumeric>Input tokens</Th>
|
||||||
<Th isNumeric>Output tokens</Th>
|
<Th isNumeric>Output tokens</Th>
|
||||||
@@ -82,19 +84,11 @@ export const TableRow = ({
|
|||||||
const requestedAt = dayjs(loggedCall.requestedAt).format("MMMM D h:mm A");
|
const requestedAt = dayjs(loggedCall.requestedAt).format("MMMM D h:mm A");
|
||||||
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
const fullTime = dayjs(loggedCall.requestedAt).toString();
|
||||||
|
|
||||||
const durationCell = (
|
|
||||||
<Td isNumeric>
|
|
||||||
{loggedCall.cacheHit ? (
|
|
||||||
<Text color="gray.500">Cached</Text>
|
|
||||||
) : (
|
|
||||||
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
);
|
|
||||||
|
|
||||||
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
||||||
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
||||||
|
|
||||||
|
const tagNames = useTagNames().data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
@@ -118,7 +112,7 @@ export const TableRow = ({
|
|||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Td>
|
</Td>
|
||||||
<Td width="100%">
|
<Td>
|
||||||
<HStack justifyContent="flex-start">
|
<HStack justifyContent="flex-start">
|
||||||
<Text
|
<Text
|
||||||
colorScheme="purple"
|
colorScheme="purple"
|
||||||
@@ -133,7 +127,14 @@ export const TableRow = ({
|
|||||||
</Text>
|
</Text>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Td>
|
</Td>
|
||||||
{durationCell}
|
{tagNames?.map((tagName) => <Td key={tagName}>{loggedCall.tags[tagName]}</Td>)}
|
||||||
|
<Td isNumeric>
|
||||||
|
{loggedCall.cacheHit ? (
|
||||||
|
<Text color="gray.500">Cached</Text>
|
||||||
|
) : (
|
||||||
|
((loggedCall.modelResponse?.durationMs ?? 0) / 1000).toFixed(2) + "s"
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
<Td isNumeric>{loggedCall.modelResponse?.inputTokens}</Td>
|
||||||
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
<Td isNumeric>{loggedCall.modelResponse?.outputTokens}</Td>
|
||||||
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
<Td sx={{ color: isError ? "red.500" : "green.500", fontWeight: "semibold" }} isNumeric>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { type Expression, type SqlBool } from "kysely";
|
import { type Expression, type SqlBool, sql } from "kysely";
|
||||||
import { sql } from "kysely";
|
import { jsonArrayFrom } from "kysely/helpers/postgres";
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
import { kysely, prisma } from "~/server/db";
|
import { kysely, prisma } from "~/server/db";
|
||||||
import { comparators } from "~/state/logFiltersSlice";
|
import { comparators, defaultFilterableFields } from "~/state/logFiltersSlice";
|
||||||
import { requireCanViewProject } from "~/utils/accessControl";
|
import { requireCanViewProject } from "~/utils/accessControl";
|
||||||
|
|
||||||
const defaultFilterableFields = ["Request", "Response", "Model", "Status Code"];
|
|
||||||
|
|
||||||
// create comparator type based off of comparators
|
// create comparator type based off of comparators
|
||||||
const comparatorToSqlValue = (comparator: (typeof comparators)[number], value: string) => {
|
const comparatorToSqlValue = (comparator: (typeof comparators)[number], value: string) => {
|
||||||
switch (comparator) {
|
switch (comparator) {
|
||||||
@@ -91,7 +89,10 @@ export const loggedCallsRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tagFilters = input.filters.filter(
|
const tagFilters = input.filters.filter(
|
||||||
(filter) => !defaultFilterableFields.includes(filter.field),
|
(filter) =>
|
||||||
|
!defaultFilterableFields.includes(
|
||||||
|
filter.field as (typeof defaultFilterableFields)[number],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
let updatedBaseQuery = baseQuery;
|
let updatedBaseQuery = baseQuery;
|
||||||
@@ -112,7 +113,7 @@ export const loggedCallsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rawCalls = await updatedBaseQuery
|
const rawCalls = await updatedBaseQuery
|
||||||
.select([
|
.select((eb) => [
|
||||||
"lc.id as id",
|
"lc.id as id",
|
||||||
"lc.requestedAt as requestedAt",
|
"lc.requestedAt as requestedAt",
|
||||||
"model",
|
"model",
|
||||||
@@ -127,28 +128,45 @@ export const loggedCallsRouter = createTRPCRouter({
|
|||||||
"cost",
|
"cost",
|
||||||
"statusCode",
|
"statusCode",
|
||||||
"durationMs",
|
"durationMs",
|
||||||
|
jsonArrayFrom(
|
||||||
|
eb
|
||||||
|
.selectFrom("LoggedCallTag")
|
||||||
|
.select(["name", "value"])
|
||||||
|
.whereRef("loggedCallId", "=", "lc.id"),
|
||||||
|
).as("tags"),
|
||||||
])
|
])
|
||||||
.orderBy("lc.requestedAt", "desc")
|
.orderBy("lc.requestedAt", "desc")
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset((page - 1) * pageSize)
|
.offset((page - 1) * pageSize)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
const calls = rawCalls.map((rawCall) => ({
|
const calls = rawCalls.map((rawCall) => {
|
||||||
id: rawCall.id,
|
const tagsObject = rawCall.tags.reduce(
|
||||||
requestedAt: rawCall.requestedAt,
|
(acc, tag) => {
|
||||||
model: rawCall.model,
|
acc[tag.name] = tag.value;
|
||||||
cacheHit: rawCall.cacheHit,
|
return acc;
|
||||||
modelResponse: {
|
},
|
||||||
receivedAt: rawCall.receivedAt,
|
{} as Record<string, string | null>,
|
||||||
reqPayload: rawCall.reqPayload,
|
);
|
||||||
respPayload: rawCall.respPayload,
|
|
||||||
inputTokens: rawCall.inputTokens,
|
return {
|
||||||
outputTokens: rawCall.outputTokens,
|
id: rawCall.id,
|
||||||
cost: rawCall.cost,
|
requestedAt: rawCall.requestedAt,
|
||||||
statusCode: rawCall.statusCode,
|
model: rawCall.model,
|
||||||
durationMs: rawCall.durationMs,
|
cacheHit: rawCall.cacheHit,
|
||||||
},
|
modelResponse: {
|
||||||
}));
|
receivedAt: rawCall.receivedAt,
|
||||||
|
reqPayload: rawCall.reqPayload,
|
||||||
|
respPayload: rawCall.respPayload,
|
||||||
|
inputTokens: rawCall.inputTokens,
|
||||||
|
outputTokens: rawCall.outputTokens,
|
||||||
|
cost: rawCall.cost,
|
||||||
|
statusCode: rawCall.statusCode,
|
||||||
|
durationMs: rawCall.durationMs,
|
||||||
|
},
|
||||||
|
tags: tagsObject,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const matchingLogIds = await updatedBaseQuery.select(["lc.id"]).execute();
|
const matchingLogIds = await updatedBaseQuery.select(["lc.id"]).execute();
|
||||||
|
|
||||||
@@ -156,7 +174,7 @@ export const loggedCallsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return { calls, count, matchingLogIds: matchingLogIds.map((log) => log.id) };
|
return { calls, count, matchingLogIds: matchingLogIds.map((log) => log.id) };
|
||||||
}),
|
}),
|
||||||
getFilterableFields: protectedProcedure
|
getTagNames: protectedProcedure
|
||||||
.input(z.object({ projectId: z.string() }))
|
.input(z.object({ projectId: z.string() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
await requireCanViewProject(input.projectId, ctx);
|
await requireCanViewProject(input.projectId, ctx);
|
||||||
@@ -169,8 +187,11 @@ export const loggedCallsRouter = createTRPCRouter({
|
|||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
},
|
},
|
||||||
|
orderBy: {
|
||||||
|
name: "asc",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...defaultFilterableFields, ...tags.map((tag) => tag.name)];
|
return tags.map((tag) => tag.name);
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { type SliceCreator } from "./store";
|
import { type SliceCreator } from "./store";
|
||||||
|
|
||||||
export const editorBackground = "#fafafa";
|
|
||||||
|
|
||||||
export const comparators = ["=", "!=", "CONTAINS"] as const;
|
export const comparators = ["=", "!=", "CONTAINS"] as const;
|
||||||
|
|
||||||
|
export const defaultFilterableFields = ["Request", "Response", "Model", "Status Code"] as const;
|
||||||
|
|
||||||
export interface LogFilter {
|
export interface LogFilter {
|
||||||
field: string;
|
field: string;
|
||||||
comparator: (typeof comparators)[number];
|
comparator: (typeof comparators)[number];
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { type SliceCreator } from "./store";
|
import { type SliceCreator } from "./store";
|
||||||
|
|
||||||
export const editorBackground = "#fafafa";
|
|
||||||
|
|
||||||
export type SelectedLogsSlice = {
|
export type SelectedLogsSlice = {
|
||||||
selectedLogIds: Set<string>;
|
selectedLogIds: Set<string>;
|
||||||
toggleSelectedLogId: (id: string) => void;
|
toggleSelectedLogId: (id: string) => void;
|
||||||
|
|||||||
@@ -187,9 +187,9 @@ export const useLoggedCalls = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFilterableFields = () => {
|
export const useTagNames = () => {
|
||||||
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
|
const selectedProjectId = useAppStore((state) => state.selectedProjectId);
|
||||||
return api.loggedCalls.getFilterableFields.useQuery(
|
return api.loggedCalls.getTagNames.useQuery(
|
||||||
{ projectId: selectedProjectId ?? "" },
|
{ projectId: selectedProjectId ?? "" },
|
||||||
{ enabled: !!selectedProjectId },
|
{ enabled: !!selectedProjectId },
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user