milestone 16: behavioral fixes for silent failures, locks, counters and the query log
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { useRef, useState, type FormEvent } from "react";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import * as api from "@/lib/api";
|
||||
import { formatMicros, formatTime } from "@/lib/format";
|
||||
import { queriesQuery } from "@/lib/queries";
|
||||
import { handleUnauthorized } from "@/lib/queryClient";
|
||||
import { queriesInfiniteQuery } from "@/lib/queries";
|
||||
import type { QueriesFilter, QueryRow } from "@/lib/types";
|
||||
import { qtypeName } from "./qtype";
|
||||
|
||||
@@ -12,6 +11,10 @@ const inputClass =
|
||||
const buttonClass =
|
||||
"rounded border border-zinc-300 px-3 py-1.5 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700";
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
function datetimeLocalToUnix(value: string): number | undefined {
|
||||
if (value === "") return undefined;
|
||||
const ms = new Date(value).getTime();
|
||||
@@ -77,25 +80,20 @@ export default function QueryLogPage() {
|
||||
const [until, setUntil] = useState("");
|
||||
|
||||
const [applied, setApplied] = useState<QueriesFilter>({});
|
||||
const [extra, setExtra] = useState<QueryRow[]>([]);
|
||||
const [cursorOverride, setCursorOverride] = useState<number | null | undefined>(undefined);
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [moreError, setMoreError] = useState<string | null>(null);
|
||||
const generation = useRef(0);
|
||||
|
||||
const base = useQuery({ ...queriesQuery(applied), placeholderData: keepPreviousData });
|
||||
const base = useInfiniteQuery(queriesInfiniteQuery(applied));
|
||||
|
||||
const rows: QueryRow[] = [...(base.data?.queries ?? []), ...extra];
|
||||
const nextBefore = cursorOverride !== undefined ? cursorOverride : (base.data?.next_before ?? null);
|
||||
const pages = base.data?.pages ?? [];
|
||||
const rows: QueryRow[] = pages.flatMap((page) => page.queries);
|
||||
const filterActive = Object.keys(applied).length > 0;
|
||||
|
||||
function resetAccumulation() {
|
||||
generation.current += 1;
|
||||
setExtra([]);
|
||||
setCursorOverride(undefined);
|
||||
setLoadingMore(false);
|
||||
setMoreError(null);
|
||||
}
|
||||
// `base.hasNextPage` reads the query state, which is empty while placeholder
|
||||
// data stands in for a filter change; derive the cursor from what is on
|
||||
// screen so the button keeps its place instead of flashing "end of log".
|
||||
const lastPage = pages[pages.length - 1];
|
||||
const hasMore = lastPage !== undefined && lastPage.next_before !== null;
|
||||
// A 401 is already redirecting via the cache-level handleUnauthorized.
|
||||
const isUnauthorized = base.error instanceof api.ApiError && base.error.status === 401;
|
||||
const moreError = base.isFetchNextPageError && !isUnauthorized ? errorMessage(base.error) : null;
|
||||
|
||||
function applyFilters(event: FormEvent) {
|
||||
event.preventDefault();
|
||||
@@ -109,7 +107,6 @@ export default function QueryLogPage() {
|
||||
const untilTs = datetimeLocalToUnix(until);
|
||||
if (untilTs !== undefined) filter.until = untilTs;
|
||||
setApplied(filter);
|
||||
resetAccumulation();
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
@@ -119,32 +116,11 @@ export default function QueryLogPage() {
|
||||
setSince("");
|
||||
setUntil("");
|
||||
setApplied({});
|
||||
resetAccumulation();
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (nextBefore === null || loadingMore || base.isPlaceholderData) return;
|
||||
const startedGeneration = generation.current;
|
||||
setLoadingMore(true);
|
||||
setMoreError(null);
|
||||
api.getQueries({ ...applied, before: nextBefore })
|
||||
.then((page) => {
|
||||
if (generation.current !== startedGeneration) return;
|
||||
setExtra((prev) => [...prev, ...page.queries]);
|
||||
setCursorOverride(page.next_before);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
if (error instanceof api.ApiError && error.status === 401) {
|
||||
handleUnauthorized(error);
|
||||
return;
|
||||
}
|
||||
if (generation.current !== startedGeneration) return;
|
||||
setMoreError(error instanceof Error ? error.message : String(error));
|
||||
})
|
||||
.finally(() => {
|
||||
if (generation.current !== startedGeneration) return;
|
||||
setLoadingMore(false);
|
||||
});
|
||||
if (!hasMore || base.isFetchingNextPage || base.isPlaceholderData) return;
|
||||
void base.fetchNextPage();
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -236,16 +212,16 @@ export default function QueryLogPage() {
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<p className="text-sm text-zinc-500">
|
||||
Showing {rows.length} {rows.length === 1 ? "query" : "queries"}
|
||||
{nextBefore === null ? " — end of log" : ""}
|
||||
{hasMore ? "" : " — end of log"}
|
||||
</p>
|
||||
{nextBefore !== null && (
|
||||
{hasMore && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadMore}
|
||||
disabled={loadingMore || base.isPlaceholderData}
|
||||
disabled={base.isFetchingNextPage || base.isPlaceholderData}
|
||||
className={buttonClass}
|
||||
>
|
||||
{loadingMore ? "Loading…" : "Load more"}
|
||||
{base.isFetchingNextPage ? "Loading…" : "Load more"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user