mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-06 17:46:23 +00:00
* fix(ui): prevent table cell text overflow - add default truncation with hover details for text cells in shared and static data tables to prevent content from spilling into adjacent columns. - adjust API key group, model, and IP restriction columns to fix badge overlap and left alignment drift. - reuse a shared truncated cell component and add width constraints for composite badge cells. * fix(table): prevent badge content from overflowing columns - make table text and badge cells shrink within constrained columns so long values truncate instead of bleeding into adjacent cells. - add a shared BadgeCell wrapper to keep badge alignment consistent across API keys and other list pages. - update affected list views to use constrained wrappers for group, provider, pricing, OAuth, and API info values.
240 lines
6.0 KiB
TypeScript
Vendored
240 lines
6.0 KiB
TypeScript
Vendored
/*
|
|
Copyright (C) 2023-2026 QuantumNous
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU Affero General Public License as
|
|
published by the Free Software Foundation, either version 3 of the
|
|
License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import * as React from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
import { TruncatedCell } from '../core/truncated-cell'
|
|
import { staticDataTableClassNames } from './static-data-table-classnames'
|
|
|
|
type StaticDataTableBaseProps = {
|
|
className?: string
|
|
tableClassName?: string
|
|
containerProps?: Omit<React.ComponentProps<'div'>, 'className' | 'children'>
|
|
tableProps?: Omit<
|
|
React.ComponentProps<typeof Table>,
|
|
'className' | 'children'
|
|
>
|
|
}
|
|
|
|
type StaticDataTableDataProps<TData = unknown> = StaticDataTableBaseProps & {
|
|
columns: StaticDataTableColumn<TData>[]
|
|
data: TData[]
|
|
getRowKey?: (row: TData, index: number) => React.Key
|
|
getRowClassName?: (row: TData, index: number) => string | undefined
|
|
renderRow?: (row: TData, index: number) => React.ReactNode
|
|
empty?: boolean
|
|
emptyContent?: React.ReactNode
|
|
emptyClassName?: string
|
|
headerRowClassName?: string
|
|
}
|
|
|
|
type StaticDataTableChildrenProps = StaticDataTableBaseProps & {
|
|
children: React.ReactNode
|
|
columns?: never
|
|
data?: never
|
|
}
|
|
|
|
type StaticDataTableProps<TData = unknown> =
|
|
| StaticDataTableDataProps<TData>
|
|
| StaticDataTableChildrenProps
|
|
|
|
export type StaticDataTableColumn<TData = unknown> = {
|
|
id: string
|
|
header: React.ReactNode
|
|
className?: string
|
|
cellClassName?: string | ((row: TData, index: number) => string | undefined)
|
|
cell?: (row: TData, index: number) => React.ReactNode
|
|
}
|
|
|
|
export function StaticDataTable<TData = unknown>(
|
|
props: StaticDataTableProps<TData>
|
|
) {
|
|
const { className, tableClassName, containerProps, tableProps } = props
|
|
|
|
return (
|
|
<div
|
|
className={cn(staticDataTableClassNames.container, className)}
|
|
{...containerProps}
|
|
>
|
|
<Table className={tableClassName} {...tableProps}>
|
|
{props.columns !== undefined ? (
|
|
<StaticDataTableWithColumns {...props} />
|
|
) : (
|
|
props.children
|
|
)}
|
|
</Table>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StaticDataTableWithColumns<TData>({
|
|
columns,
|
|
data,
|
|
getRowKey,
|
|
getRowClassName,
|
|
renderRow,
|
|
empty,
|
|
emptyContent,
|
|
emptyClassName,
|
|
headerRowClassName,
|
|
}: StaticDataTableDataProps<TData>) {
|
|
const isEmpty = empty ?? (data !== undefined && data.length === 0)
|
|
const bodyRows = data.map((row, index) => (
|
|
<StaticDataTableRow
|
|
key={getRowKey?.(row, index) ?? index}
|
|
row={row}
|
|
index={index}
|
|
columns={columns}
|
|
getRowClassName={getRowClassName}
|
|
renderRow={renderRow}
|
|
/>
|
|
))
|
|
|
|
return (
|
|
<>
|
|
<TableHeader>
|
|
<TableRow className={headerRowClassName}>
|
|
{columns.map((column) => (
|
|
<TableHead key={column.id} className={column.className}>
|
|
{column.header}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isEmpty ? (
|
|
<StaticDataTableEmptyRow
|
|
colSpan={columns.length}
|
|
className={emptyClassName}
|
|
>
|
|
{emptyContent}
|
|
</StaticDataTableEmptyRow>
|
|
) : (
|
|
bodyRows
|
|
)}
|
|
</TableBody>
|
|
</>
|
|
)
|
|
}
|
|
|
|
type StaticDataTableRowProps<TData> = Required<
|
|
Pick<StaticDataTableDataProps<TData>, 'columns'>
|
|
> &
|
|
Pick<StaticDataTableDataProps<TData>, 'getRowClassName' | 'renderRow'> & {
|
|
row: TData
|
|
index: number
|
|
}
|
|
|
|
function StaticDataTableRow<TData>({
|
|
row,
|
|
index,
|
|
columns,
|
|
getRowClassName,
|
|
renderRow,
|
|
}: StaticDataTableRowProps<TData>) {
|
|
if (renderRow) {
|
|
return <>{renderRow(row, index)}</>
|
|
}
|
|
|
|
return (
|
|
<TableRow className={getRowClassName?.(row, index)}>
|
|
{columns.map((column) => (
|
|
<TableCell
|
|
key={column.id}
|
|
className={cn(
|
|
'max-w-full min-w-0 overflow-hidden',
|
|
getStaticCellClassName(column, row, index)
|
|
)}
|
|
>
|
|
{renderStaticCellContent(column, row, index)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
)
|
|
}
|
|
|
|
function renderStaticCellContent<TData>(
|
|
column: StaticDataTableColumn<TData>,
|
|
row: TData,
|
|
index: number
|
|
) {
|
|
const content = column.cell?.(row, index)
|
|
const textContent = getPrimitiveTextContent(content)
|
|
|
|
if (!textContent) return content
|
|
|
|
return <TruncatedCell tooltipContent={textContent}>{content}</TruncatedCell>
|
|
}
|
|
|
|
function getPrimitiveTextContent(content: React.ReactNode): string | null {
|
|
if (typeof content === 'string' || typeof content === 'number') {
|
|
return String(content)
|
|
}
|
|
|
|
if (
|
|
React.isValidElement<{ children?: React.ReactNode }>(content) &&
|
|
(typeof content.props.children === 'string' ||
|
|
typeof content.props.children === 'number')
|
|
) {
|
|
return String(content.props.children)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function getStaticCellClassName<TData>(
|
|
column: StaticDataTableColumn<TData>,
|
|
row: TData,
|
|
index: number
|
|
) {
|
|
return typeof column.cellClassName === 'function'
|
|
? column.cellClassName(row, index)
|
|
: column.cellClassName
|
|
}
|
|
|
|
type StaticDataTableEmptyRowProps = {
|
|
colSpan: number
|
|
children: React.ReactNode
|
|
className?: string
|
|
}
|
|
|
|
function StaticDataTableEmptyRow({
|
|
colSpan,
|
|
children,
|
|
className,
|
|
}: StaticDataTableEmptyRowProps) {
|
|
return (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={colSpan}
|
|
className={cn('h-24 text-center', className)}
|
|
>
|
|
{children}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
}
|