|
|
@@ -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')}
|
|
|
+ >
|
|
|
+ {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'
|
|
|
>
|
|
|
- {t('Upstream Updates')}
|
|
|
+ <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>
|
|
|
- <RefreshCw size={16} />
|
|
|
+ <TestTube size={16} />
|
|
|
</DropdownMenuShortcut>
|
|
|
</DropdownMenuItem>
|
|
|
- )}
|
|
|
|
|
|
- {/* Ollama Models (only for Ollama channels) */}
|
|
|
- {channel.type === 4 && (
|
|
|
- <DropdownMenuItem onClick={handleManageOllamaModels}>
|
|
|
- {t('Manage Ollama Models')}
|
|
|
+ {/* Query Balance */}
|
|
|
+ <DropdownMenuItem onClick={handleQueryBalance}>
|
|
|
+ {t('Query Balance')}
|
|
|
<DropdownMenuShortcut>
|
|
|
- <Boxes size={16} />
|
|
|
+ <DollarSign 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')}
|
|
|
+
|
|
|
+ {/* Fetch Models */}
|
|
|
+ <DropdownMenuItem onClick={handleFetchModels}>
|
|
|
+ {t('Fetch Models')}
|
|
|
<DropdownMenuShortcut>
|
|
|
- <Key size={16} />
|
|
|
+ <Download size={16} />
|
|
|
</DropdownMenuShortcut>
|
|
|
</DropdownMenuItem>
|
|
|
- )}
|
|
|
|
|
|
- <DropdownMenuSeparator />
|
|
|
+ {/* 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>
|
|
|
+ <RefreshCw size={16} />
|
|
|
+ </DropdownMenuShortcut>
|
|
|
+ </DropdownMenuItem>
|
|
|
+ )}
|
|
|
|
|
|
- {/* Enable/Disable */}
|
|
|
- <DropdownMenuItem onClick={handleToggleStatus}>
|
|
|
- {isEnabled ? (
|
|
|
- <>
|
|
|
- {t('Disable')}
|
|
|
+ {/* Ollama Models (only for Ollama channels) */}
|
|
|
+ {channel.type === 4 && (
|
|
|
+ <DropdownMenuItem onClick={handleManageOllamaModels}>
|
|
|
+ {t('Manage Ollama Models')}
|
|
|
<DropdownMenuShortcut>
|
|
|
- <PowerOff size={16} />
|
|
|
+ <Boxes size={16} />
|
|
|
</DropdownMenuShortcut>
|
|
|
- </>
|
|
|
- ) : (
|
|
|
- <>
|
|
|
- {t('Enable')}
|
|
|
+ </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')}
|
|
|
<DropdownMenuShortcut>
|
|
|
- <Power size={16} />
|
|
|
+ <Key size={16} />
|
|
|
</DropdownMenuShortcut>
|
|
|
- </>
|
|
|
+ </DropdownMenuItem>
|
|
|
)}
|
|
|
- </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>
|
|
|
+
|
|
|
+ <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>
|
|
|
)
|
|
|
}
|