mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(ui): show filter-select label and shape loading skeletons per column
The workflow-runs Status filter leaked the internal "__all__" sentinel as its displayed value because Base UI's Select.Value renders the raw value when no items map or children function is given. Drop the sentinel and use Base UI's native null handling: a null "All statuses" item plus a placeholder, with an items map so a real selection renders its capitalized label rather than the raw status string The shared DataTable rendered every loading-skeleton cell as one identical half-width bar, which read as a rigid grid instead of the table beneath it. Vary the skeleton width per column and add a per-column skeleton shape hint (text or twoLine) on ColumnMeta so identity columns like the workflow "Run" cell get a two-line skeleton that matches their real content
This commit is contained in:
parent
a88a9bfcb2
commit
ae55170343
5 changed files with 59 additions and 17 deletions
|
|
@ -67,7 +67,13 @@ const STATUS_DOT: Record<RunStatus, string> = {
|
|||
};
|
||||
|
||||
const RUN_STATUS_OPTIONS: RunStatus[] = ["pending", "running", "paused", "completed", "failed"];
|
||||
const FILTER_ALL = "__all__";
|
||||
const STATUS_LABELS: Record<RunStatus, string> = {
|
||||
pending: "Pending",
|
||||
running: "Running",
|
||||
paused: "Paused",
|
||||
completed: "Completed",
|
||||
failed: "Failed",
|
||||
};
|
||||
|
||||
const EVENT_COLOR: Record<string, { bar: string; border: string; text: string }> = {
|
||||
"step.started": { bar: "#f0fdf4", border: "#86efac", text: "#16a34a" },
|
||||
|
|
@ -562,7 +568,7 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
|
|||
id: "run",
|
||||
accessorFn: (row) => `${runTitle(row)} ${row.run_id}`,
|
||||
header: "Run",
|
||||
meta: { title: "Run" },
|
||||
meta: { title: "Run", skeleton: "twoLine" },
|
||||
cell: ({ row }) => {
|
||||
const run = row.original;
|
||||
return (
|
||||
|
|
@ -674,17 +680,18 @@ const WorkflowRuns: React.FC<WorkflowRunsProps> = ({ accessToken }) => {
|
|||
<>
|
||||
<DataTableFilterField label="Status">
|
||||
<Select
|
||||
value={(get("status") as string) || FILTER_ALL}
|
||||
onValueChange={(value) => set("status", value === FILTER_ALL ? "" : value)}
|
||||
items={STATUS_LABELS}
|
||||
value={(get("status") as string) || null}
|
||||
onValueChange={(value: string | null) => set("status", value ?? "")}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
<SelectValue placeholder="All statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value={FILTER_ALL}>All statuses</SelectItem>
|
||||
<SelectItem value={null}>All statuses</SelectItem>
|
||||
{RUN_STATUS_OPTIONS.map((status) => (
|
||||
<SelectItem key={status} value={status} className="capitalize">
|
||||
{status}
|
||||
<SelectItem key={status} value={status}>
|
||||
{STATUS_LABELS[status]}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
|
|
|||
|
|
@ -286,6 +286,23 @@ describe("DataTable loading", () => {
|
|||
expect(screen.queryAllByTestId("skeleton-row")).toHaveLength(0);
|
||||
expect(names()).toEqual(["Charlie", "Alice", "Bob"]);
|
||||
});
|
||||
|
||||
it("varies skeleton shape and width per column instead of one fixed bar", () => {
|
||||
const columns: ColumnDef<Person, unknown>[] = [
|
||||
{ accessorKey: "name", header: "Name", meta: { skeleton: "twoLine" }, cell: () => null },
|
||||
{ accessorKey: "email", header: "Email", cell: () => null },
|
||||
];
|
||||
render(<DataTable data={CHARLIE_ALICE_BOB} columns={columns} isLoading />);
|
||||
|
||||
const firstRow = screen.getAllByTestId("skeleton-row").at(0);
|
||||
expect(firstRow).toBeDefined();
|
||||
const bars = Array.from(firstRow?.querySelectorAll('[data-slot="skeleton"]') ?? []);
|
||||
|
||||
// twoLine column contributes a main + sub bar (2); the text column contributes 1
|
||||
expect(bars).toHaveLength(3);
|
||||
// per-column widths differ instead of every cell sharing one fixed width
|
||||
expect(new Set(bars.map((bar) => bar.className)).size).toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataTable column visibility", () => {
|
||||
|
|
|
|||
|
|
@ -333,26 +333,42 @@ function DefaultEmptyState() {
|
|||
);
|
||||
}
|
||||
|
||||
function SkeletonRows({
|
||||
const SKELETON_WIDTHS = ["w-[58%]", "w-[44%]", "w-[70%]", "w-[50%]", "w-[64%]", "w-[48%]"] as const;
|
||||
|
||||
function SkeletonCell<TData>({ column, index }: { column: Column<TData, unknown> | undefined; index: number }) {
|
||||
const meta = column?.columnDef.meta;
|
||||
const width = SKELETON_WIDTHS[index % SKELETON_WIDTHS.length];
|
||||
if (meta?.skeleton === "twoLine") {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className={cn("h-3.5", width)} />
|
||||
<Skeleton className="h-2.5 w-2/5 opacity-65" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Skeleton className={cn("h-3.5", width, meta?.numeric ? "ml-auto" : "")} />;
|
||||
}
|
||||
|
||||
function SkeletonRows<TData>({
|
||||
rowCount,
|
||||
columnCount,
|
||||
columns,
|
||||
size,
|
||||
message,
|
||||
}: {
|
||||
rowCount: number;
|
||||
columnCount: number;
|
||||
columns: readonly Column<TData, unknown>[];
|
||||
size: DataTableSize;
|
||||
message?: string;
|
||||
}) {
|
||||
const rowKeys = Array.from({ length: Math.max(rowCount, 1) }, (_, index) => index);
|
||||
const columnKeys = Array.from({ length: Math.max(columnCount, 1) }, (_, index) => index);
|
||||
const cells = columns.length > 0 ? columns : [undefined];
|
||||
return (
|
||||
<Fragment>
|
||||
{rowKeys.map((rowKey) => (
|
||||
<TableRow key={`skeleton-${rowKey}`} className="hover:bg-transparent" data-testid="skeleton-row">
|
||||
{columnKeys.map((columnKey) => (
|
||||
<TableCell key={columnKey} className={size === "compact" ? "px-2 py-1" : ""}>
|
||||
<Skeleton className="h-4 w-3/5" />
|
||||
{cells.map((column, columnKey) => (
|
||||
<TableCell key={column?.id ?? columnKey} className={size === "compact" ? "px-2 py-1" : ""}>
|
||||
<SkeletonCell column={column} index={columnKey} />
|
||||
{rowKey === 0 && columnKey === 0 && message !== undefined ? (
|
||||
<span className="sr-only">{message}</span>
|
||||
) : null}
|
||||
|
|
@ -521,7 +537,7 @@ export function DataTable<TData extends RowData, TValue>(props: DataTableProps<T
|
|||
return (
|
||||
<SkeletonRows
|
||||
rowCount={skeletonRowCount}
|
||||
columnCount={visibleColumnCount}
|
||||
columns={table.getVisibleLeafColumns()}
|
||||
size={size}
|
||||
message={loadingMessage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { RowData } from "@tanstack/react-table";
|
||||
|
||||
import type { ColumnPinnedSide } from "./types";
|
||||
import type { ColumnPinnedSide, DataTableSkeletonShape } from "./types";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
|
|
@ -9,5 +9,6 @@ declare module "@tanstack/react-table" {
|
|||
headerClassName?: string;
|
||||
title?: string;
|
||||
pinned?: ColumnPinnedSide;
|
||||
skeleton?: DataTableSkeletonShape;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export type FilterMode = "none" | "client" | "server";
|
|||
export type ColumnResizeMode = "onEnd" | "onChange";
|
||||
export type DataTableSize = "compact" | "default";
|
||||
export type ColumnPinnedSide = "left" | "right";
|
||||
export type DataTableSkeletonShape = "text" | "twoLine";
|
||||
|
||||
export interface DataTableProps<TData extends RowData, TValue> {
|
||||
data: TData[];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue