mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
refactor(ui): reskin shared DataTable from tremor onto shadcn table primitives (#32209)
* test(ui): characterize DataTable behavior before shadcn reskin Pins the shared view_logs DataTable contract with library-agnostic queries ahead of the tremor-to-shadcn table migration: loading and empty states, TanStack column defs with custom cell renderers, onRowClick payload, both expansion render paths (colspan sub-component and sibling child rows), the getRowCanExpand gate, and client-side sorting on and off. These must pass unchanged after the reskin. * refactor(ui): reskin shared DataTable from tremor onto shadcn table primitives Swaps the view_logs DataTable's presentational layer from @tremor/react to the in-repo components/ui/table primitives and hardens the seam that every later table migration copies: - getRowId is injected instead of hardcoded to request_id through an any cast; identity defaults to the row index and the logs page now passes request_id explicitly, keeping expansion state attached to the right row across refetch reorders - one expansion render path: renderChildRows had zero consumers and is removed; renderSubComponent (colspan cell) is the single path - the four consumers passing dead no-op renderSubComponent and getRowCanExpand boilerplate drop it - loading and empty defaults become generic (Loading... / No results) instead of log-specific The characterization tests from the previous commit pass unchanged except the dead child-rows path test, replaced by a reorder-stability test for injected getRowId plus coverage of the new generic defaults. First tremor removal of the tables track; view_logs/table.tsx no longer imports @tremor/react. * test(ui): assert child rows hidden before expansion in DataTable test * fix(ui): suppress row hover on DataTable placeholder rows * feat(ui): polish DataTable with skeleton loading, header band, and numeric column alignment * feat(ui): shape DataTable skeletons per column and keep stale rows during refetch * revert(ui): drop DataTable skeleton loading, restore text loading row * fix(ui): clip DataTable to its rounded wrapper and right-align Duration/TTFT values
This commit is contained in:
parent
85d1fe6e2a
commit
e9e30dffb6
10 changed files with 124 additions and 78 deletions
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"@typescript-eslint/no-explicit-any": 1990,
|
||||
"@typescript-eslint/no-explicit-any": 1988,
|
||||
"complexity": 128,
|
||||
"max-depth": 59,
|
||||
"no-console": 15
|
||||
|
|
|
|||
|
|
@ -2077,11 +2077,6 @@
|
|||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/view_logs/table.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"src/components/view_user_spend.tsx": {
|
||||
"react-hooks/set-state-in-effect": {
|
||||
"count": 2
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
|
|||
const spendColumn = {
|
||||
header: "Spend (USD)",
|
||||
accessorKey: "spend",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => {
|
||||
const value = info.getValue();
|
||||
return value > 0 && value < 0.01 ? "<$0.01" : `$${formatNumberWithCommas(value, 2)}`;
|
||||
|
|
@ -247,13 +248,7 @@ const TopKeyView: React.FC<TopKeyViewProps> = ({ topKeys, teams, showTags = fals
|
|||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden max-h-[600px] overflow-y-auto">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={topKeys}
|
||||
renderSubComponent={() => <></>}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={false}
|
||||
/>
|
||||
<DataTable columns={columns} data={topKeys} isLoading={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
|
|||
{
|
||||
header: "Spend (USD)",
|
||||
accessorKey: "spend",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => {
|
||||
const value = info.getValue();
|
||||
return `$${formatNumberWithCommas(value, 2)}`;
|
||||
|
|
@ -38,16 +39,19 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
|
|||
{
|
||||
header: "Successful",
|
||||
accessorKey: "successful_requests",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => <span className="text-green-600">{info.getValue()?.toLocaleString() || 0}</span>,
|
||||
},
|
||||
{
|
||||
header: "Failed",
|
||||
accessorKey: "failed_requests",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => <span className="text-red-600">{info.getValue()?.toLocaleString() || 0}</span>,
|
||||
},
|
||||
{
|
||||
header: "Tokens",
|
||||
accessorKey: "tokens",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => info.getValue()?.toLocaleString() || 0,
|
||||
},
|
||||
];
|
||||
|
|
@ -99,13 +103,7 @@ export default function TopModelView({ topModels, topModelsLimit, setTopModelsLi
|
|||
</div>
|
||||
) : (
|
||||
<div className="border rounded-lg overflow-hidden max-h-[600px] overflow-y-auto">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={processedTopModels}
|
||||
renderSubComponent={() => <></>}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={false}
|
||||
/>
|
||||
<DataTable columns={columns} data={processedTopModels} isLoading={false} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -509,8 +509,6 @@ export function MCPToolsetsTab({ accessToken, userRole }: MCPToolsetsTabProps) {
|
|||
<DataTable
|
||||
data={toolsets}
|
||||
columns={columns}
|
||||
renderSubComponent={() => <div />}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={isLoading}
|
||||
noDataMessage="No toolsets yet. Click 'New Toolset' to create one."
|
||||
loadingMessage="Loading toolsets..."
|
||||
|
|
|
|||
|
|
@ -263,8 +263,6 @@ const PassThroughSettings: React.FC<GeneralSettingsPageProps> = ({
|
|||
<DataTable
|
||||
data={generalSettings}
|
||||
columns={columns}
|
||||
renderSubComponent={() => <div></div>}
|
||||
getRowCanExpand={() => false}
|
||||
isLoading={false}
|
||||
noDataMessage="No pass-through endpoints configured"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -231,13 +231,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
|
|||
: "Cost",
|
||||
accessorKey: "spend",
|
||||
size: 110,
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => {
|
||||
const row = info.row.original;
|
||||
const mcpCount = row.mcp_tool_call_count || 0;
|
||||
const mcpSpend = row.mcp_tool_call_spend || 0;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col items-end">
|
||||
<Tooltip title={`$${String(info.getValue() || 0)}`}>
|
||||
<span>{getSpendString(info.getValue() || 0)}</span>
|
||||
</Tooltip>
|
||||
|
|
@ -263,13 +264,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
|
|||
)
|
||||
: "Duration (s)",
|
||||
accessorKey: "request_duration_ms",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => {
|
||||
const ms = info.getValue();
|
||||
if (ms == null) return <span>-</span>;
|
||||
const seconds = (ms / 1000).toFixed(2);
|
||||
return (
|
||||
<Tooltip title={`${ms}ms`}>
|
||||
<span className="max-w-[15ch] truncate block">{seconds}</span>
|
||||
<span className="max-w-[15ch] truncate inline-block">{seconds}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
|
@ -287,6 +289,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
|
|||
)
|
||||
: "TTFT (s)",
|
||||
accessorKey: "completionStartTime",
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => {
|
||||
const row = info.row.original;
|
||||
const completionStartTime = info.getValue();
|
||||
|
|
@ -298,7 +301,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
|
|||
const ttftSeconds = (ttftMs / 1000).toFixed(2);
|
||||
return (
|
||||
<Tooltip title={`${ttftMs}ms`}>
|
||||
<span className="max-w-[15ch] truncate block">{ttftSeconds}</span>
|
||||
<span className="max-w-[15ch] truncate inline-block">{ttftSeconds}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
|
|
@ -395,6 +398,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
|
|||
: "Tokens",
|
||||
accessorKey: "total_tokens",
|
||||
size: 140,
|
||||
meta: { numeric: true },
|
||||
cell: (info: any) => {
|
||||
const row = info.row.original;
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -287,6 +287,7 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
|
|||
<DataTable
|
||||
columns={columns}
|
||||
data={deferredData}
|
||||
getRowId={(row) => row.request_id}
|
||||
onRowClick={handleRowClick}
|
||||
isLoading={isLogsLoading}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,35 @@ describe("DataTable states", () => {
|
|||
expect(screen.getByText("Nothing here")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("falls back to generic loading and empty defaults", () => {
|
||||
const { rerender } = render(<DataTable data={data} columns={unsizedColumns} isLoading />);
|
||||
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
||||
|
||||
rerender(<DataTable data={[]} columns={unsizedColumns} />);
|
||||
expect(screen.getByText("No results")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("suppresses the primitive's row hover on loading, empty, and expansion placeholder rows", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { rerender } = render(<DataTable data={data} columns={unsizedColumns} isLoading />);
|
||||
expect(screen.getByText("Loading...").closest("tr")).toHaveClass("hover:bg-transparent");
|
||||
|
||||
rerender(<DataTable data={[]} columns={unsizedColumns} />);
|
||||
expect(screen.getByText("No results").closest("tr")).toHaveClass("hover:bg-transparent");
|
||||
|
||||
rerender(
|
||||
<DataTable
|
||||
data={data}
|
||||
columns={[expanderColumn, ...unsizedColumns]}
|
||||
getRowCanExpand={() => true}
|
||||
renderSubComponent={({ row }) => <div>details for {row.original.request_id}</div>}
|
||||
/>,
|
||||
);
|
||||
await user.click(screen.getByRole("button", { name: "expand r1" }));
|
||||
expect(screen.getByText("details for r1").closest("tr")).toHaveClass("hover:bg-transparent");
|
||||
expect(screen.getByText("alpha").closest("tr")).not.toHaveClass("hover:bg-transparent");
|
||||
});
|
||||
|
||||
it("renders row data through plain TanStack column defs, including custom cell renderers", () => {
|
||||
const columns: ColumnDef<Row>[] = [
|
||||
{ header: "A", accessorKey: "a" },
|
||||
|
|
@ -84,6 +113,29 @@ describe("DataTable states", () => {
|
|||
expect(screen.getByText("alpha")).toBeInTheDocument();
|
||||
expect(screen.getByText("custom:beta")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("clips the table to the rounded wrapper so the header band cannot bleed past the corners", () => {
|
||||
const { container } = render(<DataTable data={data} columns={unsizedColumns} />);
|
||||
|
||||
const wrapper = container.firstElementChild;
|
||||
expect(wrapper).toHaveClass("rounded-lg", "overflow-hidden");
|
||||
});
|
||||
|
||||
it("right-aligns headers and cells with tabular figures for numeric meta columns", () => {
|
||||
const columns: ColumnDef<Row>[] = [
|
||||
{ header: "A", accessorKey: "a" },
|
||||
{ header: "B", accessorKey: "b", meta: { numeric: true } },
|
||||
];
|
||||
render(<DataTable data={data} columns={columns} />);
|
||||
|
||||
const headers = screen.getAllByRole("columnheader");
|
||||
expect(headers[1].querySelector("div")).toHaveClass("justify-end");
|
||||
expect(headers[0].querySelector("div")).not.toHaveClass("justify-end");
|
||||
|
||||
const cells = screen.getAllByRole("cell");
|
||||
expect(cells[1]).toHaveClass("text-right", "tabular-nums");
|
||||
expect(cells[0]).not.toHaveClass("text-right");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataTable row interaction", () => {
|
||||
|
|
@ -129,28 +181,33 @@ describe("DataTable expansion", () => {
|
|||
expect(screen.queryByText("details for r1")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders child rows as sibling table rows (child-rows path)", async () => {
|
||||
it("keeps expansion attached to the same row through data reorders when getRowId is injected", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
const { rerender } = render(
|
||||
<DataTable
|
||||
data={rows}
|
||||
columns={[expanderColumn, ...unsizedColumns]}
|
||||
getRowId={(row) => row.request_id}
|
||||
getRowCanExpand={() => true}
|
||||
renderChildRows={({ row }) => (
|
||||
<tr>
|
||||
<td colSpan={3}>child of {row.original.request_id}</td>
|
||||
</tr>
|
||||
)}
|
||||
renderSubComponent={({ row }) => <div>details for {row.original.request_id}</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText("child of r2")).not.toBeInTheDocument();
|
||||
await user.click(screen.getByRole("button", { name: "expand r1" }));
|
||||
expect(screen.getByText("details for r1")).toBeInTheDocument();
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "expand r2" }));
|
||||
rerender(
|
||||
<DataTable
|
||||
data={[...rows].reverse()}
|
||||
columns={[expanderColumn, ...unsizedColumns]}
|
||||
getRowId={(row) => row.request_id}
|
||||
getRowCanExpand={() => true}
|
||||
renderSubComponent={({ row }) => <div>details for {row.original.request_id}</div>}
|
||||
/>,
|
||||
);
|
||||
|
||||
const childCell = screen.getByText("child of r2");
|
||||
expect(childCell.closest("tr")).not.toBeNull();
|
||||
expect(within(screen.getByRole("table")).getByText("child of r2")).toBeInTheDocument();
|
||||
expect(screen.getByText("details for r1")).toBeInTheDocument();
|
||||
expect(screen.queryByText("details for r2")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not expand rows when getRowCanExpand is missing even if a renderer is provided", () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Fragment, useState } from "react";
|
||||
import {
|
||||
ColumnDef,
|
||||
RowData,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getExpandedRowModel,
|
||||
|
|
@ -10,16 +11,21 @@ import {
|
|||
SortingState,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react";
|
||||
import { Table, TableHeader, TableHead, TableBody, TableRow, TableCell } from "@/components/ui/table";
|
||||
|
||||
declare module "@tanstack/react-table" {
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
numeric?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
data: TData[];
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
getRowId?: (row: TData, index: number) => string;
|
||||
onRowClick?: (row: TData) => void;
|
||||
/** Renders inside a single colspan cell (used by audit logs) */
|
||||
/** Renders inside a single colspan cell */
|
||||
renderSubComponent?: (props: { row: Row<TData> }) => React.ReactElement;
|
||||
/** Renders directly in tbody as sibling table rows (used by MCP children) */
|
||||
renderChildRows?: (props: { row: Row<TData> }) => React.ReactNode;
|
||||
getRowCanExpand?: (row: Row<TData>) => boolean;
|
||||
isLoading?: boolean;
|
||||
loadingMessage?: string;
|
||||
|
|
@ -31,16 +37,16 @@ interface DataTableProps<TData, TValue> {
|
|||
export function DataTable<TData, TValue>({
|
||||
data = [],
|
||||
columns,
|
||||
getRowId,
|
||||
onRowClick,
|
||||
renderSubComponent,
|
||||
renderChildRows,
|
||||
getRowCanExpand,
|
||||
isLoading = false,
|
||||
loadingMessage = "🚅 Loading logs...",
|
||||
noDataMessage = "No logs found",
|
||||
loadingMessage = "Loading...",
|
||||
noDataMessage = "No results",
|
||||
enableSorting = false,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const supportsExpansion = !!(renderSubComponent || renderChildRows) && !!getRowCanExpand;
|
||||
const supportsExpansion = !!renderSubComponent && !!getRowCanExpand;
|
||||
const hasExplicitColumnSizes = columns.some((column) => column.size !== undefined);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
|
|
@ -55,58 +61,56 @@ export function DataTable<TData, TValue>({
|
|||
enableSortingRemoval: false,
|
||||
}),
|
||||
...(supportsExpansion && { getRowCanExpand }),
|
||||
getRowId: (row: TData, index: number) => {
|
||||
const _row: any = row as any;
|
||||
return _row?.request_id ?? String(index);
|
||||
},
|
||||
...(getRowId && { getRowId }),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...(enableSorting && { getSortedRowModel: getSortedRowModel() }),
|
||||
...(supportsExpansion && { getExpandedRowModel: getExpandedRowModel() }),
|
||||
});
|
||||
|
||||
const tableClassName = hasExplicitColumnSizes
|
||||
? "[&_td]:py-0.5 [&_th]:py-1 [&_table]:table-fixed"
|
||||
: "[&_td]:py-0.5 [&_th]:py-1 table-fixed w-full box-border";
|
||||
const tableClassName = hasExplicitColumnSizes ? "table-fixed" : "table-fixed w-full box-border";
|
||||
const tableStyle = hasExplicitColumnSizes ? { minWidth: table.getCenterTotalSize() } : { minWidth: "400px" };
|
||||
|
||||
return (
|
||||
<div className="rounded-lg custom-border overflow-x-auto w-full max-w-full box-border">
|
||||
<div className="rounded-lg custom-border overflow-hidden w-full max-w-full box-border">
|
||||
<Table className={tableClassName} style={tableStyle}>
|
||||
<TableHead>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
<TableRow key={headerGroup.id} className="bg-muted/50 hover:bg-muted/50">
|
||||
{headerGroup.headers.map((header) => {
|
||||
const canSort = enableSorting && header.column.getCanSort();
|
||||
const isSorted = header.column.getIsSorted();
|
||||
const numeric = header.column.columnDef.meta?.numeric;
|
||||
|
||||
return (
|
||||
<TableHeaderCell
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className={`py-1 h-8 ${canSort ? "cursor-pointer select-none hover:bg-gray-50" : ""}`}
|
||||
className={`py-1 h-8 text-xs font-medium text-muted-foreground first:pl-4 last:pr-4 ${
|
||||
canSort ? "cursor-pointer select-none hover:bg-muted" : ""
|
||||
}`}
|
||||
style={hasExplicitColumnSizes ? { width: header.getSize() } : undefined}
|
||||
onClick={canSort ? header.column.getToggleSortingHandler() : undefined}
|
||||
>
|
||||
{header.isPlaceholder ? null : (
|
||||
<div className="flex items-center gap-1">
|
||||
<div className={`flex items-center gap-1 ${numeric ? "justify-end" : ""}`}>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{canSort && (
|
||||
<span className="text-gray-400">
|
||||
<span className="text-muted-foreground">
|
||||
{isSorted === "asc" ? "↑" : isSorted === "desc" ? "↓" : "⇅"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</TableHeaderCell>
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHead>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
<TableRow>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>{loadingMessage}</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -115,13 +119,15 @@ export function DataTable<TData, TValue>({
|
|||
table.getRowModel().rows.map((row) => (
|
||||
<Fragment key={row.id}>
|
||||
<TableRow
|
||||
className={`h-8 ${onRowClick ? "cursor-pointer hover:bg-gray-50" : ""}`}
|
||||
className={`h-8 ${onRowClick ? "cursor-pointer" : ""}`}
|
||||
onClick={() => onRowClick?.(row.original)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
className={`py-0.5 max-h-8 overflow-hidden text-ellipsis whitespace-nowrap first:pl-4 last:pr-4 ${
|
||||
cell.column.columnDef.meta?.numeric ? "text-right tabular-nums" : ""
|
||||
}`}
|
||||
style={hasExplicitColumnSizes ? { width: cell.column.getSize() } : undefined}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
|
|
@ -129,12 +135,8 @@ export function DataTable<TData, TValue>({
|
|||
))}
|
||||
</TableRow>
|
||||
|
||||
{/* Child rows rendered as real table rows (MCP children) */}
|
||||
{supportsExpansion && row.getIsExpanded() && renderChildRows && renderChildRows({ row })}
|
||||
|
||||
{/* Legacy sub-component in colspan cell (audit logs) */}
|
||||
{supportsExpansion && row.getIsExpanded() && renderSubComponent && !renderChildRows && (
|
||||
<TableRow>
|
||||
{supportsExpansion && row.getIsExpanded() && renderSubComponent && (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={row.getVisibleCells().length} className="p-0">
|
||||
<div className="w-full max-w-full overflow-hidden box-border">{renderSubComponent({ row })}</div>
|
||||
</TableCell>
|
||||
|
|
@ -143,11 +145,9 @@ export function DataTable<TData, TValue>({
|
|||
</Fragment>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-8 text-center">
|
||||
<div className="text-center text-gray-500">
|
||||
<p>{noDataMessage}</p>
|
||||
</div>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center align-middle">
|
||||
<p className="text-sm text-muted-foreground">{noDataMessage}</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue