{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 (
-