From 3c1bb0a74f2e2e74ede997c111178bd7cee60022 Mon Sep 17 00:00:00 2001 From: QuentinHsu Date: Mon, 15 Jun 2026 14:52:57 +0800 Subject: [PATCH] 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. --- .../components/data-table/core/badge-cell.tsx | 34 +++++++ .../data-table/core/badge-list-cell.tsx | 4 +- .../data-table/core/data-table-row.tsx | 36 ++++++- .../data-table/core/truncated-cell.tsx | 91 ++++++++++++++++++ .../src/components/data-table/index.ts | 2 + .../data-table/static/static-data-table.tsx | 37 +++++++- web/default/src/components/group-badge.tsx | 8 +- web/default/src/components/status-badge.tsx | 18 +++- web/default/src/components/truncated-text.tsx | 24 +---- .../keys/components/api-keys-cells.tsx | 13 ++- .../keys/components/api-keys-columns.tsx | 19 ++-- .../models/components/deployments-columns.tsx | 2 +- .../models/components/models-columns.tsx | 12 ++- .../pricing/components/pricing-columns.tsx | 24 +++-- .../components/redemptions-columns.tsx | 10 +- .../components/subscriptions-columns.tsx | 13 ++- .../components/provider-table.tsx | 29 +++--- .../content/api-info-section.tsx | 94 +++++++++---------- .../models/upstream-ratio-sync-columns.tsx | 16 ++-- .../users/components/users-columns.tsx | 9 +- 20 files changed, 355 insertions(+), 140 deletions(-) create mode 100644 web/default/src/components/data-table/core/badge-cell.tsx create mode 100644 web/default/src/components/data-table/core/truncated-cell.tsx diff --git a/web/default/src/components/data-table/core/badge-cell.tsx b/web/default/src/components/data-table/core/badge-cell.tsx new file mode 100644 index 00000000..2409f975 --- /dev/null +++ b/web/default/src/components/data-table/core/badge-cell.tsx @@ -0,0 +1,34 @@ +/* +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 . + +For commercial licensing, please contact support@quantumnous.com +*/ +import * as React from 'react' +import { cn } from '@/lib/utils' + +type BadgeCellProps = React.HTMLAttributes + +export function BadgeCell({ className, ...props }: BadgeCellProps) { + return ( +
+ ) +} diff --git a/web/default/src/components/data-table/core/badge-list-cell.tsx b/web/default/src/components/data-table/core/badge-list-cell.tsx index 74128ce9..7196a785 100644 --- a/web/default/src/components/data-table/core/badge-list-cell.tsx +++ b/web/default/src/components/data-table/core/badge-list-cell.tsx @@ -17,13 +17,13 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import * as React from 'react' -import { StatusBadgeList } from '@/components/status-badge' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' +import { StatusBadgeList } from '@/components/status-badge' interface BadgeListCellProps { items: React.ReactNode[] @@ -50,7 +50,7 @@ export function BadgeListCell({ return ( - }> + }> . For commercial licensing, please contact support@quantumnous.com */ 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 { TruncatedCell } from './truncated-cell' import type { DataTableColumnClassName } from './types' type DataTableRowProps = { @@ -42,9 +44,12 @@ function DataTableRowInner({ {row.getVisibleCells().map((cell) => ( - {flexRender(cell.column.columnDef.cell, cell.getContext())} + {renderCellContent(cell)} ))} @@ -61,3 +66,28 @@ export const DataTableRow = React.memo(DataTableRowInner, (prev, next) => { prev.row.getIsSelected() === next.row.getIsSelected() ) }) as typeof DataTableRowInner + +function renderCellContent(cell: Cell) { + const content = flexRender(cell.column.columnDef.cell, cell.getContext()) + const textContent = getPrimitiveTextContent(content) + + if (!textContent) return content + + return {content} +} + +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 +} diff --git a/web/default/src/components/data-table/core/truncated-cell.tsx b/web/default/src/components/data-table/core/truncated-cell.tsx new file mode 100644 index 00000000..f635cb6e --- /dev/null +++ b/web/default/src/components/data-table/core/truncated-cell.tsx @@ -0,0 +1,91 @@ +/* +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 . + +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 ( +
+ {children} +
+ ) + } + + return ( + + + } + > +
{children}
+
+ + {content} + +
+ ) +} + +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 '' +} diff --git a/web/default/src/components/data-table/index.ts b/web/default/src/components/data-table/index.ts index ded3be53..e4d6a89e 100644 --- a/web/default/src/components/data-table/index.ts +++ b/web/default/src/components/data-table/index.ts @@ -18,7 +18,9 @@ For commercial licensing, please contact support@quantumnous.com */ export { DataTablePagination } from './core/pagination' export { DataTableColumnHeader } from './core/column-header' +export { BadgeCell } from './core/badge-cell' export { BadgeListCell } from './core/badge-list-cell' +export { TruncatedCell } from './core/truncated-cell' export { DataTableViewOptions } from './toolbar/view-options' export { DataTableToolbar } from './toolbar/toolbar' export { DataTableBulkActions } from './toolbar/bulk-actions' diff --git a/web/default/src/components/data-table/static/static-data-table.tsx b/web/default/src/components/data-table/static/static-data-table.tsx index 72bde152..d882c4c6 100644 --- a/web/default/src/components/data-table/static/static-data-table.tsx +++ b/web/default/src/components/data-table/static/static-data-table.tsx @@ -26,6 +26,7 @@ import { TableHeader, TableRow, } from '@/components/ui/table' +import { TruncatedCell } from '../core/truncated-cell' import { staticDataTableClassNames } from './static-data-table-classnames' type StaticDataTableBaseProps = { @@ -163,15 +164,47 @@ function StaticDataTableRow({ {columns.map((column) => ( - {column.cell?.(row, index)} + {renderStaticCellContent(column, row, index)} ))} ) } +function renderStaticCellContent( + column: StaticDataTableColumn, + row: TData, + index: number +) { + const content = column.cell?.(row, index) + const textContent = getPrimitiveTextContent(content) + + if (!textContent) return content + + return {content} +} + +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( column: StaticDataTableColumn, row: TData, diff --git a/web/default/src/components/group-badge.tsx b/web/default/src/components/group-badge.tsx index aa19d408..9d01a0a6 100644 --- a/web/default/src/components/group-badge.tsx +++ b/web/default/src/components/group-badge.tsx @@ -60,6 +60,7 @@ export function GroupBadge(props: GroupBadgeProps) { ratio, copyable = false, showDot, + className, ...badgeProps } = props const groupName = group?.trim() @@ -82,6 +83,7 @@ export function GroupBadge(props: GroupBadgeProps) { showDot={showDot ?? (isSpecialGroup ? false : undefined)} variant={isSpecialGroup ? 'neutral' : undefined} autoColor={isSpecialGroup ? undefined : groupName} + className={cn('min-w-0 shrink overflow-hidden', className)} /> ) @@ -90,11 +92,11 @@ export function GroupBadge(props: GroupBadgeProps) { } return ( - - {badge} + + {badge} diff --git a/web/default/src/components/status-badge.tsx b/web/default/src/components/status-badge.tsx index 24e04aaa..69cfd24b 100644 --- a/web/default/src/components/status-badge.tsx +++ b/web/default/src/components/status-badge.tsx @@ -22,6 +22,7 @@ import { type LucideIcon } from 'lucide-react' import { stringToColor } from '@/lib/colors' import { cn } from '@/lib/utils' import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard' + export const dotColorMap = { success: 'bg-success', warning: 'bg-warning', @@ -81,7 +82,8 @@ export type StatusBadgeType = 'badge' | 'text' | 'underline' /** Context that lets ancestor components (e.g. MobileCardList field area) * override the badge type without modifying every call site. */ -export const StatusBadgeTypeContext = React.createContext('badge') +export const StatusBadgeTypeContext = + React.createContext('badge') const sizeMap = { sm: 'h-5 gap-1 px-1.5 text-xs leading-none', @@ -153,15 +155,21 @@ export function StatusBadge({ ) : null) const isBadge = type === 'badge' + const title = copyable + ? `Click to copy: ${copyText || label || ''}` + : label || undefined return ( {showDot && ( @@ -221,7 +229,7 @@ export function StatusBadgeList(props: StatusBadgeListProps) { return (
- - - } - > - {text} - - - {text} - - - + + {text} + ) } diff --git a/web/default/src/features/keys/components/api-keys-cells.tsx b/web/default/src/features/keys/components/api-keys-cells.tsx index 6e414724..4403b958 100644 --- a/web/default/src/features/keys/components/api-keys-cells.tsx +++ b/web/default/src/features/keys/components/api-keys-cells.tsx @@ -32,6 +32,7 @@ import { TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' +import { BadgeCell } from '@/components/data-table' import { StatusBadge } from '@/components/status-badge' import { type ApiKey } from '../types' import { useApiKeys } from './api-keys-provider' @@ -157,7 +158,12 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) { if (!apiKey.model_limits_enabled || !apiKey.model_limits) { return ( - + ) } @@ -165,7 +171,7 @@ export function ModelLimitsCell({ apiKey }: { apiKey: ApiKey }) { return ( - }> + }> ) } @@ -206,7 +213,7 @@ export function IpRestrictionsCell({ apiKey }: { apiKey: ApiKey }) { return ( - }> + }> [] { accessorKey: 'name', header: t('Name'), cell: ({ row }) => ( -
- {row.getValue('name')} -
+ {row.getValue('name')} ), size: 180, meta: { mobileTitle: true }, @@ -200,9 +199,7 @@ export function useApiKeysColumns(): ColumnDef[] { return ( - } + render={} > {apiKey.cross_group_retry && ( @@ -223,7 +220,15 @@ export function useApiKeysColumns(): ColumnDef[] { ) } - return + return ( + + + + ) }, size: 160, meta: { mobileHidden: true }, diff --git a/web/default/src/features/models/components/deployments-columns.tsx b/web/default/src/features/models/components/deployments-columns.tsx index 541fefb5..1598e5aa 100644 --- a/web/default/src/features/models/components/deployments-columns.tsx +++ b/web/default/src/features/models/components/deployments-columns.tsx @@ -197,7 +197,7 @@ export function useDeploymentsColumns(opts: { if (!hardware) return - return ( -
+
[] { return ( - }>{badge} + }> + {badge} + [] { return - } - return + return ( + + + + ) }, filterFn: (row, id, value) => { if (!value || value.length === 0 || value.includes('all')) return true diff --git a/web/default/src/features/pricing/components/pricing-columns.tsx b/web/default/src/features/pricing/components/pricing-columns.tsx index d64d3bcc..b34879c7 100644 --- a/web/default/src/features/pricing/components/pricing-columns.tsx +++ b/web/default/src/features/pricing/components/pricing-columns.tsx @@ -19,7 +19,11 @@ For commercial licensing, please contact support@quantumnous.com import { type ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' 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 { StatusBadge } from '@/components/status-badge' import { DEFAULT_TOKEN_UNIT, QUOTA_TYPE_VALUES } from '../constants' @@ -74,7 +78,7 @@ export function usePricingColumns( const modelIcon = modelIconKey ? getLobeIcon(modelIconKey, 14) : null return ( -
+
{modelIcon} {model.model_name} @@ -124,7 +128,7 @@ export function usePricingColumns( if (dynamicSummary) { if (dynamicSummary.isSpecialExpression) { return ( -
+
{t('Special billing expression')}
@@ -148,7 +152,7 @@ export function usePricingColumns( } return ( -
+
{primaryEntries.map((entry, index) => ( @@ -195,7 +199,7 @@ export function usePricingColumns( ) return ( -
+
{inputPrice} / @@ -218,7 +222,7 @@ export function usePricingColumns( ) return ( -
+
{price}
/ {t('request')} @@ -261,7 +265,7 @@ export function usePricingColumns( } return ( -
+
{stripTrailingZeros(cacheEntry.formatted)} @@ -290,7 +294,7 @@ export function usePricingColumns( ) return ( -
+
{cachedPrice} @@ -317,7 +321,7 @@ export function usePricingColumns( ? getLobeIcon(model.vendor_icon, 12) : null return ( - + {vendorIcon} - + ) }, size: 130, diff --git a/web/default/src/features/redemption-codes/components/redemptions-columns.tsx b/web/default/src/features/redemption-codes/components/redemptions-columns.tsx index 6512fb73..638c6890 100644 --- a/web/default/src/features/redemption-codes/components/redemptions-columns.tsx +++ b/web/default/src/features/redemption-codes/components/redemptions-columns.tsx @@ -74,13 +74,9 @@ export function useRedemptionsColumns(): ColumnDef[] { accessorKey: 'name', header: t('Name'), meta: { mobileTitle: true }, - cell: ({ row }) => { - return ( -
- {row.getValue('name')} -
- ) - }, + cell: ({ row }) => ( + {row.getValue('name')} + ), size: 180, }, { diff --git a/web/default/src/features/subscriptions/components/subscriptions-columns.tsx b/web/default/src/features/subscriptions/components/subscriptions-columns.tsx index 4c4d6497..17bc0872 100644 --- a/web/default/src/features/subscriptions/components/subscriptions-columns.tsx +++ b/web/default/src/features/subscriptions/components/subscriptions-columns.tsx @@ -20,6 +20,7 @@ import { useMemo } from 'react' import { type ColumnDef } from '@tanstack/react-table' import { useTranslation } from 'react-i18next' import { formatQuota } from '@/lib/format' +import { BadgeCell } from '@/components/data-table' import { GroupBadge } from '@/components/group-badge' import { StatusBadge } from '@/components/status-badge' import { TableId } from '@/components/table-id' @@ -48,7 +49,7 @@ export function useSubscriptionsColumns(): ColumnDef[] { cell: ({ row }) => { const plan = row.original.plan return ( -
+
{plan.title}
{plan.subtitle && (
@@ -134,7 +135,7 @@ export function useSubscriptionsColumns(): ColumnDef[] { cell: ({ row }) => { const plan = row.original.plan return ( -
+ {plan.stripe_price_id && ( [] { copyable={false} /> )} -
+ ) }, size: 140, @@ -182,7 +183,11 @@ export function useSubscriptionsColumns(): ColumnDef[] { {t('No Upgrade')} ) } - return + return ( + + + + ) }, size: 120, }, diff --git a/web/default/src/features/system-settings/auth/custom-oauth/components/provider-table.tsx b/web/default/src/features/system-settings/auth/custom-oauth/components/provider-table.tsx index b3cdb1f9..4fd076d2 100644 --- a/web/default/src/features/system-settings/auth/custom-oauth/components/provider-table.tsx +++ b/web/default/src/features/system-settings/auth/custom-oauth/components/provider-table.tsx @@ -21,7 +21,7 @@ import { Pencil, Trash2, Plus } from 'lucide-react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' 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 { useDeleteProvider } from '../hooks/use-custom-oauth-mutations' import type { CustomOAuthProvider } from '../types' @@ -83,28 +83,33 @@ export function ProviderTable(props: ProviderTableProps) { id: 'slug', header: t('Slug'), cell: (provider) => ( - + + + ), }, { id: 'status', header: t('Status'), cell: (provider) => ( - + + + ), }, { id: '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, }, { diff --git a/web/default/src/features/system-settings/content/api-info-section.tsx b/web/default/src/features/system-settings/content/api-info-section.tsx index d8e5bfa3..fd69e993 100644 --- a/web/default/src/features/system-settings/content/api-info-section.tsx +++ b/web/default/src/features/system-settings/content/api-info-section.tsx @@ -16,7 +16,7 @@ along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ -import { useEffect, useState } from 'react' +import { useMemo, useState } from 'react' import * as z from 'zod' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' @@ -54,7 +54,7 @@ import { SelectTrigger, SelectValue, } from '@/components/ui/select' -import { StaticDataTable } from '@/components/data-table' +import { BadgeCell, StaticDataTable } from '@/components/data-table' import { Dialog } from '@/components/dialog' import { StatusBadge } from '@/components/status-badge' import { SettingsSwitchField } from '../components/settings-form-layout' @@ -103,18 +103,37 @@ const colorOptions = [ { 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) { const { t } = useTranslation() const updateOption = useUpdateOption() const apiInfoSchema = createApiInfoSchema(t) - const [apiInfoList, setApiInfoList] = useState([]) - const [isEnabled, setIsEnabled] = useState(enabled) - const [hasChanges, setHasChanges] = useState(false) + const parsedApiInfoList = useMemo(() => parseApiInfoList(data), [data]) + const [draftApiInfoList, setDraftApiInfoList] = useState( + null + ) + const [isEnabledDraft, setIsEnabledDraft] = useState(null) const [selectedIds, setSelectedIds] = useState([]) const [showDialog, setShowDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [editingApiInfo, setEditingApiInfo] = useState(null) const [deleteTarget, setDeleteTarget] = useState<'single' | 'batch'>('single') + const apiInfoList = draftApiInfoList ?? parsedApiInfoList + const isEnabled = isEnabledDraft ?? enabled + const hasChanges = draftApiInfoList !== null const form = useForm({ resolver: zodResolver(apiInfoSchema), @@ -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) => { try { await updateOption.mutateAsync({ key: 'console_setting.api_info_enabled', value: checked, }) - setIsEnabled(checked) + setIsEnabledDraft(checked) toast.success(t('Setting saved')) } catch { toast.error(t('Failed to update setting')) @@ -198,17 +197,15 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { const confirmDelete = () => { if (deleteTarget === 'single' && editingApiInfo) { - setApiInfoList((prev) => - prev.filter((item) => item.id !== editingApiInfo.id) + setDraftApiInfoList( + apiInfoList.filter((item) => item.id !== editingApiInfo.id) ) - setHasChanges(true) toast.success(t('API info deleted. Click "Save Settings" to apply.')) } else if (deleteTarget === 'batch') { - setApiInfoList((prev) => - prev.filter((item) => !selectedIds.includes(item.id)) + setDraftApiInfoList( + apiInfoList.filter((item) => !selectedIds.includes(item.id)) ) setSelectedIds([]) - setHasChanges(true) toast.success( t('{{count}} API entries deleted. Click "Save Settings" to apply.', { count: selectedIds.length, @@ -221,18 +218,17 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { const handleSubmitForm = (values: ApiInfoFormValues) => { if (editingApiInfo) { - setApiInfoList((prev) => - prev.map((item) => + setDraftApiInfoList( + apiInfoList.map((item) => item.id === editingApiInfo.id ? { ...item, ...values } : item ) ) toast.success(t('API info updated. Click "Save Settings" to apply.')) } else { 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.')) } - setHasChanges(true) setShowDialog(false) } @@ -243,7 +239,7 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { value: JSON.stringify(apiInfoList), }) if (result.success) { - setHasChanges(false) + setDraftApiInfoList(null) } } catch { toast.error(t('Failed to save API info')) @@ -330,22 +326,26 @@ export function ApiInfoSection({ enabled, data }: ApiInfoSectionProps) { header: t('URL'), cellClassName: 'max-w-xs truncate font-mono text-sm', cell: (apiInfo) => ( - + + + ), }, { id: 'route', header: t('Route'), cell: (apiInfo) => ( - + + + ), }, { diff --git a/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx b/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx index 6b153a3f..a88e3735 100644 --- a/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx +++ b/web/default/src/features/system-settings/models/upstream-ratio-sync-columns.tsx @@ -27,6 +27,7 @@ import { TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' +import { BadgeCell } from '@/components/data-table' import { StatusBadge } from '@/components/status-badge' import type { RatioType } from '../types' import { @@ -63,8 +64,8 @@ export function useUpstreamRatioSyncColumns( cell: ({ row }) => { const model = row.original.model return ( -
- {model} +
+ {model} {row.original.billingConflict && ( @@ -94,14 +95,11 @@ export function useUpstreamRatioSyncColumns( ratioTypeFilter ) return ( -
+
{fields.map((ratioType) => { const current = row.original.ratioTypes[ratioType]?.current return ( -
+ )} -
+ ) })}
@@ -215,7 +213,7 @@ export function useUpstreamRatioSyncColumns( ) return ( -
+
{fields.map((ratioType) => { const diff = row.original.ratioTypes[ratioType] const upstreamVal = diff?.upstreams?.[upstreamName] diff --git a/web/default/src/features/users/components/users-columns.tsx b/web/default/src/features/users/components/users-columns.tsx index 0fd70605..389cea0e 100644 --- a/web/default/src/features/users/components/users-columns.tsx +++ b/web/default/src/features/users/components/users-columns.tsx @@ -27,6 +27,7 @@ import { TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' +import { BadgeCell } from '@/components/data-table' import { GroupBadge } from '@/components/group-badge' import { LongText } from '@/components/long-text' import { StatusBadge } from '@/components/status-badge' @@ -227,7 +228,11 @@ export function useUsersColumns(): ColumnDef[] { header: t('Group'), cell: ({ row }) => { const group = row.getValue('group') as string - return + return ( + + + + ) }, filterFn: (row, id, value) => { const group = String(row.getValue(id) || t('User Group')).toLowerCase() @@ -272,7 +277,7 @@ export function useUsersColumns(): ColumnDef[] { const inviterId = user.inviter_id || 0 return ( -
+