Commit 3c1bb0a7 by QuentinHsu Committed by GitHub

fix(web): prevent list cell text and badge overflow (#5510)

* 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.
parent 1ac0f580
/*
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'
type BadgeCellProps = React.HTMLAttributes<HTMLDivElement>
export function BadgeCell({ className, ...props }: BadgeCellProps) {
return (
<div
className={cn(
'-ml-1.5 flex max-w-full min-w-0 items-center gap-1 overflow-hidden [&_[data-slot=status-badge]]:max-w-full [&_[data-slot=status-badge]]:min-w-0',
className
)}
{...props}
/>
)
}
...@@ -17,13 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,13 +17,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import * as React from 'react' import * as React from 'react'
import { StatusBadgeList } from '@/components/status-badge'
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { StatusBadgeList } from '@/components/status-badge'
interface BadgeListCellProps { interface BadgeListCellProps {
items: React.ReactNode[] items: React.ReactNode[]
...@@ -50,7 +50,7 @@ export function BadgeListCell({ ...@@ -50,7 +50,7 @@ export function BadgeListCell({
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger render={<div className='-ml-1.5' />}> <TooltipTrigger render={<div className='-ml-1.5 max-w-full' />}>
<StatusBadgeList <StatusBadgeList
items={items} items={items}
max={max} max={max}
......
...@@ -17,8 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,8 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import * as React from 'react' import * as React from 'react'
import { flexRender, type Row } from '@tanstack/react-table' import { flexRender, type Cell, type Row } from '@tanstack/react-table'
import { cn } from '@/lib/utils'
import { TableCell, TableRow } from '@/components/ui/table' import { TableCell, TableRow } from '@/components/ui/table'
import { TruncatedCell } from './truncated-cell'
import type { DataTableColumnClassName } from './types' import type { DataTableColumnClassName } from './types'
type DataTableRowProps<TData> = { type DataTableRowProps<TData> = {
...@@ -42,9 +44,12 @@ function DataTableRowInner<TData>({ ...@@ -42,9 +44,12 @@ function DataTableRowInner<TData>({
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell <TableCell
key={cell.id} key={cell.id}
className={getColumnClassName?.(cell.column.id, 'cell')} className={cn(
'max-w-full min-w-0 overflow-hidden',
getColumnClassName?.(cell.column.id, 'cell')
)}
> >
{flexRender(cell.column.columnDef.cell, cell.getContext())} {renderCellContent(cell)}
</TableCell> </TableCell>
))} ))}
</TableRow> </TableRow>
...@@ -61,3 +66,28 @@ export const DataTableRow = React.memo(DataTableRowInner, (prev, next) => { ...@@ -61,3 +66,28 @@ export const DataTableRow = React.memo(DataTableRowInner, (prev, next) => {
prev.row.getIsSelected() === next.row.getIsSelected() prev.row.getIsSelected() === next.row.getIsSelected()
) )
}) as typeof DataTableRowInner }) as typeof DataTableRowInner
function renderCellContent<TData>(cell: Cell<TData, unknown>) {
const content = flexRender(cell.column.columnDef.cell, cell.getContext())
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
}
/*
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 {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
type TruncatedCellProps = {
children: React.ReactNode
cellClassName?: string
className?: string
contentClassName?: string
side?: 'top' | 'bottom' | 'left' | 'right'
tooltipClassName?: string
tooltipContent?: React.ReactNode
}
export function TruncatedCell({
children,
cellClassName,
className,
contentClassName,
side = 'top',
tooltipClassName,
tooltipContent,
}: TruncatedCellProps) {
const content = tooltipContent ?? getTextContent(children)
if (!content) {
return (
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
>
{children}
</div>
)
}
return (
<Tooltip>
<TooltipTrigger
render={
<div
className={cn(
'block max-w-full min-w-0 truncate',
cellClassName,
className
)}
/>
}
>
<div className={cn('truncate', contentClassName)}>{children}</div>
</TooltipTrigger>
<TooltipContent
side={side}
className={cn('max-w-xs break-all', tooltipClassName)}
>
{content}
</TooltipContent>
</Tooltip>
)
}
function getTextContent(node: React.ReactNode): string {
if (typeof node === 'string' || typeof node === 'number') return String(node)
if (Array.isArray(node)) return node.map(getTextContent).join('')
return ''
}
...@@ -18,7 +18,9 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -18,7 +18,9 @@ For commercial licensing, please contact support@quantumnous.com
*/ */
export { DataTablePagination } from './core/pagination' export { DataTablePagination } from './core/pagination'
export { DataTableColumnHeader } from './core/column-header' export { DataTableColumnHeader } from './core/column-header'
export { BadgeCell } from './core/badge-cell'
export { BadgeListCell } from './core/badge-list-cell' export { BadgeListCell } from './core/badge-list-cell'
export { TruncatedCell } from './core/truncated-cell'
export { DataTableViewOptions } from './toolbar/view-options' export { DataTableViewOptions } from './toolbar/view-options'
export { DataTableToolbar } from './toolbar/toolbar' export { DataTableToolbar } from './toolbar/toolbar'
export { DataTableBulkActions } from './toolbar/bulk-actions' export { DataTableBulkActions } from './toolbar/bulk-actions'
......
...@@ -26,6 +26,7 @@ import { ...@@ -26,6 +26,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { TruncatedCell } from '../core/truncated-cell'
import { staticDataTableClassNames } from './static-data-table-classnames' import { staticDataTableClassNames } from './static-data-table-classnames'
type StaticDataTableBaseProps = { type StaticDataTableBaseProps = {
...@@ -163,15 +164,47 @@ function StaticDataTableRow<TData>({ ...@@ -163,15 +164,47 @@ function StaticDataTableRow<TData>({
{columns.map((column) => ( {columns.map((column) => (
<TableCell <TableCell
key={column.id} key={column.id}
className={getStaticCellClassName(column, row, index)} className={cn(
'max-w-full min-w-0 overflow-hidden',
getStaticCellClassName(column, row, index)
)}
> >
{column.cell?.(row, index)} {renderStaticCellContent(column, row, index)}
</TableCell> </TableCell>
))} ))}
</TableRow> </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>( function getStaticCellClassName<TData>(
column: StaticDataTableColumn<TData>, column: StaticDataTableColumn<TData>,
row: TData, row: TData,
......
...@@ -60,6 +60,7 @@ export function GroupBadge(props: GroupBadgeProps) { ...@@ -60,6 +60,7 @@ export function GroupBadge(props: GroupBadgeProps) {
ratio, ratio,
copyable = false, copyable = false,
showDot, showDot,
className,
...badgeProps ...badgeProps
} = props } = props
const groupName = group?.trim() const groupName = group?.trim()
...@@ -82,6 +83,7 @@ export function GroupBadge(props: GroupBadgeProps) { ...@@ -82,6 +83,7 @@ export function GroupBadge(props: GroupBadgeProps) {
showDot={showDot ?? (isSpecialGroup ? false : undefined)} showDot={showDot ?? (isSpecialGroup ? false : undefined)}
variant={isSpecialGroup ? 'neutral' : undefined} variant={isSpecialGroup ? 'neutral' : undefined}
autoColor={isSpecialGroup ? undefined : groupName} autoColor={isSpecialGroup ? undefined : groupName}
className={cn('min-w-0 shrink overflow-hidden', className)}
/> />
) )
...@@ -90,11 +92,11 @@ export function GroupBadge(props: GroupBadgeProps) { ...@@ -90,11 +92,11 @@ export function GroupBadge(props: GroupBadgeProps) {
} }
return ( return (
<span className='inline-flex items-center gap-2 text-xs'> <span className='inline-flex max-w-full min-w-0 items-center gap-2 text-xs'>
{badge} <span className='max-w-full min-w-0 overflow-hidden'>{badge}</span>
<span <span
className={cn( className={cn(
'inline-flex h-5 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums', 'inline-flex h-5 shrink-0 items-center rounded-full px-1.5 font-mono text-xs leading-none font-medium tabular-nums',
getGroupRatioClassName(ratio) getGroupRatioClassName(ratio)
)} )}
> >
......
...@@ -22,6 +22,7 @@ import { type LucideIcon } from 'lucide-react' ...@@ -22,6 +22,7 @@ import { type LucideIcon } from 'lucide-react'
import { stringToColor } from '@/lib/colors' import { stringToColor } from '@/lib/colors'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
export const dotColorMap = { export const dotColorMap = {
success: 'bg-success', success: 'bg-success',
warning: 'bg-warning', warning: 'bg-warning',
...@@ -81,7 +82,8 @@ export type StatusBadgeType = 'badge' | 'text' | 'underline' ...@@ -81,7 +82,8 @@ export type StatusBadgeType = 'badge' | 'text' | 'underline'
/** Context that lets ancestor components (e.g. MobileCardList field area) /** Context that lets ancestor components (e.g. MobileCardList field area)
* override the badge type without modifying every call site. */ * override the badge type without modifying every call site. */
export const StatusBadgeTypeContext = React.createContext<StatusBadgeType>('badge') export const StatusBadgeTypeContext =
React.createContext<StatusBadgeType>('badge')
const sizeMap = { const sizeMap = {
sm: 'h-5 gap-1 px-1.5 text-xs leading-none', sm: 'h-5 gap-1 px-1.5 text-xs leading-none',
...@@ -153,15 +155,21 @@ export function StatusBadge({ ...@@ -153,15 +155,21 @@ export function StatusBadge({
) : null) ) : null)
const isBadge = type === 'badge' const isBadge = type === 'badge'
const title = copyable
? `Click to copy: ${copyText || label || ''}`
: label || undefined
return ( return (
<span <span
data-slot='status-badge' data-slot='status-badge'
className={cn( className={cn(
'inline-flex w-fit max-w-full shrink-0 items-center font-medium tracking-normal whitespace-nowrap transition-colors', 'inline-flex w-fit max-w-full min-w-0 shrink items-center font-medium tracking-normal whitespace-nowrap transition-colors',
isBadge isBadge
? cn('rounded-4xl', sizeMap[size ?? 'sm']) ? cn('rounded-4xl', sizeMap[size ?? 'sm'])
: cn(textSizeMap[size ?? 'sm'], type === 'underline' && 'border-b border-current pb-px'), : cn(
textSizeMap[size ?? 'sm'],
type === 'underline' && 'border-b border-current pb-px'
),
textColorMap[computedVariant], textColorMap[computedVariant],
pulse && 'animate-pulse', pulse && 'animate-pulse',
copyable && copyable &&
...@@ -169,7 +177,7 @@ export function StatusBadge({ ...@@ -169,7 +177,7 @@ export function StatusBadge({
className className
)} )}
onClick={handleClick} onClick={handleClick}
title={copyable ? `Click to copy: ${copyText || label || ''}` : undefined} title={title}
{...props} {...props}
> >
{showDot && ( {showDot && (
...@@ -221,7 +229,7 @@ export function StatusBadgeList<T>(props: StatusBadgeListProps<T>) { ...@@ -221,7 +229,7 @@ export function StatusBadgeList<T>(props: StatusBadgeListProps<T>) {
return ( return (
<div <div
className={cn( className={cn(
'flex max-w-full items-center gap-1 overflow-hidden', 'flex max-w-full min-w-0 items-center gap-1 overflow-hidden',
className className
)} )}
{...domProps} {...domProps}
......
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { import { TruncatedCell } from '@/components/data-table'
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
interface TruncatedTextProps { interface TruncatedTextProps {
text: string text: string
...@@ -20,19 +15,8 @@ export function TruncatedText({ ...@@ -20,19 +15,8 @@ export function TruncatedText({
side = 'top', side = 'top',
}: TruncatedTextProps) { }: TruncatedTextProps) {
return ( return (
<TooltipProvider delay={300}> <TruncatedCell className={cn(maxWidth, className)} side={side}>
<Tooltip> {text}
<TooltipTrigger </TruncatedCell>
render={
<span className={cn('block truncate', maxWidth, className)} />
}
>
{text}
</TooltipTrigger>
<TooltipContent side={side} className='max-w-xs break-all'>
{text}
</TooltipContent>
</Tooltip>
</TooltipProvider>
) )
} }
...@@ -32,6 +32,7 @@ import { ...@@ -32,6 +32,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { BadgeCell } from '@/components/data-table'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { type ApiKey } from '../types' import { type ApiKey } from '../types'
import { useApiKeys } from './api-keys-provider' import { useApiKeys } from './api-keys-provider'
...@@ -157,7 +158,12 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) { ...@@ -157,7 +158,12 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) {
if (!apiKey.model_limits_enabled || !apiKey.model_limits) { if (!apiKey.model_limits_enabled || !apiKey.model_limits) {
return ( return (
<StatusBadge label={t('Unlimited')} variant='neutral' copyable={false} /> <StatusBadge
label={t('Unlimited')}
variant='neutral'
copyable={false}
className='-ml-1.5'
/>
) )
} }
...@@ -165,7 +171,7 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) { ...@@ -165,7 +171,7 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger render={<span />}> <TooltipTrigger render={<BadgeCell />}>
<StatusBadge <StatusBadge
label={t('{{count}} model(s)', { count: models.length })} label={t('{{count}} model(s)', { count: models.length })}
variant='neutral' variant='neutral'
...@@ -195,6 +201,7 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) { ...@@ -195,6 +201,7 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) {
label={t('No restriction')} label={t('No restriction')}
variant='neutral' variant='neutral'
copyable={false} copyable={false}
className='-ml-1.5'
/> />
) )
} }
...@@ -206,7 +213,7 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) { ...@@ -206,7 +213,7 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger render={<span />}> <TooltipTrigger render={<BadgeCell />}>
<StatusBadge <StatusBadge
label={t('{{count}} IP(s)', { count: ips.length })} label={t('{{count}} IP(s)', { count: ips.length })}
variant='neutral' variant='neutral'
......
...@@ -29,6 +29,7 @@ import { ...@@ -29,6 +29,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { BadgeCell, TruncatedCell } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { API_KEY_STATUSES } from '../constants' import { API_KEY_STATUSES } from '../constants'
...@@ -97,9 +98,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] { ...@@ -97,9 +98,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
accessorKey: 'name', accessorKey: 'name',
header: t('Name'), header: t('Name'),
cell: ({ row }) => ( cell: ({ row }) => (
<div className='max-w-[200px] truncate font-medium'> <span className='font-medium'>{row.getValue('name')}</span>
{row.getValue('name')}
</div>
), ),
size: 180, size: 180,
meta: { mobileTitle: true }, meta: { mobileTitle: true },
...@@ -200,9 +199,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] { ...@@ -200,9 +199,7 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
return ( return (
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={<BadgeCell className='gap-1.5 text-xs' />}
<span className='inline-flex items-center gap-1.5 text-xs' />
}
> >
<GroupBadge group='auto' /> <GroupBadge group='auto' />
{apiKey.cross_group_retry && ( {apiKey.cross_group_retry && (
...@@ -223,7 +220,15 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] { ...@@ -223,7 +220,15 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
</Tooltip> </Tooltip>
) )
} }
return <GroupBadge group={group} ratio={ratio} /> return (
<TruncatedCell
className='-ml-1.5'
tooltipContent={group || '-'}
tooltipClassName='break-all'
>
<GroupBadge group={group} ratio={ratio} />
</TruncatedCell>
)
}, },
size: 160, size: 160,
meta: { mobileHidden: true }, meta: { mobileHidden: true },
......
...@@ -197,7 +197,7 @@ export function useDeploymentsColumns(opts: { ...@@ -197,7 +197,7 @@ export function useDeploymentsColumns(opts: {
if (!hardware) if (!hardware)
return <span className='text-muted-foreground text-xs'>-</span> return <span className='text-muted-foreground text-xs'>-</span>
return ( return (
<div className='flex flex-wrap items-center gap-2'> <div className='flex max-w-full min-w-0 flex-nowrap items-center gap-2 overflow-hidden'>
<StatusBadge <StatusBadge
label={String(hardware)} label={String(hardware)}
variant='neutral' variant='neutral'
......
...@@ -27,7 +27,7 @@ import { ...@@ -27,7 +27,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { BadgeListCell } from '@/components/data-table' import { BadgeCell, BadgeListCell } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { ProviderBadge } from '@/components/provider-badge' import { ProviderBadge } from '@/components/provider-badge'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
...@@ -187,7 +187,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] { ...@@ -187,7 +187,9 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return ( return (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger render={<div className='-ml-1.5' />}>{badge}</TooltipTrigger> <TooltipTrigger render={<div className='-ml-1.5' />}>
{badge}
</TooltipTrigger>
<TooltipContent <TooltipContent
side='top' side='top'
className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2' className='border-border bg-popover max-h-48 max-w-[320px] overflow-y-auto p-2'
...@@ -248,7 +250,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] { ...@@ -248,7 +250,11 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
return <span className='text-muted-foreground text-xs'>-</span> return <span className='text-muted-foreground text-xs'>-</span>
} }
return <ProviderBadge iconKey={vendor.icon} label={vendor.name} /> return (
<BadgeCell>
<ProviderBadge iconKey={vendor.icon} label={vendor.name} />
</BadgeCell>
)
}, },
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
if (!value || value.length === 0 || value.includes('all')) return true if (!value || value.length === 0 || value.includes('all')) return true
......
...@@ -19,7 +19,11 @@ For commercial licensing, please contact support@quantumnous.com ...@@ -19,7 +19,11 @@ For commercial licensing, please contact support@quantumnous.com
import { type ColumnDef } from '@tanstack/react-table' import { type ColumnDef } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getLobeIcon } from '@/lib/lobe-icon' import { getLobeIcon } from '@/lib/lobe-icon'
import { DataTableColumnHeader, BadgeListCell } from '@/components/data-table' import {
BadgeCell,
BadgeListCell,
DataTableColumnHeader,
} from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants'
...@@ -74,7 +78,7 @@ export function usePricingColumns( ...@@ -74,7 +78,7 @@ export function usePricingColumns(
const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null
return ( return (
<div className='flex min-w-[200px] items-center gap-2'> <div className='flex max-w-full min-w-0 items-center gap-2'>
{modelIcon} {modelIcon}
<span className='truncate font-mono text-sm font-medium'> <span className='truncate font-mono text-sm font-medium'>
{model.model_name} {model.model_name}
...@@ -124,7 +128,7 @@ export function usePricingColumns( ...@@ -124,7 +128,7 @@ export function usePricingColumns(
if (dynamicSummary) { if (dynamicSummary) {
if (dynamicSummary.isSpecialExpression) { if (dynamicSummary.isSpecialExpression) {
return ( return (
<div className='max-w-[320px] min-w-[200px]'> <div className='max-w-full min-w-0'>
<div className='text-xs font-medium text-amber-700 dark:text-amber-300'> <div className='text-xs font-medium text-amber-700 dark:text-amber-300'>
{t('Special billing expression')} {t('Special billing expression')}
</div> </div>
...@@ -148,7 +152,7 @@ export function usePricingColumns( ...@@ -148,7 +152,7 @@ export function usePricingColumns(
} }
return ( return (
<div className='min-w-[180px]'> <div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'> <span className='font-mono text-sm tabular-nums'>
{primaryEntries.map((entry, index) => ( {primaryEntries.map((entry, index) => (
<span key={entry.key}> <span key={entry.key}>
...@@ -195,7 +199,7 @@ export function usePricingColumns( ...@@ -195,7 +199,7 @@ export function usePricingColumns(
) )
return ( return (
<div className='min-w-[160px]'> <div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'> <span className='font-mono text-sm tabular-nums'>
{inputPrice} {inputPrice}
<span className='text-muted-foreground/40 mx-1'>/</span> <span className='text-muted-foreground/40 mx-1'>/</span>
...@@ -218,7 +222,7 @@ export function usePricingColumns( ...@@ -218,7 +222,7 @@ export function usePricingColumns(
) )
return ( return (
<div className='min-w-[100px]'> <div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'>{price}</span> <span className='font-mono text-sm tabular-nums'>{price}</span>
<div className='text-muted-foreground/50 text-[10px]'> <div className='text-muted-foreground/50 text-[10px]'>
/ {t('request')} / {t('request')}
...@@ -261,7 +265,7 @@ export function usePricingColumns( ...@@ -261,7 +265,7 @@ export function usePricingColumns(
} }
return ( return (
<div className='min-w-[80px]'> <div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'> <span className='font-mono text-sm tabular-nums'>
{stripTrailingZeros(cacheEntry.formatted)} {stripTrailingZeros(cacheEntry.formatted)}
</span> </span>
...@@ -290,7 +294,7 @@ export function usePricingColumns( ...@@ -290,7 +294,7 @@ export function usePricingColumns(
) )
return ( return (
<div className='min-w-[80px]'> <div className='max-w-full min-w-0'>
<span className='font-mono text-sm tabular-nums'> <span className='font-mono text-sm tabular-nums'>
{cachedPrice} {cachedPrice}
</span> </span>
...@@ -317,7 +321,7 @@ export function usePricingColumns( ...@@ -317,7 +321,7 @@ export function usePricingColumns(
? getLobeIcon(model.vendor_icon, 12) ? getLobeIcon(model.vendor_icon, 12)
: null : null
return ( return (
<span className='flex items-center gap-1.5'> <BadgeCell className='gap-1.5'>
{vendorIcon} {vendorIcon}
<StatusBadge <StatusBadge
label={model.vendor_name} label={model.vendor_name}
...@@ -325,7 +329,7 @@ export function usePricingColumns( ...@@ -325,7 +329,7 @@ export function usePricingColumns(
size='sm' size='sm'
copyable={false} copyable={false}
/> />
</span> </BadgeCell>
) )
}, },
size: 130, size: 130,
......
...@@ -74,13 +74,9 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] { ...@@ -74,13 +74,9 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
accessorKey: 'name', accessorKey: 'name',
header: t('Name'), header: t('Name'),
meta: { mobileTitle: true }, meta: { mobileTitle: true },
cell: ({ row }) => { cell: ({ row }) => (
return ( <span className='font-medium'>{row.getValue('name')}</span>
<div className='max-w-[150px] truncate font-medium'> ),
{row.getValue('name')}
</div>
)
},
size: 180, size: 180,
}, },
{ {
......
...@@ -20,6 +20,7 @@ import { useMemo } from 'react' ...@@ -20,6 +20,7 @@ import { useMemo } from 'react'
import { type ColumnDef } from '@tanstack/react-table' import { type ColumnDef } from '@tanstack/react-table'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { formatQuota } from '@/lib/format' import { formatQuota } from '@/lib/format'
import { BadgeCell } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { TableId } from '@/components/table-id' import { TableId } from '@/components/table-id'
...@@ -48,7 +49,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] { ...@@ -48,7 +49,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
cell: ({ row }) => { cell: ({ row }) => {
const plan = row.original.plan const plan = row.original.plan
return ( return (
<div className='max-w-[200px]'> <div className='max-w-full min-w-0'>
<div className='truncate font-medium'>{plan.title}</div> <div className='truncate font-medium'>{plan.title}</div>
{plan.subtitle && ( {plan.subtitle && (
<div className='text-muted-foreground truncate text-xs'> <div className='text-muted-foreground truncate text-xs'>
...@@ -134,7 +135,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] { ...@@ -134,7 +135,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
cell: ({ row }) => { cell: ({ row }) => {
const plan = row.original.plan const plan = row.original.plan
return ( return (
<div className='flex gap-1'> <BadgeCell>
{plan.stripe_price_id && ( {plan.stripe_price_id && (
<StatusBadge <StatusBadge
label='Stripe' label='Stripe'
...@@ -152,7 +153,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] { ...@@ -152,7 +153,7 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
copyable={false} copyable={false}
/> />
)} )}
</div> </BadgeCell>
) )
}, },
size: 140, size: 140,
...@@ -182,7 +183,11 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] { ...@@ -182,7 +183,11 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
<span className='text-muted-foreground'>{t('No Upgrade')}</span> <span className='text-muted-foreground'>{t('No Upgrade')}</span>
) )
} }
return <GroupBadge group={group} /> return (
<BadgeCell>
<GroupBadge group={group} />
</BadgeCell>
)
}, },
size: 120, size: 120,
}, },
......
...@@ -21,7 +21,7 @@ import { Pencil, Trash2, Plus } from 'lucide-react' ...@@ -21,7 +21,7 @@ import { Pencil, Trash2, Plus } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ConfirmDialog } from '@/components/confirm-dialog' import { ConfirmDialog } from '@/components/confirm-dialog'
import { StaticDataTable } from '@/components/data-table' import { BadgeCell, StaticDataTable } from '@/components/data-table'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { useDeleteProvider } from '../hooks/use-custom-oauth-mutations' import { useDeleteProvider } from '../hooks/use-custom-oauth-mutations'
import type { CustomOAuthProvider } from '../types' import type { CustomOAuthProvider } from '../types'
...@@ -83,28 +83,33 @@ export function ProviderTable(props: ProviderTableProps) { ...@@ -83,28 +83,33 @@ export function ProviderTable(props: ProviderTableProps) {
id: 'slug', id: 'slug',
header: t('Slug'), header: t('Slug'),
cell: (provider) => ( cell: (provider) => (
<StatusBadge <BadgeCell>
label={provider.slug} <StatusBadge
variant='neutral' label={provider.slug}
copyable={false} variant='neutral'
/> copyable={false}
/>
</BadgeCell>
), ),
}, },
{ {
id: 'status', id: 'status',
header: t('Status'), header: t('Status'),
cell: (provider) => ( cell: (provider) => (
<StatusBadge <BadgeCell>
label={provider.enabled ? t('Enabled') : t('Disabled')} <StatusBadge
variant={provider.enabled ? 'success' : 'neutral'} label={provider.enabled ? t('Enabled') : t('Disabled')}
copyable={false} variant={provider.enabled ? 'success' : 'neutral'}
/> copyable={false}
/>
</BadgeCell>
), ),
}, },
{ {
id: 'client-id', id: 'client-id',
header: t('Client ID'), header: t('Client ID'),
cellClassName: 'text-muted-foreground max-w-[120px] truncate font-mono', cellClassName:
'text-muted-foreground max-w-[120px] truncate font-mono',
cell: (provider) => provider.client_id, cell: (provider) => provider.client_id,
}, },
{ {
......
...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
import { useEffect, useState } from 'react' import { useMemo, useState } from 'react'
import * as z from 'zod' import * as z from 'zod'
import { useForm } from 'react-hook-form' import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod' import { zodResolver } from '@hookform/resolvers/zod'
...@@ -54,7 +54,7 @@ import { ...@@ -54,7 +54,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { StaticDataTable } from '@/components/data-table' import { BadgeCell, StaticDataTable } from '@/components/data-table'
import { Dialog } from '@/components/dialog' import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { SettingsSwitchField } from '../components/settings-form-layout' import { SettingsSwitchField } from '../components/settings-form-layout'
...@@ -103,18 +103,37 @@ const colorOptions = [ ...@@ -103,18 +103,37 @@ const colorOptions = [
{ value: 'slate', label: 'Slate' }, { value: 'slate', label: 'Slate' },
] ]
function parseApiInfoList(data: string): ApiInfo[] {
try {
const parsed = JSON.parse(data || '[]')
if (!Array.isArray(parsed)) return []
return parsed.map((item, idx) => ({
...item,
id: item.id || idx + 1,
}))
} catch {
return []
}
}
export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
const { t } = useTranslation() const { t } = useTranslation()
const updateOption = useUpdateOption() const updateOption = useUpdateOption()
const apiInfoSchema = createApiInfoSchema(t) const apiInfoSchema = createApiInfoSchema(t)
const [apiInfoList, setApiInfoList] = useState<ApiInfo[]>([]) const parsedApiInfoList = useMemo(() => parseApiInfoList(data), [data])
const [isEnabled, setIsEnabled] = useState(enabled) const [draftApiInfoList, setDraftApiInfoList] = useState<ApiInfo[] | null>(
const [hasChanges, setHasChanges] = useState(false) null
)
const [isEnabledDraft, setIsEnabledDraft] = useState<boolean | null>(null)
const [selectedIds, setSelectedIds] = useState<number[]>([]) const [selectedIds, setSelectedIds] = useState<number[]>([])
const [showDialog, setShowDialog] = useState(false) const [showDialog, setShowDialog] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [editingApiInfo, setEditingApiInfo] = useState<ApiInfo | null>(null) const [editingApiInfo, setEditingApiInfo] = useState<ApiInfo | null>(null)
const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single') const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single')
const apiInfoList = draftApiInfoList ?? parsedApiInfoList
const isEnabled = isEnabledDraft ?? enabled
const hasChanges = draftApiInfoList !== null
const form = useForm<ApiInfoFormValues>({ const form = useForm<ApiInfoFormValues>({
resolver: zodResolver(apiInfoSchema), resolver: zodResolver(apiInfoSchema),
...@@ -126,33 +145,13 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -126,33 +145,13 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
}, },
}) })
useEffect(() => {
try {
const parsed = JSON.parse(data || '[]')
if (Array.isArray(parsed)) {
setApiInfoList(
parsed.map((item, idx) => ({
...item,
id: item.id || idx + 1,
}))
)
}
} catch {
setApiInfoList([])
}
}, [data])
useEffect(() => {
setIsEnabled(enabled)
}, [enabled])
const handleToggleEnabled = async (checked: boolean) => { const handleToggleEnabled = async (checked: boolean) => {
try { try {
await updateOption.mutateAsync({ await updateOption.mutateAsync({
key: 'console_setting.api_info_enabled', key: 'console_setting.api_info_enabled',
value: checked, value: checked,
}) })
setIsEnabled(checked) setIsEnabledDraft(checked)
toast.success(t('Setting saved')) toast.success(t('Setting saved'))
} catch { } catch {
toast.error(t('Failed to update setting')) toast.error(t('Failed to update setting'))
...@@ -198,17 +197,15 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -198,17 +197,15 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
const confirmDelete = () => { const confirmDelete = () => {
if (deleteTarget === 'single' && editingApiInfo) { if (deleteTarget === 'single' && editingApiInfo) {
setApiInfoList((prev) => setDraftApiInfoList(
prev.filter((item) => item.id !== editingApiInfo.id) apiInfoList.filter((item) => item.id !== editingApiInfo.id)
) )
setHasChanges(true)
toast.success(t('API info deleted. Click "Save Settings" to apply.')) toast.success(t('API info deleted. Click "Save Settings" to apply.'))
} else if (deleteTarget === 'batch') { } else if (deleteTarget === 'batch') {
setApiInfoList((prev) => setDraftApiInfoList(
prev.filter((item) => !selectedIds.includes(item.id)) apiInfoList.filter((item) => !selectedIds.includes(item.id))
) )
setSelectedIds([]) setSelectedIds([])
setHasChanges(true)
toast.success( toast.success(
t('{{count}} API entries deleted. Click "Save Settings" to apply.', { t('{{count}} API entries deleted. Click "Save Settings" to apply.', {
count: selectedIds.length, count: selectedIds.length,
...@@ -221,18 +218,17 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -221,18 +218,17 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
const handleSubmitForm = (values: ApiInfoFormValues) => { const handleSubmitForm = (values: ApiInfoFormValues) => {
if (editingApiInfo) { if (editingApiInfo) {
setApiInfoList((prev) => setDraftApiInfoList(
prev.map((item) => apiInfoList.map((item) =>
item.id === editingApiInfo.id ? { ...item, ...values } : item item.id === editingApiInfo.id ? { ...item, ...values } : item
) )
) )
toast.success(t('API info updated. Click "Save Settings" to apply.')) toast.success(t('API info updated. Click "Save Settings" to apply.'))
} else { } else {
const newId = Math.max(...apiInfoList.map((item) => item.id), 0) + 1 const newId = Math.max(...apiInfoList.map((item) => item.id), 0) + 1
setApiInfoList((prev) => [...prev, { id: newId, ...values }]) setDraftApiInfoList([...apiInfoList, { id: newId, ...values }])
toast.success(t('API info added. Click "Save Settings" to apply.')) toast.success(t('API info added. Click "Save Settings" to apply.'))
} }
setHasChanges(true)
setShowDialog(false) setShowDialog(false)
} }
...@@ -243,7 +239,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -243,7 +239,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
value: JSON.stringify(apiInfoList), value: JSON.stringify(apiInfoList),
}) })
if (result.success) { if (result.success) {
setHasChanges(false) setDraftApiInfoList(null)
} }
} catch { } catch {
toast.error(t('Failed to save API info')) toast.error(t('Failed to save API info'))
...@@ -330,22 +326,26 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { ...@@ -330,22 +326,26 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) {
header: t('URL'), header: t('URL'),
cellClassName: 'max-w-xs truncate font-mono text-sm', cellClassName: 'max-w-xs truncate font-mono text-sm',
cell: (apiInfo) => ( cell: (apiInfo) => (
<StatusBadge <BadgeCell>
label={apiInfo.url} <StatusBadge
variant='neutral' label={apiInfo.url}
copyable={false} variant='neutral'
/> copyable={false}
/>
</BadgeCell>
), ),
}, },
{ {
id: 'route', id: 'route',
header: t('Route'), header: t('Route'),
cell: (apiInfo) => ( cell: (apiInfo) => (
<StatusBadge <BadgeCell>
label={apiInfo.route} <StatusBadge
variant='neutral' label={apiInfo.route}
copyable={false} variant='neutral'
/> copyable={false}
/>
</BadgeCell>
), ),
}, },
{ {
......
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { BadgeCell } from '@/components/data-table'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import type { RatioType } from '../types' import type { RatioType } from '../types'
import { import {
...@@ -63,8 +64,8 @@ export function useUpstreamRatioSyncColumns( ...@@ -63,8 +64,8 @@ export function useUpstreamRatioSyncColumns(
cell: ({ row }) => { cell: ({ row }) => {
const model = row.original.model const model = row.original.model
return ( return (
<div className='flex min-w-[180px] items-center gap-2'> <div className='flex max-w-full min-w-0 items-center gap-2'>
<span className='font-medium'>{model}</span> <span className='truncate font-medium'>{model}</span>
{row.original.billingConflict && ( {row.original.billingConflict && (
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
...@@ -94,14 +95,11 @@ export function useUpstreamRatioSyncColumns( ...@@ -94,14 +95,11 @@ export function useUpstreamRatioSyncColumns(
ratioTypeFilter ratioTypeFilter
) )
return ( return (
<div className='flex min-w-[260px] flex-col gap-2'> <div className='flex max-w-full min-w-0 flex-col gap-2'>
{fields.map((ratioType) => { {fields.map((ratioType) => {
const current = row.original.ratioTypes[ratioType]?.current const current = row.original.ratioTypes[ratioType]?.current
return ( return (
<div <BadgeCell key={ratioType} className='ml-0 flex-wrap gap-2'>
key={ratioType}
className='flex min-w-0 flex-wrap items-center gap-2'
>
<StatusBadge <StatusBadge
label={getSyncFieldLabel(ratioType, t)} label={getSyncFieldLabel(ratioType, t)}
autoColor={ratioType} autoColor={ratioType}
...@@ -136,7 +134,7 @@ export function useUpstreamRatioSyncColumns( ...@@ -136,7 +134,7 @@ export function useUpstreamRatioSyncColumns(
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
)} )}
</div> </BadgeCell>
) )
})} })}
</div> </div>
...@@ -215,7 +213,7 @@ export function useUpstreamRatioSyncColumns( ...@@ -215,7 +213,7 @@ export function useUpstreamRatioSyncColumns(
) )
return ( return (
<div className='flex min-w-[280px] flex-col gap-2'> <div className='flex max-w-full min-w-0 flex-col gap-2'>
{fields.map((ratioType) => { {fields.map((ratioType) => {
const diff = row.original.ratioTypes[ratioType] const diff = row.original.ratioTypes[ratioType]
const upstreamVal = diff?.upstreams?.[upstreamName] const upstreamVal = diff?.upstreams?.[upstreamName]
......
...@@ -27,6 +27,7 @@ import { ...@@ -27,6 +27,7 @@ import {
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { BadgeCell } from '@/components/data-table'
import { GroupBadge } from '@/components/group-badge' import { GroupBadge } from '@/components/group-badge'
import { LongText } from '@/components/long-text' import { LongText } from '@/components/long-text'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
...@@ -227,7 +228,11 @@ export function useUsersColumns(): ColumnDef<User>[] { ...@@ -227,7 +228,11 @@ export function useUsersColumns(): ColumnDef<User>[] {
header: t('Group'), header: t('Group'),
cell: ({ row }) => { cell: ({ row }) => {
const group = row.getValue('group') as string const group = row.getValue('group') as string
return <GroupBadge group={group} /> return (
<BadgeCell>
<GroupBadge group={group} />
</BadgeCell>
)
}, },
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
const group = String(row.getValue(id) || t('User Group')).toLowerCase() const group = String(row.getValue(id) || t('User Group')).toLowerCase()
...@@ -272,7 +277,7 @@ export function useUsersColumns(): ColumnDef<User>[] { ...@@ -272,7 +277,7 @@ export function useUsersColumns(): ColumnDef<User>[] {
const inviterId = user.inviter_id || 0 const inviterId = user.inviter_id || 0
return ( return (
<div className='flex min-w-[220px] flex-wrap items-center gap-1'> <div className='flex max-w-full min-w-0 flex-wrap items-center gap-1 overflow-hidden'>
<Tooltip> <Tooltip>
<TooltipTrigger <TooltipTrigger
render={ render={
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment