fix(ui): order the auto-routers table newest first so a new router lands on page one (#38545)

/v2/model/info returns llm_router.model_list, which carries no defined order: the DB
read has no order_by and an edited deployment is popped and re-appended. The Auto
routers table rendered that order verbatim behind a ten-row first page, so on a proxy
with more than ten auto routers a router created moments ago was drawn wherever the
API happened to return it, in practice last, and read as never created

Adopt the ordering the rest of the dashboard already uses, with the two cases this
table has and its siblings do not. created_at is enterprise-gated and config.yaml
routers never carry one, so seeding created_at desc alone leaves every comparison
tied on a non-premium proxy and the fix a no-op. The column now declares
sortUndefined last, which table-core applies before the desc flip so undated rows
stay last in both directions, and the row emits undefined rather than null so that
branch is reachable at all. Name is the secondary key, giving the undated block a
defined order too

Page size is deliberately unchanged: it exposes the missing order rather than
causing it
This commit is contained in:
tin-berri 2026-08-27 11:30:48 -07:00 committed by GitHub
parent ca9007be39
commit 490face7de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 7 deletions

View file

@ -107,6 +107,33 @@ const mockDeploymentsPage = () => {
modelInfoCall.mockResolvedValue(pageOf(DEPLOYMENTS));
};
// Oldest-first, as the proxy returns them, and two more than the ten-row first page holds.
const BULK_ROUTER_NAMES = [
"router-01-oldest",
...Array.from({ length: 10 }, (_, i) => `router-${i + 2}`),
"router-12-newest",
];
const A_FULL_PAGE_AND_TWO_MORE = Array.from({ length: 12 }, (_, index) => ({
model_name: BULK_ROUTER_NAMES[index],
litellm_params: {
model: "auto_router/complexity_router",
complexity_router_config: { tiers: {}, classifier_type: "heuristic" },
},
model_info: {
id: `bulk-${index + 1}`,
db_model: true,
created_at: `2026-08-${String(index + 1).padStart(2, "0")}T00:00:00.000000+00:00`,
},
}));
/** Row order as rendered, header row dropped. */
const routerNamesInOrder = () =>
screen
.getAllByRole("row")
.slice(1)
.map((row) => row.querySelector("span.text-sm.font-medium")?.textContent ?? "");
const renderPanel = (canModify = true) =>
renderWithProviders(
<AutoRoutersPanel
@ -257,4 +284,32 @@ describe("AutoRoutersPanel", () => {
await screen.findByText("config-router");
expect(screen.queryByTestId("auto-router-actions-auto-4")).not.toBeInTheDocument();
});
// /v2/model/info returns an unordered model_list, and created_at is absent on config routers
// and on non-enterprise proxies, so both halves of the order have to be pinned here.
it("orders newest first, then the undated routers by name", async () => {
renderPanel();
await screen.findByText("tri-tier-router");
expect(routerNamesInOrder()).toEqual([
"tri-tier-router", // 2026-07-28
"support-router", // 2026-07-27
"adaptive-router", // undated, sorts after every dated row, then by name
"config-router",
]);
});
// The reported bug: the newest router was rendered last, so it landed on page 2 and read
// as never created.
it("puts a just-created router on the first page of a list longer than one page", async () => {
modelInfoCall.mockResolvedValue(pageOf(A_FULL_PAGE_AND_TWO_MORE));
renderPanel();
expect(await screen.findByRole("button", { name: "router-12-newest" })).toBeInTheDocument();
// Page one holds the ten newest, so the two oldest are the ones pushed off it.
expect(screen.queryByRole("button", { name: "router-01-oldest" })).not.toBeInTheDocument();
expect(routerNamesInOrder()[0]).toBe("router-12-newest");
});
});

View file

@ -1,7 +1,7 @@
"use client";
import { SortingState } from "@tanstack/react-table";
import { useMemo, useState } from "react";
import { useMemo } from "react";
import { DataTable } from "@/components/shared/DataTable";
import { AutoRouterIcon } from "@/components/shared/table_cells";
@ -19,6 +19,11 @@ interface AutoRoutersTableProps {
const PAGE_SIZE_OPTIONS = [10, 25, 50];
const DEFAULT_SORTING: SortingState = [
{ id: "createdAt", desc: true },
{ id: "name", desc: false },
];
function EmptyState({ canModify }: { canModify: boolean }) {
return (
<div className="flex flex-col items-center gap-1 py-6">
@ -42,8 +47,6 @@ export function AutoRoutersTable({
onRouterClick,
onDeleteClick,
}: AutoRoutersTableProps) {
const [sorting, setSorting] = useState<SortingState>([]);
const columns = useMemo(
() => getAutoRoutersTableColumns({ canModify, onRouterClick, onDeleteClick }),
[canModify, onRouterClick, onDeleteClick],
@ -55,8 +58,7 @@ export function AutoRoutersTable({
columns={columns}
getRowId={(router) => router.id}
sortingMode="client"
sorting={sorting}
onSortingChange={setSorting}
defaultSorting={DEFAULT_SORTING}
paginationMode="client"
pageSizeOptions={PAGE_SIZE_OPTIONS}
isLoading={isLoading}

View file

@ -155,6 +155,7 @@ export const getAutoRoutersTableColumns = ({
size: 150,
enableSorting: true,
sortingFn: "datetime",
sortUndefined: "last",
cell: ({ row }) => <DateCell value={row.original.createdAt} precision="date" />,
},
...(canModify

View file

@ -30,7 +30,8 @@ export interface AutoRouterRow {
editBlockedReason: EditBlockedReason | null;
targets: string[];
defaultModel: string | null;
createdAt: string | null;
/** `undefined`, not `null`: the table's `sortUndefined` pin only matches `undefined` */
createdAt: string | undefined;
deployment: AutoRouterDeployment;
}
@ -113,7 +114,7 @@ export const toAutoRouterRow = (
canEdit: canEdit && mayActOnRow,
canDelete: canDelete && mayActOnRow,
editBlockedReason,
createdAt: info.created_at ?? null,
createdAt: info.created_at ?? undefined,
defaultModel: (params[strategy.defaultModelKey] as string | null | undefined) ?? null,
deployment,
...PRESENTERS[strategy.kind](asRecord(params[strategy.configKey])),