mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(ui): show Lens run reloads as a search spinner and a blurred body
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
5e7b0afd5c
commit
03bec959bf
9 changed files with 33 additions and 37 deletions
|
|
@ -469,12 +469,3 @@
|
||||||
left: 100%;
|
left: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes rows-sweep {
|
|
||||||
from {
|
|
||||||
left: -25%;
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -671,12 +671,12 @@ describe("AgentTracesPage", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
fireEvent.click(screen.getByTestId(`sort-header-${key}`));
|
fireEvent.click(screen.getByTestId(`sort-header-${key}`));
|
||||||
expect(await screen.findByRole("progressbar", { name: "Updating rows" })).toBeInTheDocument();
|
expect(await screen.findByRole("status", { name: "Loading results" })).toBeInTheDocument();
|
||||||
expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(rowsBefore.length);
|
expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(rowsBefore.length);
|
||||||
expect(listRequest(-1)).toMatchObject({ order: { key, descending: true }, page: { cursor: null } });
|
expect(listRequest(-1)).toMatchObject({ order: { key, descending: true }, page: { cursor: null } });
|
||||||
|
|
||||||
act(release);
|
act(release);
|
||||||
await waitFor(() => expect(screen.queryByRole("progressbar")).not.toBeInTheDocument());
|
await waitFor(() => expect(screen.queryByRole("status", { name: "Loading results" })).not.toBeInTheDocument());
|
||||||
expect(screen.getAllByTestId("agent-trace-row")[0]).toHaveTextContent(topAfter);
|
expect(screen.getAllByTestId("agent-trace-row")[0]).toHaveTextContent(topAfter);
|
||||||
expect(lastUrl(onUrlUpdate).get("sort_by")).toBe(key);
|
expect(lastUrl(onUrlUpdate).get("sort_by")).toBe(key);
|
||||||
expect(lastUrl(onUrlUpdate).has("sort_dir")).toBe(false);
|
expect(lastUrl(onUrlUpdate).has("sort_dir")).toBe(false);
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,14 @@ export function AgentTracesSection({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Inspector.Panel>
|
</Inspector.Panel>
|
||||||
<RunsToolbar query={query} onQueryChange={setQuery} runs={traces.traces} range={zoom ?? window} order={order}>
|
<RunsToolbar
|
||||||
|
query={query}
|
||||||
|
onQueryChange={setQuery}
|
||||||
|
runs={traces.traces}
|
||||||
|
range={zoom ?? window}
|
||||||
|
order={order}
|
||||||
|
busy={traces.isPlaceholder}
|
||||||
|
>
|
||||||
{timeControls && (
|
{timeControls && (
|
||||||
<TimeRangeControls
|
<TimeRangeControls
|
||||||
range={range}
|
range={range}
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,6 @@ describe("AgentTracesTable sorting", () => {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(runs.length);
|
expect(screen.getAllByTestId("agent-trace-row")).toHaveLength(runs.length);
|
||||||
expect(screen.getByRole("progressbar", { name: "Updating rows" })).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId("runs-placeholder")).not.toBeInTheDocument();
|
expect(screen.queryByTestId("runs-placeholder")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
||||||
expect(onLoadMore).not.toHaveBeenCalled();
|
expect(onLoadMore).not.toHaveBeenCalled();
|
||||||
|
|
|
||||||
|
|
@ -287,10 +287,10 @@ export function AgentTracesTable({
|
||||||
<InspectorTable.Root table={table} data-testid="runs-table">
|
<InspectorTable.Root table={table} data-testid="runs-table">
|
||||||
<ScrollToTop order={order} />
|
<ScrollToTop order={order} />
|
||||||
<InspectorTable.Grid aria-label="Agent runs" aria-busy={isFetching} className="min-w-[900px] text-xs">
|
<InspectorTable.Grid aria-label="Agent runs" aria-busy={isFetching} className="min-w-[900px] text-xs">
|
||||||
<InspectorTable.Header busy={isPlaceholder} />
|
<InspectorTable.Header />
|
||||||
<InspectorTable.Body<TraceSummary>
|
<InspectorTable.Body<TraceSummary>
|
||||||
rowHeight={() => ROW_HEIGHT}
|
rowHeight={() => ROW_HEIGHT}
|
||||||
className={cn(isPlaceholder && "opacity-60 transition-opacity motion-reduce:transition-none")}
|
className={cn("transition-[filter]", isPlaceholder && "blur-[1.5px]")}
|
||||||
after={
|
after={
|
||||||
<>
|
<>
|
||||||
{isLoading && SKELETON_ROWS.map((row) => <PlaceholderRow key={row} index={row} />)}
|
{isLoading && SKELETON_ROWS.map((row) => <PlaceholderRow key={row} index={row} />)}
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,11 @@ interface RunSearchProps {
|
||||||
/** The range and order the list shows; the copied query follows both. */
|
/** The range and order the list shows; the copied query follows both. */
|
||||||
range?: TimeWindow;
|
range?: TimeWindow;
|
||||||
order?: RunOrder;
|
order?: RunOrder;
|
||||||
|
busy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The runs list query box: free text plus `key:value` filters over run fields, copyable as a trace query. */
|
/** The runs list query box: free text plus `key:value` filters over run fields, copyable as a trace query. */
|
||||||
export function RunSearch({ value, onChange, runs, range, order = NEWEST }: RunSearchProps) {
|
export function RunSearch({ value, onChange, runs, range, order = NEWEST, busy = false }: RunSearchProps) {
|
||||||
const command = useMemo(() => runQueryCommand(range, order), [range, order]);
|
const command = useMemo(() => runQueryCommand(range, order), [range, order]);
|
||||||
return (
|
return (
|
||||||
<SearchBox.Root
|
<SearchBox.Root
|
||||||
|
|
@ -37,6 +38,7 @@ export function RunSearch({ value, onChange, runs, range, order = NEWEST }: RunS
|
||||||
>
|
>
|
||||||
<SearchBox.Input
|
<SearchBox.Input
|
||||||
className="h-full rounded-none border-0 px-3 focus-within:bg-muted/40 focus-within:ring-0 dark:bg-transparent"
|
className="h-full rounded-none border-0 px-3 focus-within:bg-muted/40 focus-within:ring-0 dark:bg-transparent"
|
||||||
|
busy={busy}
|
||||||
placeholder="Search runs, or filter like agent:researcher status:error"
|
placeholder="Search runs, or filter like agent:researcher status:error"
|
||||||
/>
|
/>
|
||||||
<SearchBox.Suggestions>
|
<SearchBox.Suggestions>
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,16 @@ interface RunsToolbarProps {
|
||||||
/** The range and order the list shows, for the copied query. */
|
/** The range and order the list shows, for the copied query. */
|
||||||
range?: TimeWindow;
|
range?: TimeWindow;
|
||||||
order?: RunOrder;
|
order?: RunOrder;
|
||||||
|
/** Shows a spinner in the search box while the list reloads for a new query or order. */
|
||||||
|
busy?: boolean;
|
||||||
/** Extra controls (time range, live tail) rendered on the right. */
|
/** Extra controls (time range, live tail) rendered on the right. */
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RunsToolbar({ query, onQueryChange, runs, range, order, children }: RunsToolbarProps) {
|
export function RunsToolbar({ query, onQueryChange, runs, range, order, busy, children }: RunsToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-10 shrink-0 items-stretch border-b border-border bg-card">
|
<div className="flex h-10 shrink-0 items-stretch border-b border-border bg-card">
|
||||||
<RunSearch value={query} onChange={onQueryChange} runs={runs} range={range} order={order} />
|
<RunSearch value={query} onChange={onQueryChange} runs={runs} range={range} order={order} busy={busy} />
|
||||||
{children && <div className="flex shrink-0 items-stretch">{children}</div>}
|
{children && <div className="flex shrink-0 items-stretch">{children}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -73,11 +73,9 @@ function ariaSort<T>(column: Column<T>): "ascending" | "descending" | "none" | u
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
readonly hidden?: boolean;
|
readonly hidden?: boolean;
|
||||||
/** Shows a progress line under the headings while the rows below are being replaced. */
|
|
||||||
readonly busy?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Header({ hidden = false, busy = false }: HeaderProps) {
|
function Header({ hidden = false }: HeaderProps) {
|
||||||
const { table } = useInspectorTable();
|
const { table } = useInspectorTable();
|
||||||
return (
|
return (
|
||||||
<TableHeader
|
<TableHeader
|
||||||
|
|
@ -100,19 +98,6 @@ function Header({ hidden = false, busy = false }: HeaderProps) {
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{busy && (
|
|
||||||
<tr className="border-0">
|
|
||||||
<th colSpan={table.getVisibleLeafColumns().length} className="relative h-0 p-0">
|
|
||||||
<div
|
|
||||||
role="progressbar"
|
|
||||||
aria-label="Updating rows"
|
|
||||||
className="absolute inset-x-0 bottom-0 h-0.5 overflow-hidden bg-[#3b5bfd]/10"
|
|
||||||
>
|
|
||||||
<div className="absolute inset-y-0 w-1/4 bg-[#3b5bfd]/60 motion-safe:animate-[rows-sweep_1.2s_ease-in-out_infinite] motion-reduce:hidden" />
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import "prosemirror-view/style/prosemirror.css";
|
||||||
|
|
||||||
import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer";
|
import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer";
|
||||||
import { ProseMirror, ProseMirrorDoc, reactKeys, useEditorEventCallback } from "@handlewithcare/react-prosemirror";
|
import { ProseMirror, ProseMirrorDoc, reactKeys, useEditorEventCallback } from "@handlewithcare/react-prosemirror";
|
||||||
import { Check, Copy, CornerDownLeft, type LucideIcon, Search } from "lucide-react";
|
import { Check, Copy, CornerDownLeft, Loader2, type LucideIcon, Search } from "lucide-react";
|
||||||
import { Schema } from "prosemirror-model";
|
import { Schema } from "prosemirror-model";
|
||||||
import { EditorState, Plugin, TextSelection, type Transaction } from "prosemirror-state";
|
import { EditorState, Plugin, TextSelection, type Transaction } from "prosemirror-state";
|
||||||
import { Decoration, DecorationSet, type EditorView } from "prosemirror-view";
|
import { Decoration, DecorationSet, type EditorView } from "prosemirror-view";
|
||||||
|
|
@ -231,10 +231,12 @@ function Root<F extends string>({
|
||||||
|
|
||||||
export type SearchBoxInputProps = ComponentProps<"div"> & {
|
export type SearchBoxInputProps = ComponentProps<"div"> & {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
/** Swaps the search icon for a spinner while results for the current query load. */
|
||||||
|
busy?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** The bordered field holding the editor; shows `placeholder` while the query is empty. */
|
/** The bordered field holding the editor; shows `placeholder` while the query is empty. */
|
||||||
function Input({ placeholder, className, ...props }: SearchBoxInputProps) {
|
function Input({ placeholder, busy = false, className, ...props }: SearchBoxInputProps) {
|
||||||
const { text } = useSearchBox();
|
const { text } = useSearchBox();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -245,7 +247,15 @@ function Input({ placeholder, className, ...props }: SearchBoxInputProps) {
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
{busy ? (
|
||||||
|
<Loader2
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading results"
|
||||||
|
className="size-3.5 shrink-0 animate-spin text-muted-foreground"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
<Search className="size-3.5 shrink-0 text-muted-foreground" />
|
||||||
|
)}
|
||||||
<div className="relative flex min-w-0 flex-1">
|
<div className="relative flex min-w-0 flex-1">
|
||||||
<ProseMirrorDoc />
|
<ProseMirrorDoc />
|
||||||
{!text && (
|
{!text && (
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue