/*
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)
}}
/>
)
}