feat(ui): improve table controls and analytics filters
This commit is contained in:
@@ -28,6 +28,7 @@ import {
|
||||
} from '@/components/ui/tooltip'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { DataTableColumnHeader } from '@/components/data-table/column-header'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import {
|
||||
StatusBadge,
|
||||
dotColorMap,
|
||||
@@ -876,8 +877,8 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
const group = row.getValue('group') as string
|
||||
const groupArray = parseGroupsList(group)
|
||||
|
||||
const groupBadges = groupArray.map((g, idx) => (
|
||||
<StatusBadge key={idx} label={g} autoColor={g} size='sm' />
|
||||
const groupBadges = groupArray.map((g) => (
|
||||
<GroupBadge key={g} group={g} size='sm' />
|
||||
))
|
||||
|
||||
return (
|
||||
@@ -1035,7 +1036,7 @@ export function useChannelsColumns(): ColumnDef<Channel>[] {
|
||||
|
||||
return <DataTableRowActions row={row} />
|
||||
},
|
||||
size: 100,
|
||||
size: 132,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
DISABLED_ROW_DESKTOP,
|
||||
DISABLED_ROW_MOBILE,
|
||||
DataTableToolbar,
|
||||
TableSkeleton,
|
||||
TableEmpty,
|
||||
@@ -368,9 +370,7 @@ export function ChannelsTable() {
|
||||
emptyTitle='No Channels Found'
|
||||
emptyDescription='No channels available. Create your first channel to get started.'
|
||||
getRowClassName={(row) =>
|
||||
isDisabledChannelRow(row.original)
|
||||
? 'border-l-4 border-l-muted-foreground/35 bg-muted/85 dark:border-l-zinc-300/70 dark:bg-zinc-700/55'
|
||||
: undefined
|
||||
isDisabledChannelRow(row.original) ? DISABLED_ROW_MOBILE : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
@@ -419,7 +419,7 @@ export function ChannelsTable() {
|
||||
data-state={row.getIsSelected() && 'selected'}
|
||||
className={cn(
|
||||
isDisabledChannelRow(row.original) &&
|
||||
'bg-muted/85 hover:bg-muted [&>td:first-child]:border-l-muted-foreground/35 dark:bg-zinc-700/55 dark:hover:bg-zinc-700/70 [&>td:first-child]:border-l-4 [&>td:first-child]:pl-1 dark:[&>td:first-child]:border-l-zinc-300/70'
|
||||
DISABLED_ROW_DESKTOP
|
||||
)}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
|
||||
+180
-121
@@ -6,6 +6,7 @@ import {
|
||||
Boxes,
|
||||
Pencil,
|
||||
TestTube,
|
||||
Gauge,
|
||||
DollarSign,
|
||||
Download,
|
||||
Copy,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
Key,
|
||||
Trash2,
|
||||
RefreshCw,
|
||||
Loader2,
|
||||
} from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -25,10 +27,17 @@ import {
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { MODEL_FETCHABLE_TYPES } from '../constants'
|
||||
import {
|
||||
channelsQueryKeys,
|
||||
handleDeleteChannel,
|
||||
handleTestChannel,
|
||||
handleToggleChannelStatus,
|
||||
isChannelEnabled,
|
||||
isMultiKeyChannel,
|
||||
@@ -47,6 +56,8 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
const { setOpen, setCurrentRow, upstream } = useChannels()
|
||||
const queryClient = useQueryClient()
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||
const [isTesting, setIsTesting] = useState(false)
|
||||
const [isTogglingStatus, setIsTogglingStatus] = useState(false)
|
||||
|
||||
const isEnabled = isChannelEnabled(channel)
|
||||
const isMultiKey = isMultiKeyChannel(channel)
|
||||
@@ -61,6 +72,18 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
setOpen('test-channel')
|
||||
}
|
||||
|
||||
const handleDirectTest = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation()
|
||||
setIsTesting(true)
|
||||
try {
|
||||
await handleTestChannel(channel.id, undefined, () => {
|
||||
queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() })
|
||||
})
|
||||
} finally {
|
||||
setIsTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleQueryBalance = () => {
|
||||
setCurrentRow(channel)
|
||||
setOpen('balance-query')
|
||||
@@ -86,148 +109,184 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
setOpen('multi-key-manage')
|
||||
}
|
||||
|
||||
const handleToggleStatus = () => {
|
||||
handleToggleChannelStatus(channel.id, channel.status, queryClient)
|
||||
const handleToggleStatus = async (
|
||||
e?: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
e?.stopPropagation()
|
||||
setIsTogglingStatus(true)
|
||||
try {
|
||||
await handleToggleChannelStatus(channel.id, channel.status, queryClient)
|
||||
} finally {
|
||||
setIsTogglingStatus(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='data-[state=open]:bg-muted flex h-8 w-8 p-0'
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Open menu')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-48'>
|
||||
{/* Edit */}
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
{t('Edit')}
|
||||
<DropdownMenuShortcut>
|
||||
<Pencil size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Test Connection */}
|
||||
<DropdownMenuItem onClick={handleTest}>
|
||||
{t('Test Connection')}
|
||||
<DropdownMenuShortcut>
|
||||
<TestTube size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Query Balance */}
|
||||
<DropdownMenuItem onClick={handleQueryBalance}>
|
||||
{t('Query Balance')}
|
||||
<DropdownMenuShortcut>
|
||||
<DollarSign size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Fetch Models */}
|
||||
<DropdownMenuItem onClick={handleFetchModels}>
|
||||
{t('Fetch Models')}
|
||||
<DropdownMenuShortcut>
|
||||
<Download size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Detect Upstream Updates (only for fetchable channel types) */}
|
||||
{MODEL_FETCHABLE_TYPES.has(channel.type) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const meta = parseUpstreamUpdateMeta(channel.settings)
|
||||
if (
|
||||
meta.pendingAddModels.length > 0 ||
|
||||
meta.pendingRemoveModels.length > 0
|
||||
) {
|
||||
upstream.openModal(
|
||||
channel,
|
||||
meta.pendingAddModels,
|
||||
meta.pendingRemoveModels,
|
||||
meta.pendingAddModels.length > 0 ? 'add' : 'remove'
|
||||
)
|
||||
} else {
|
||||
upstream.detectChannelUpdates(channel)
|
||||
}
|
||||
}}
|
||||
<div className='flex items-center justify-end gap-1'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
onClick={handleDirectTest}
|
||||
disabled={isTesting}
|
||||
aria-label={t('Test Connection')}
|
||||
>
|
||||
{t('Upstream Updates')}
|
||||
{isTesting ? (
|
||||
<Loader2 className='size-4 animate-spin' />
|
||||
) : (
|
||||
<Gauge className='size-4' />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t('Test Connection')}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon-sm'
|
||||
onClick={handleToggleStatus}
|
||||
disabled={isTogglingStatus}
|
||||
aria-label={isEnabled ? t('Disable') : t('Enable')}
|
||||
className={
|
||||
isEnabled
|
||||
? 'text-destructive hover:text-destructive'
|
||||
: 'text-emerald-600 hover:text-emerald-600 dark:text-emerald-400 dark:hover:text-emerald-400'
|
||||
}
|
||||
>
|
||||
{isTogglingStatus ? (
|
||||
<Loader2 className='size-4 animate-spin' />
|
||||
) : isEnabled ? (
|
||||
<PowerOff className='size-4' />
|
||||
) : (
|
||||
<Power className='size-4' />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{isEnabled ? t('Disable') : t('Enable')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='data-[state=open]:bg-muted flex h-8 w-8 p-0'
|
||||
>
|
||||
<MoreHorizontal className='h-4 w-4' />
|
||||
<span className='sr-only'>{t('Open menu')}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end' className='w-48'>
|
||||
{/* Edit */}
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
{t('Edit')}
|
||||
<DropdownMenuShortcut>
|
||||
<RefreshCw size={16} />
|
||||
<Pencil size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{/* Ollama Models (only for Ollama channels) */}
|
||||
{channel.type === 4 && (
|
||||
<DropdownMenuItem onClick={handleManageOllamaModels}>
|
||||
{t('Manage Ollama Models')}
|
||||
{/* Test Connection */}
|
||||
<DropdownMenuItem onClick={handleTest}>
|
||||
{t('Test Connection')}
|
||||
<DropdownMenuShortcut>
|
||||
<Boxes size={16} />
|
||||
<TestTube size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Copy Channel */}
|
||||
<DropdownMenuItem onClick={handleCopy}>
|
||||
{t('Copy Channel')}
|
||||
<DropdownMenuShortcut>
|
||||
<Copy size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Manage Keys (only for multi-key channels) */}
|
||||
{isMultiKey && (
|
||||
<DropdownMenuItem onClick={handleManageKeys}>
|
||||
{t('Manage Keys')}
|
||||
{/* Query Balance */}
|
||||
<DropdownMenuItem onClick={handleQueryBalance}>
|
||||
{t('Query Balance')}
|
||||
<DropdownMenuShortcut>
|
||||
<Key size={16} />
|
||||
<DollarSign size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
{/* Fetch Models */}
|
||||
<DropdownMenuItem onClick={handleFetchModels}>
|
||||
{t('Fetch Models')}
|
||||
<DropdownMenuShortcut>
|
||||
<Download size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Enable/Disable */}
|
||||
<DropdownMenuItem onClick={handleToggleStatus}>
|
||||
{isEnabled ? (
|
||||
<>
|
||||
{t('Disable')}
|
||||
{/* Detect Upstream Updates (only for fetchable channel types) */}
|
||||
{MODEL_FETCHABLE_TYPES.has(channel.type) && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const meta = parseUpstreamUpdateMeta(channel.settings)
|
||||
if (
|
||||
meta.pendingAddModels.length > 0 ||
|
||||
meta.pendingRemoveModels.length > 0
|
||||
) {
|
||||
upstream.openModal(
|
||||
channel,
|
||||
meta.pendingAddModels,
|
||||
meta.pendingRemoveModels,
|
||||
meta.pendingAddModels.length > 0 ? 'add' : 'remove'
|
||||
)
|
||||
} else {
|
||||
upstream.detectChannelUpdates(channel)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('Upstream Updates')}
|
||||
<DropdownMenuShortcut>
|
||||
<PowerOff size={16} />
|
||||
<RefreshCw size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('Enable')}
|
||||
<DropdownMenuShortcut>
|
||||
<Power size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
{/* Ollama Models (only for Ollama channels) */}
|
||||
{channel.type === 4 && (
|
||||
<DropdownMenuItem onClick={handleManageOllamaModels}>
|
||||
{t('Manage Ollama Models')}
|
||||
<DropdownMenuShortcut>
|
||||
<Boxes size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{/* Delete */}
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
setDeleteConfirmOpen(true)
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
>
|
||||
{t('Delete')}
|
||||
<DropdownMenuShortcut>
|
||||
<Trash2 size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Copy Channel */}
|
||||
<DropdownMenuItem onClick={handleCopy}>
|
||||
{t('Copy Channel')}
|
||||
<DropdownMenuShortcut>
|
||||
<Copy size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
|
||||
{/* Manage Keys (only for multi-key channels) */}
|
||||
{isMultiKey && (
|
||||
<DropdownMenuItem onClick={handleManageKeys}>
|
||||
{t('Manage Keys')}
|
||||
<DropdownMenuShortcut>
|
||||
<Key size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* Delete */}
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault()
|
||||
setDeleteConfirmOpen(true)
|
||||
}}
|
||||
className='text-destructive focus:text-destructive'
|
||||
>
|
||||
{t('Delete')}
|
||||
<DropdownMenuShortcut>
|
||||
<Trash2 size={16} />
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<ConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
@@ -241,6 +300,6 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
|
||||
setDeleteConfirmOpen(false)
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from '@/components/ui/select'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { GroupBadge } from '@/components/group-badge'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import {
|
||||
editTagChannels,
|
||||
@@ -388,17 +389,14 @@ export function EditTagDialog({ open, onOpenChange }: EditTagDialogProps) {
|
||||
</Label>
|
||||
<div className='flex min-h-[60px] flex-wrap gap-2 rounded-md border p-3'>
|
||||
{availableGroups.map((group) => (
|
||||
<StatusBadge
|
||||
<GroupBadge
|
||||
key={group}
|
||||
variant={
|
||||
selectedGroups.includes(group) ? 'info' : 'neutral'
|
||||
}
|
||||
className='cursor-pointer transition-opacity hover:opacity-70'
|
||||
copyable={false}
|
||||
group={group}
|
||||
className={`cursor-pointer rounded-sm transition-opacity hover:opacity-70 ${
|
||||
selectedGroups.includes(group) ? 'bg-muted/70 px-1' : ''
|
||||
}`}
|
||||
onClick={() => handleToggleGroup(group)}
|
||||
>
|
||||
{group}
|
||||
</StatusBadge>
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user