Allow user to select logs
This commit is contained in:
@@ -10,7 +10,7 @@ export default function LoggedCallsTable() {
|
|||||||
return (
|
return (
|
||||||
<Card width="100%" overflow="hidden">
|
<Card width="100%" overflow="hidden">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader />
|
<TableHeader showCheckbox />
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{loggedCalls?.calls.map((loggedCall) => {
|
{loggedCalls?.calls.map((loggedCall) => {
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +25,7 @@ export default function LoggedCallsTable() {
|
|||||||
setExpandedRow(loggedCall.id);
|
setExpandedRow(loggedCall.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
showCheckbox
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Text,
|
Text,
|
||||||
|
Checkbox,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
@@ -19,32 +20,60 @@ 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 { useLoggedCalls } from "~/utils/hooks";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
type LoggedCall = RouterOutputs["loggedCalls"]["list"]["calls"][0];
|
type LoggedCall = RouterOutputs["loggedCalls"]["list"]["calls"][0];
|
||||||
|
|
||||||
export const TableHeader = () => (
|
export const TableHeader = ({ showCheckbox }: { showCheckbox?: boolean }) => {
|
||||||
<Thead>
|
const matchingLogIds = useLoggedCalls().data?.matchingLogIds;
|
||||||
<Tr>
|
const selectedLogIds = useAppStore((s) => s.selectedLogs.selectedLogIds);
|
||||||
<Th>Time</Th>
|
const addAll = useAppStore((s) => s.selectedLogs.addSelectedLogIds);
|
||||||
<Th>Model</Th>
|
const clearAll = useAppStore((s) => s.selectedLogs.clearSelectedLogIds);
|
||||||
<Th isNumeric>Duration</Th>
|
const allSelected = useMemo(() => {
|
||||||
<Th isNumeric>Input tokens</Th>
|
if (!matchingLogIds) return false;
|
||||||
<Th isNumeric>Output tokens</Th>
|
return matchingLogIds.every((id) => selectedLogIds.has(id));
|
||||||
<Th isNumeric>Status</Th>
|
}, [selectedLogIds, matchingLogIds]);
|
||||||
</Tr>
|
return (
|
||||||
</Thead>
|
<Thead>
|
||||||
);
|
<Tr>
|
||||||
|
{showCheckbox && (
|
||||||
|
<Th>
|
||||||
|
<HStack w={8}>
|
||||||
|
<Checkbox
|
||||||
|
isChecked={allSelected}
|
||||||
|
onChange={() => {
|
||||||
|
allSelected ? clearAll() : addAll(matchingLogIds || []);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Text>({selectedLogIds.size})</Text>
|
||||||
|
</HStack>
|
||||||
|
</Th>
|
||||||
|
)}
|
||||||
|
<Th>Time</Th>
|
||||||
|
<Th>Model</Th>
|
||||||
|
<Th isNumeric>Duration</Th>
|
||||||
|
<Th isNumeric>Input tokens</Th>
|
||||||
|
<Th isNumeric>Output tokens</Th>
|
||||||
|
<Th isNumeric>Status</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const TableRow = ({
|
export const TableRow = ({
|
||||||
loggedCall,
|
loggedCall,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
showCheckbox,
|
||||||
}: {
|
}: {
|
||||||
loggedCall: LoggedCall;
|
loggedCall: LoggedCall;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
|
showCheckbox?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
const isError = loggedCall.modelResponse?.statusCode !== 200;
|
||||||
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
|
const timeAgo = dayjs(loggedCall.requestedAt).fromNow();
|
||||||
@@ -60,16 +89,24 @@ export const TableRow = ({
|
|||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isChecked = useAppStore((s) => s.selectedLogs.selectedLogIds.has(loggedCall.id));
|
||||||
|
const toggleChecked = useAppStore((s) => s.selectedLogs.toggleSelectedLogId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr
|
<Tr
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
key={loggedCall.id}
|
key={loggedCall.id}
|
||||||
_hover={{ bgColor: "gray.100", cursor: "pointer" }}
|
_hover={{ bgColor: "gray.50", cursor: "pointer" }}
|
||||||
sx={{
|
sx={{
|
||||||
"> td": { borderBottom: "none" },
|
"> td": { borderBottom: "none" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{showCheckbox && (
|
||||||
|
<Td>
|
||||||
|
<Checkbox isChecked={isChecked} onChange={() => toggleChecked(loggedCall.id)} />
|
||||||
|
</Td>
|
||||||
|
)}
|
||||||
<Td>
|
<Td>
|
||||||
<Tooltip label={fullTime} placement="top">
|
<Tooltip label={fullTime} placement="top">
|
||||||
<Box whiteSpace="nowrap" minW="120px">
|
<Box whiteSpace="nowrap" minW="120px">
|
||||||
|
|||||||
@@ -19,10 +19,15 @@ export const loggedCallsRouter = createTRPCRouter({
|
|||||||
take: pageSize,
|
take: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const matchingLogs = await prisma.loggedCall.findMany({
|
||||||
|
where: { projectId },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
const count = await prisma.loggedCall.count({
|
const count = await prisma.loggedCall.count({
|
||||||
where: { projectId },
|
where: { projectId },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { count, calls };
|
return { calls, count, matchingLogIds: matchingLogs.map((log) => log.id) };
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,16 +4,13 @@ export const editorBackground = "#fafafa";
|
|||||||
|
|
||||||
export type SelectedLogsSlice = {
|
export type SelectedLogsSlice = {
|
||||||
selectedLogIds: Set<string>;
|
selectedLogIds: Set<string>;
|
||||||
setSelectedLogIds: (ids: Set<string>) => void;
|
|
||||||
toggleSelectedLogId: (id: string) => void;
|
toggleSelectedLogId: (id: string) => void;
|
||||||
|
addSelectedLogIds: (ids: string[]) => void;
|
||||||
|
clearSelectedLogIds: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createSelectedLogsSlice: SliceCreator<SelectedLogsSlice> = (set, get) => ({
|
export const createSelectedLogsSlice: SliceCreator<SelectedLogsSlice> = (set, get) => ({
|
||||||
selectedLogIds: new Set(),
|
selectedLogIds: new Set(),
|
||||||
setSelectedLogIds: (ids: Set<string>) =>
|
|
||||||
set((state) => {
|
|
||||||
state.selectedLogs.selectedLogIds = ids;
|
|
||||||
}),
|
|
||||||
toggleSelectedLogId: (id: string) =>
|
toggleSelectedLogId: (id: string) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
if (state.selectedLogs.selectedLogIds.has(id)) {
|
if (state.selectedLogs.selectedLogIds.has(id)) {
|
||||||
@@ -22,4 +19,12 @@ export const createSelectedLogsSlice: SliceCreator<SelectedLogsSlice> = (set, ge
|
|||||||
state.selectedLogs.selectedLogIds.add(id);
|
state.selectedLogs.selectedLogIds.add(id);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
addSelectedLogIds: (ids: string[]) =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedLogs.selectedLogIds = new Set([...state.selectedLogs.selectedLogIds, ...ids]);
|
||||||
|
}),
|
||||||
|
clearSelectedLogIds: () =>
|
||||||
|
set((state) => {
|
||||||
|
state.selectedLogs.selectedLogIds = new Set();
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { type StateCreator, create } from "zustand";
|
import { type StateCreator, create } from "zustand";
|
||||||
import { immer } from "zustand/middleware/immer";
|
import { immer } from "zustand/middleware/immer";
|
||||||
|
import { enableMapSet } from "immer";
|
||||||
import { persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
import { createSelectors } from "./createSelectors";
|
import { createSelectors } from "./createSelectors";
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +11,8 @@ import { type APIClient } from "~/utils/api";
|
|||||||
import { persistOptions, type stateToPersist } from "./persist";
|
import { persistOptions, type stateToPersist } from "./persist";
|
||||||
import { type SelectedLogsSlice, createSelectedLogsSlice } from "./selectedLogsSlice";
|
import { type SelectedLogsSlice, createSelectedLogsSlice } from "./selectedLogsSlice";
|
||||||
|
|
||||||
|
enableMapSet();
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
drawerOpen: boolean;
|
drawerOpen: boolean;
|
||||||
openDrawer: () => void;
|
openDrawer: () => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user