Paginate Query History table (#3592)

* Add pagination for query history table

* Fix method name

* Fix mypy
This commit is contained in:
skylares
2025-01-17 18:31:42 -05:00
committed by GitHub
parent 6fc52c81ab
commit af953ff8a3
14 changed files with 517 additions and 341 deletions

View File

@@ -10,7 +10,6 @@ import { cn } from "@/lib/utils";
import { CalendarIcon } from "lucide-react";
import { format } from "date-fns";
import { getXDaysAgo } from "./dateUtils";
import { Separator } from "@/components/ui/separator";
export const THIRTY_DAYS = "30d";
@@ -84,8 +83,16 @@ export const DateRangeSelector = memo(function DateRangeSelector({
defaultMonth={value?.from}
selected={value}
onSelect={(range) => {
if (range?.from && range?.to) {
onValueChange({ from: range.from, to: range.to });
if (range?.from) {
if (range.to) {
// Normal range selection when initialized with a range
onValueChange({ from: range.from, to: range.to });
} else {
// Single date selection when initilized without a range
const to = new Date(range.from);
const from = new Date(to.setDate(to.getDate() - 1));
onValueChange({ from, to });
}
}
}}
numberOfMonths={2}

View File

@@ -65,27 +65,6 @@ export const useOnyxBotAnalytics = (timeRange: DateRangePickerValue) => {
};
};
export const useQueryHistory = ({
selectedFeedbackType,
timeRange,
}: {
selectedFeedbackType: Feedback | null;
timeRange: DateRange;
}) => {
const url = buildApiPath("/api/admin/chat-session-history", {
feedback_type: selectedFeedbackType,
start: convertDateToStartOfDay(timeRange?.from)?.toISOString(),
end: convertDateToEndOfDay(timeRange?.to)?.toISOString(),
});
const swrResponse = useSWR<ChatSessionMinimal[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshQueryHistory: () => mutate(url),
};
};
export function getDatesList(startDate: Date): string[] {
const datesList: string[] = [];
const endDate = new Date(); // current date

View File

@@ -1,4 +1,3 @@
import { useQueryHistory, useTimeRange } from "../lib";
import { Separator } from "@/components/ui/separator";
import {
Table,
@@ -20,8 +19,8 @@ import {
import { ThreeDotsLoader } from "@/components/Loading";
import { ChatSessionMinimal } from "../usage/types";
import { timestampToReadableDate } from "@/lib/dateUtils";
import { FiFrown, FiMinus, FiSmile } from "react-icons/fi";
import { useCallback, useState } from "react";
import { FiFrown, FiMinus, FiSmile, FiMeh } from "react-icons/fi";
import { useCallback, useState, useMemo } from "react";
import { Feedback } from "@/lib/types";
import { DateRange, DateRangeSelector } from "../DateRangeSelector";
import { PageSelector } from "@/components/PageSelector";
@@ -29,8 +28,11 @@ import Link from "next/link";
import { FeedbackBadge } from "./FeedbackBadge";
import { DownloadAsCSV } from "./DownloadAsCSV";
import CardSection from "@/components/admin/CardSection";
import usePaginatedFetch from "@/hooks/usePaginatedFetch";
import { ErrorCallout } from "@/components/ErrorCallout";
const NUM_IN_PAGE = 20;
const ITEMS_PER_PAGE = 20;
const PAGES_PER_BATCH = 2;
function QueryHistoryTableRow({
chatSessionMinimal,
@@ -108,6 +110,12 @@ function SelectFeedbackType({
<span>Dislike</span>
</div>
</SelectItem>
<SelectItem value="mixed">
<div className="flex items-center gap-2">
<FiMeh className="h-4 w-4" />
<span>Mixed</span>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
@@ -116,31 +124,55 @@ function SelectFeedbackType({
}
export function QueryHistoryTable() {
const [selectedFeedbackType, setSelectedFeedbackType] = useState<
Feedback | "all"
>("all");
const [timeRange, setTimeRange] = useTimeRange();
const [dateRange, setDateRange] = useState<DateRange>(undefined);
const [filters, setFilters] = useState<{
feedback_type?: Feedback | "all";
start_time?: string;
end_time?: string;
}>({});
const { data: chatSessionData } = useQueryHistory({
selectedFeedbackType:
selectedFeedbackType === "all" ? null : selectedFeedbackType,
timeRange,
const {
currentPageData: chatSessionData,
isLoading,
error,
currentPage,
totalPages,
goToPage,
refresh,
} = usePaginatedFetch<ChatSessionMinimal>({
itemsPerPage: ITEMS_PER_PAGE,
pagesPerBatch: PAGES_PER_BATCH,
endpoint: "/api/admin/chat-session-history",
filter: filters,
});
const [page, setPage] = useState(1);
const onTimeRangeChange = useCallback((value: DateRange) => {
setDateRange(value);
const onTimeRangeChange = useCallback(
(value: DateRange) => {
if (value) {
setTimeRange((prevTimeRange) => ({
...prevTimeRange,
from: new Date(value.from),
to: new Date(value.to),
}));
}
},
[setTimeRange]
);
if (value?.from && value?.to) {
setFilters((prev) => ({
...prev,
start_time: value.from.toISOString(),
end_time: value.to.toISOString(),
}));
} else {
setFilters((prev) => {
const newFilters = { ...prev };
delete newFilters.start_time;
delete newFilters.end_time;
return newFilters;
});
}
}, []);
if (error) {
return (
<ErrorCallout
errorTitle="Error fetching query history"
errorMsg={error?.message}
/>
);
}
return (
<CardSection className="mt-8">
@@ -148,12 +180,22 @@ export function QueryHistoryTable() {
<div className="flex">
<div className="gap-y-3 flex flex-col">
<SelectFeedbackType
value={selectedFeedbackType || "all"}
onValueChange={setSelectedFeedbackType}
value={filters.feedback_type || "all"}
onValueChange={(value) => {
setFilters((prev) => {
const newFilters = { ...prev };
if (value === "all") {
delete newFilters.feedback_type;
} else {
newFilters.feedback_type = value;
}
return newFilters;
});
}}
/>
<DateRangeSelector
value={timeRange}
value={dateRange}
onValueChange={onTimeRangeChange}
/>
</div>
@@ -172,33 +214,33 @@ export function QueryHistoryTable() {
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{chatSessionData &&
chatSessionData
.slice(NUM_IN_PAGE * (page - 1), NUM_IN_PAGE * page)
.map((chatSessionMinimal) => (
<QueryHistoryTableRow
key={chatSessionMinimal.id}
chatSessionMinimal={chatSessionMinimal}
/>
))}
</TableBody>
{isLoading ? (
<TableBody>
<TableRow>
<TableCell colSpan={6} className="text-center">
<ThreeDotsLoader />
</TableCell>
</TableRow>
</TableBody>
) : (
<TableBody>
{chatSessionData?.map((chatSessionMinimal) => (
<QueryHistoryTableRow
key={chatSessionMinimal.id}
chatSessionMinimal={chatSessionMinimal}
/>
))}
</TableBody>
)}
</Table>
{chatSessionData && (
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(chatSessionData.length / NUM_IN_PAGE)}
currentPage={page}
onPageChange={(newPage) => {
setPage(newPage);
window.scrollTo({
top: 0,
left: 0,
behavior: "smooth",
});
}}
totalPages={totalPages}
currentPage={currentPage}
onPageChange={goToPage}
/>
</div>
</div>

View File

@@ -106,9 +106,7 @@ export default function QueryPage(props: { params: Promise<{ id: string }> }) {
<div className="flex flex-col">
{chatSessionSnapshot.messages.map((message) => {
return (
<MessageDisplay key={message.time_created} message={message} />
);
return <MessageDisplay key={message.id} message={message} />;
})}
</div>
</CardSection>

View File

@@ -25,6 +25,7 @@ export interface AbridgedSearchDoc {
}
export interface MessageSnapshot {
id: number;
message: string;
message_type: "user" | "assistant";
documents: AbridgedSearchDoc[];

View File

@@ -5,12 +5,14 @@ import {
AcceptedUserSnapshot,
InvitedUserSnapshot,
} from "@/lib/types";
import { ChatSessionMinimal } from "@/app/ee/admin/performance/usage/types";
import { errorHandlingFetcher } from "@/lib/fetcher";
type PaginatedType =
| IndexAttemptSnapshot
| AcceptedUserSnapshot
| InvitedUserSnapshot;
| InvitedUserSnapshot
| ChatSessionMinimal;
interface PaginatedApiResponse<T extends PaginatedType> {
items: T[];
@@ -22,7 +24,7 @@ interface PaginationConfig {
pagesPerBatch: number;
endpoint: string;
query?: string;
filter?: Record<string, string | boolean | number | string[]>;
filter?: Record<string, string | boolean | number | string[] | Date>;
refreshIntervalInMs?: number;
}

View File

@@ -98,7 +98,7 @@ export type ValidStatuses =
| "in_progress"
| "not_started";
export type TaskStatus = "PENDING" | "STARTED" | "SUCCESS" | "FAILURE";
export type Feedback = "like" | "dislike";
export type Feedback = "like" | "dislike" | "mixed";
export type AccessType = "public" | "private" | "sync";
export type SessionType = "Chat" | "Search" | "Slack";