diff --git a/e2e/request-logs-column-reorder.spec.ts b/e2e/request-logs-column-reorder.spec.ts
index 61e1941b..d6f596fa 100644
--- a/e2e/request-logs-column-reorder.spec.ts
+++ b/e2e/request-logs-column-reorder.spec.ts
@@ -247,16 +247,18 @@ test("Request Logs: response metrics column resize clamps at its minimum width",
await page.waitForTimeout(80);
const during = await readResponseMetricsColumnState(page);
- expect(during.width).toBeGreaterThanOrEqual(183);
- expect(during.width).toBeLessThanOrEqual(185);
+ expect(during.width).toBeGreaterThanOrEqual(239);
+ expect(during.width).toBeLessThanOrEqual(241);
expect(during.text).toMatch(/Streaming|流式/);
+ expect(during.text).toMatch(/First Token|首 Token/);
+ expect(during.text).toContain("90ms");
expect(during.text).not.toContain("--");
expect(during.chipsStayInsideCell).toBe(true);
await page.mouse.up();
const after = await readResponseMetricsColumnState(page);
- expect(after.storedLatencyWidth).toBe(184);
+ expect(after.storedLatencyWidth).toBe(240);
});
test("Request Logs: column reorder follows the pointer and auto-scrolls horizontally", async ({
diff --git a/features/request-log-viewer/requestLogsShared.tsx b/features/request-log-viewer/requestLogsShared.tsx
index d1970c77..490de9e3 100644
--- a/features/request-log-viewer/requestLogsShared.tsx
+++ b/features/request-log-viewer/requestLogsShared.tsx
@@ -96,22 +96,27 @@ const resolveLatencyToneClasses = (latencyText: string): string => {
function RequestLogMetricChip({
ariaLabel,
+ label,
value,
className,
}: {
ariaLabel: string;
+ label?: string;
value: string;
className: string;
}) {
return (
- {value}
+ {label ? (
+ {label}
+ ) : null}
+ {value}
);
}
@@ -396,8 +401,8 @@ export function buildRequestLogsColumns(
{
key: "latency",
label: t("request_logs.col_response_metrics"),
- width: "w-52",
- minWidthPx: 184,
+ width: "w-64",
+ minWidthPx: 240,
headerClassName: "text-center",
cellClassName: "text-center text-xs tabular-nums text-slate-700 dark:text-slate-200",
render: (row) => {
@@ -417,16 +422,25 @@ export function buildRequestLogsColumns(
content={tooltipLines.join("\n")}
disabled={tooltipLines.length === 0}
placement="bottom"
- className="max-w-full justify-center"
+ className="block max-w-full"
>
-
+
{hasLatency ? (
) : null}
+ {hasFirstToken ? (
+
+ ) : null}
{
headerClassName?: string;
/** Extra cell class */
cellClassName?: string;
+ /** Extra class for the inner cell content wrapper. */
+ cellContentClassName?: string;
/** Overflow tooltip text for a truncated cell. Primitive render output is used by default. */
overflowTooltip?: boolean | ((row: T, index: number) => string | null | undefined);
/** Custom header render function (overrides label) */
@@ -246,8 +248,19 @@ function calculateScrollbarThumbs(scrollMetrics: ScrollMetrics, headerHeight: nu
return { vThumb: v, hThumb: h };
}
+function parseArbitraryMinWidthPx(widthClassName?: string) {
+ const match = widthClassName?.match(/(?:^|\s)min-w-\[(\d+(?:\.\d+)?)px\](?:\s|$)/);
+ if (!match) return null;
+ const value = Number(match[1]);
+ return Number.isFinite(value) && value > 0 ? Math.round(value) : null;
+}
+
+function resolveColumnMinWidth(column: DataTableColumn) {
+ return column.minWidthPx ?? parseArbitraryMinWidthPx(column.width) ?? DEFAULT_MIN_COLUMN_WIDTH;
+}
+
function clampColumnWidth(column: DataTableColumn, width: number) {
- const minWidth = column.minWidthPx ?? DEFAULT_MIN_COLUMN_WIDTH;
+ const minWidth = resolveColumnMinWidth(column);
const maxWidth = column.maxWidthPx ?? DEFAULT_MAX_COLUMN_WIDTH;
return Math.max(minWidth, Math.min(maxWidth, Math.round(width)));
}
@@ -1207,7 +1220,7 @@ export function DataTable({
const rect = headerCell.getBoundingClientRect();
const containerRect = containerRef.current?.getBoundingClientRect();
const startWidth = rect.width;
- const minWidth = column.minWidthPx ?? DEFAULT_MIN_COLUMN_WIDTH;
+ const minWidth = resolveColumnMinWidth(column);
const maxWidth = column.maxWidthPx ?? DEFAULT_MAX_COLUMN_WIDTH;
const nextStartWidth = Math.max(minWidth, Math.min(maxWidth, startWidth));
@@ -2199,7 +2212,12 @@ export function DataTable({
>
{
headerClassName?: string;
/** Extra cell class */
cellClassName?: string;
+ /** Extra class for the inner cell content wrapper. */
+ cellContentClassName?: string;
/** Overflow tooltip text for a truncated cell. Primitive render output is used by default. */
overflowTooltip?: boolean | ((row: T, index: number) => string | null | undefined);
/** Custom header render function (overrides label) */
diff --git a/packages/ui/src/data-table/__tests__/tableStorage.test.ts b/packages/ui/src/data-table/__tests__/tableStorage.test.ts
new file mode 100644
index 00000000..a8567010
--- /dev/null
+++ b/packages/ui/src/data-table/__tests__/tableStorage.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, test } from "vitest";
+import type { DataTableColumn } from "../DataTable.types";
+import { clampColumnWidth } from "../tableStorage";
+
+describe("tableStorage", () => {
+ test("clamps stored widths to arbitrary min-width classes", () => {
+ const column: DataTableColumn<{ key: string }> = {
+ key: "key",
+ label: "Key",
+ width: "w-[320px] min-w-[320px]",
+ render: () => null,
+ };
+
+ expect(clampColumnWidth(column, 88)).toBe(320);
+ });
+
+ test("lets explicit minWidthPx override width class inference", () => {
+ const column: DataTableColumn<{ key: string }> = {
+ key: "key",
+ label: "Key",
+ width: "w-[320px] min-w-[320px]",
+ minWidthPx: 180,
+ render: () => null,
+ };
+
+ expect(clampColumnWidth(column, 88)).toBe(180);
+ });
+});
diff --git a/packages/ui/src/data-table/tableStorage.ts b/packages/ui/src/data-table/tableStorage.ts
index db99aaf3..efa11b0b 100644
--- a/packages/ui/src/data-table/tableStorage.ts
+++ b/packages/ui/src/data-table/tableStorage.ts
@@ -128,8 +128,19 @@ export function calculateScrollbarThumbs(scrollMetrics: ScrollMetrics, headerHei
return { vThumb: v, hThumb: h };
}
+function parseArbitraryMinWidthPx(widthClassName?: string) {
+ const match = widthClassName?.match(/(?:^|\s)min-w-\[(\d+(?:\.\d+)?)px\](?:\s|$)/);
+ if (!match) return null;
+ const value = Number(match[1]);
+ return Number.isFinite(value) && value > 0 ? Math.round(value) : null;
+}
+
+function resolveColumnMinWidth(column: DataTableColumn) {
+ return column.minWidthPx ?? parseArbitraryMinWidthPx(column.width) ?? DEFAULT_MIN_COLUMN_WIDTH;
+}
+
export function clampColumnWidth(column: DataTableColumn, width: number) {
- const minWidth = column.minWidthPx ?? DEFAULT_MIN_COLUMN_WIDTH;
+ const minWidth = resolveColumnMinWidth(column);
const maxWidth = column.maxWidthPx ?? DEFAULT_MAX_COLUMN_WIDTH;
return Math.max(minWidth, Math.min(maxWidth, Math.round(width)));
}
diff --git a/pages/api-keys/components/ApiKeyColumns.tsx b/pages/api-keys/components/ApiKeyColumns.tsx
index f83fc10a..aae87d63 100644
--- a/pages/api-keys/components/ApiKeyColumns.tsx
+++ b/pages/api-keys/components/ApiKeyColumns.tsx
@@ -31,6 +31,17 @@ type CreateApiKeyColumnsOptions = {
onDelete: (index: number) => void;
};
+function ApiKeyPermissionAllValue({ label }: { label: string }) {
+ return (
+
+
+
+ {label}
+
+
+ );
+}
+
export const createApiKeyColumns = ({
t,
onToggleDisable,
@@ -84,12 +95,17 @@ export const createApiKeyColumns = ({
key: "key",
label: t("api_keys_page.col_key"),
width: "w-[320px] min-w-[320px]",
- cellClassName: "whitespace-nowrap",
- render: (row) => (
-
- {maskApiKey(row.key)}
-
- ),
+ cellClassName: "min-w-0 whitespace-nowrap",
+ render: (row) => {
+ const maskedKey = maskApiKey(row.key);
+ return (
+
+
+ {maskedKey}
+
+
+ );
+ },
},
{
key: "dailyLimit",
@@ -218,9 +234,9 @@ export const createApiKeyColumns = ({
))}
}
- className="block min-w-0"
+ className="!flex min-w-0 max-w-full overflow-hidden"
>
-
+
{row["allowed-models"].length}
@@ -230,9 +246,7 @@ export const createApiKeyColumns = ({
) : (
-
- {t("api_keys_page.all_models")}
-
+
),
},
{
@@ -255,9 +269,9 @@ export const createApiKeyColumns = ({
))}
}
- className="block min-w-0"
+ className="!flex min-w-0 max-w-full overflow-hidden"
>
-
+
{row["allowed-channel-groups"].length}
@@ -267,9 +281,7 @@ export const createApiKeyColumns = ({
) : (
-
- {t("api_keys_page.all_channel_groups")}
-
+
),
},
{
@@ -292,9 +304,9 @@ export const createApiKeyColumns = ({
))}
}
- className="block min-w-0"
+ className="!flex min-w-0 max-w-full overflow-hidden"
>
-
+
{row["allowed-channels"].length}
@@ -304,9 +316,7 @@ export const createApiKeyColumns = ({
) : (
-
- {t("api_keys_page.all_channels")}
-
+
),
},
{
diff --git a/pages/api-keys/components/__tests__/ApiKeyColumns.test.tsx b/pages/api-keys/components/__tests__/ApiKeyColumns.test.tsx
index 682db020..69a6a6e0 100644
--- a/pages/api-keys/components/__tests__/ApiKeyColumns.test.tsx
+++ b/pages/api-keys/components/__tests__/ApiKeyColumns.test.tsx
@@ -101,6 +101,67 @@ describe("ApiKeyColumns", () => {
expect(keyColumn?.width).toBe("w-[320px] min-w-[320px]");
});
+ test("renders key and unrestricted permission cells with bounded truncation", () => {
+ const row: ApiKeyEntry = {
+ key: "sk-team-a-abcdefghijklmnopqrstuvwxyz1234567890",
+ name: "Test key",
+ "created-at": "2026-04-28T00:00:00Z",
+ };
+ const columns = createApiKeyColumns({
+ t,
+ onCopy: vi.fn(),
+ onDelete: vi.fn(),
+ onEdit: vi.fn(),
+ onImportToCcSwitch: vi.fn(),
+ onToggleDisable: vi.fn(),
+ onViewUsage: vi.fn(),
+ });
+ const keyColumn = columns.find((column) => column.key === "key");
+ const modelsColumn = columns.find((column) => column.key === "allowedModels");
+
+ const { container } = render(
+
+
{keyColumn?.render(row, 0)}
+
{modelsColumn?.render(row, 0)}
+
,
+ );
+
+ const code = container.querySelector("code");
+ expect(code).toHaveClass("max-w-full");
+ expect(code).toHaveClass("truncate");
+ expect(screen.getByText("api_keys_page.all_models")).toHaveClass("truncate");
+ });
+
+ test("keeps restricted permission summaries bounded inside the cell", () => {
+ const row: ApiKeyEntry = {
+ key: "sk-team-a-abcdefghijklmnopqrstuvwxyz1234567890",
+ name: "Test key",
+ "created-at": "2026-04-28T00:00:00Z",
+ "allowed-models": ["deepseek-r1-ultra-long-name", "gpt-5.3-codex"],
+ };
+ const columns = createApiKeyColumns({
+ t,
+ onCopy: vi.fn(),
+ onDelete: vi.fn(),
+ onEdit: vi.fn(),
+ onImportToCcSwitch: vi.fn(),
+ onToggleDisable: vi.fn(),
+ onViewUsage: vi.fn(),
+ });
+ const modelsColumn = columns.find((column) => column.key === "allowedModels");
+
+ const { container } = render({modelsColumn?.render(row, 0)}
);
+ const trigger = container.querySelector("[data-tooltip-managed='true']");
+ const summary = container.querySelector("span.flex.max-w-full.overflow-hidden");
+
+ expect(trigger).toHaveClass("!flex");
+ expect(trigger).toHaveClass("max-w-full");
+ expect(trigger).toHaveClass("overflow-hidden");
+ expect(summary).toHaveClass("flex");
+ expect(summary).toHaveClass("min-w-0");
+ expect(screen.getByText("deepseek-r1-ultra-long-name")).toHaveClass("truncate");
+ });
+
test("shows API key spending limits as a dedicated cost column", async () => {
const row: ApiKeyEntry = {
key: "sk-test",
diff --git a/pages/monitor/__tests__/requestLogsShared.test.ts b/pages/monitor/__tests__/requestLogsShared.test.ts
index 4540d290..a50b4397 100644
--- a/pages/monitor/__tests__/requestLogsShared.test.ts
+++ b/pages/monitor/__tests__/requestLogsShared.test.ts
@@ -65,7 +65,7 @@ describe("requestLogsShared", () => {
expect(columns.find((column) => column.key === "latency")?.label).toBe(
"request_logs.col_response_metrics",
);
- expect(columns.find((column) => column.key === "latency")?.minWidthPx).toBe(184);
+ expect(columns.find((column) => column.key === "latency")?.minWidthPx).toBe(240);
expect(keys.indexOf("latency")).toBeLessThan(keys.indexOf("apiKeyName"));
expect(keys.indexOf("inputTokens")).toBeLessThan(keys.indexOf("apiKeyName"));
expect(keys.indexOf("cachedTokens")).toBeLessThan(keys.indexOf("model"));
diff --git a/pages/request-logs/__tests__/RequestLogsPage.test.tsx b/pages/request-logs/__tests__/RequestLogsPage.test.tsx
index 3ac8daa3..e645a503 100644
--- a/pages/request-logs/__tests__/RequestLogsPage.test.tsx
+++ b/pages/request-logs/__tests__/RequestLogsPage.test.tsx
@@ -153,7 +153,7 @@ describe("RequestLogsPage", () => {
mocks.clearUsageLogs.mockReset();
});
- test("renders first token latency in the response metrics tooltip", async () => {
+ test("renders first token latency in the response metrics cell and tooltip", async () => {
await i18n.changeLanguage("en");
const user = userEvent.setup();
@@ -209,9 +209,10 @@ describe("RequestLogsPage", () => {
expect(within(table).getByRole("columnheader", { name: "Response Metrics" })).toBeInTheDocument();
expect(within(table).getByText("Streaming")).toBeInTheDocument();
expect(within(table).getByText("1.20s")).toBeInTheDocument();
- expect(within(table).queryByText("183ms")).not.toBeInTheDocument();
+ expect(within(table).getByText("First Token")).toBeInTheDocument();
+ expect(within(table).getByText("183ms")).toBeInTheDocument();
- await user.hover(within(table).getByLabelText("Duration: 1.20s"));
+ await user.hover(within(table).getByLabelText("First Token: 183ms"));
expect(await screen.findByRole("tooltip")).toHaveTextContent("First Token: 183ms");
});