/* 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 { useContext, useState } from 'react' import { useQueryClient } from '@tanstack/react-query' import type { Row } from '@tanstack/react-table' import { MoreHorizontal, Boxes, Pencil, PlugZap, Gauge, DollarSign, Download, Copy, Power, PowerOff, Key, Trash2, RefreshCw, Loader2, } from 'lucide-react' import { useTranslation } from 'react-i18next' import { ConfirmDialog } from '@/components/confirm-dialog' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { MODEL_FETCHABLE_TYPES } from '../constants' import { channelsQueryKeys, handleDeleteChannel, handleTestChannel, handleToggleChannelStatus, isChannelEnabled, isMultiKeyChannel, } from '../lib' import { parseUpstreamUpdateMeta } from '../lib/upstream-update-utils' import type { Channel } from '../types' import { ChannelRowActionsLayoutContext } from './channel-row-actions-context' import { useChannels } from './channels-provider' interface DataTableRowActionsProps { row: Row } export function DataTableRowActions({ row }: DataTableRowActionsProps) { const { t } = useTranslation() const layout = useContext(ChannelRowActionsLayoutContext) const channel = row.original 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) const handleEdit = () => { setCurrentRow(channel) setOpen('update-channel') } const handleTest = () => { setCurrentRow(channel) setOpen('test-channel') } const handleDirectTest = async (e: React.MouseEvent) => { e.stopPropagation() setIsTesting(true) try { await handleTestChannel(channel.id, { channelName: channel.name }, () => { queryClient.invalidateQueries({ queryKey: channelsQueryKeys.lists() }) }) } finally { setIsTesting(false) } } const handleQueryBalance = () => { setCurrentRow(channel) setOpen('balance-query') } const handleFetchModels = () => { setCurrentRow(channel) setOpen('fetch-models') } const handleManageOllamaModels = () => { setCurrentRow(channel) setOpen('ollama-models') } const handleCopy = () => { setCurrentRow(channel) setOpen('copy-channel') } const handleManageKeys = () => { setCurrentRow(channel) setOpen('multi-key-manage') } const handleToggleStatus = async ( e?: React.MouseEvent ) => { e?.stopPropagation() setIsTogglingStatus(true) try { await handleToggleChannelStatus(channel.id, channel.status, queryClient) } finally { setIsTogglingStatus(false) } } let statusIcon = if (isTogglingStatus) { statusIcon = } else if (isEnabled) { statusIcon = } return (
{ e.stopPropagation() handleEdit() }} aria-label={t('Edit')} /> } > {t('Edit')} } > {isTesting ? ( ) : ( )} {t('Test Connection')} {layout === 'card' && ( { e.stopPropagation() handleTest() }} aria-label={t('Test Channel Connection')} /> } > {t('Test Channel Connection')} )} } > {statusIcon} {isEnabled ? t('Disable') : t('Enable')} } > {t('Open menu')} {/* Test Connection */} {t('Test Connection')} {/* Query Balance */} {t('Query Balance')} {/* Fetch Models */} {t('Fetch Models')} {/* Detect Upstream Updates (only for fetchable channel types) */} {MODEL_FETCHABLE_TYPES.has(channel.type) && ( { 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')} )} {/* Ollama Models (only for Ollama channels) */} {channel.type === 4 && ( {t('Manage Ollama Models')} )} {/* Copy Channel */} {t('Copy Channel')} {/* Manage Keys (only for multi-key channels) */} {isMultiKey && ( {t('Manage Keys')} )} {/* Delete */} { e.preventDefault() setDeleteConfirmOpen(true) }} className='text-destructive focus:text-destructive' > {t('Delete')} { handleDeleteChannel(channel.id, queryClient) setDeleteConfirmOpen(false) }} />
) }