Group #id/name and balance on the left and priority/weight plus response/last-tested on the right. Simplify field labels by dropping the CJK-unfriendly uppercase/letter-spacing, left-align the right column, and shorten the "Last Tested" zh label.
151 lines
6.0 KiB
TypeScript
Vendored
151 lines
6.0 KiB
TypeScript
Vendored
/*
|
|
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 <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
import { flexRender, type Row } from '@tanstack/react-table'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { cn } from '@/lib/utils'
|
|
import { GroupBadge } from '@/components/group-badge'
|
|
import { CHANNEL_STATUS } from '../constants'
|
|
import { isTagAggregateRow, parseGroupsList } from '../lib'
|
|
import type { Channel } from '../types'
|
|
import { ChannelRowActionsLayoutContext } from './channel-row-actions-context'
|
|
|
|
/**
|
|
* Bespoke channel card for the card view. Reuses every column's existing cell
|
|
* renderer via `flexRender`, so the table's information and interactions are
|
|
* preserved: row selection, provider/multi-key/IO.NET type badge, id,
|
|
* name/remark + warning icons, status (with tooltips), groups, inline
|
|
* priority/weight spinners, balance refresh, response/test times, tag
|
|
* expand-collapse, and the per-row (or per-tag) actions menu.
|
|
*/
|
|
export function ChannelCard({ row }: { row: Row<Channel> }) {
|
|
const { t } = useTranslation()
|
|
const isTagRow = isTagAggregateRow(row.original)
|
|
const cells = row.getAllCells()
|
|
|
|
const renderCell = (id: string) => {
|
|
const cell = cells.find((c) => c.column.id === id)
|
|
if (!cell || !cell.column.columnDef.cell) {
|
|
return null
|
|
}
|
|
return flexRender(cell.column.columnDef.cell, cell.getContext())
|
|
}
|
|
|
|
const fieldLabels: Record<string, string> = {
|
|
balance: t('Used / Remaining'),
|
|
response_time: t('Response'),
|
|
test_time: t('Last Tested'),
|
|
}
|
|
|
|
const groups = parseGroupsList(row.original.group ?? '')
|
|
|
|
const selectCell = renderCell('select')
|
|
const typeCell = renderCell('type')
|
|
const nameCell = renderCell('name')
|
|
const statusCell = renderCell('status')
|
|
const actionsCell = renderCell('actions')
|
|
const priorityCell = renderCell('priority')
|
|
const weightCell = renderCell('weight')
|
|
const balanceCell = renderCell('balance')
|
|
const responseCell = renderCell('response_time')
|
|
const testCell = renderCell('test_time')
|
|
|
|
const labelClass = 'text-muted-foreground text-[11px] font-medium select-none'
|
|
|
|
// In card view the enable/disable state is already conveyed by the inline
|
|
// power toggle, so the plain "Enabled"/"Disabled" badge is redundant. Keep
|
|
// only the informative states (e.g. auto-disabled, unknown) and tag rows.
|
|
const showStatusBadge =
|
|
isTagRow ||
|
|
(row.original.status !== CHANNEL_STATUS.ENABLED &&
|
|
row.original.status !== CHANNEL_STATUS.MANUAL_DISABLED)
|
|
|
|
return (
|
|
<div className='flex flex-col gap-3'>
|
|
{/* Row 1: selection + type, with status badge + actions menu */}
|
|
<div className='flex items-center justify-between gap-2'>
|
|
<div className='flex min-w-0 flex-1 items-center gap-2'>
|
|
{!isTagRow && selectCell && (
|
|
<span className='flex-shrink-0'>{selectCell}</span>
|
|
)}
|
|
<div className='min-w-0 overflow-hidden'>{typeCell}</div>
|
|
</div>
|
|
<div className='flex flex-shrink-0 items-center gap-1.5'>
|
|
{showStatusBadge && statusCell}
|
|
<ChannelRowActionsLayoutContext.Provider value='card'>
|
|
{actionsCell}
|
|
</ChannelRowActionsLayoutContext.Provider>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Body: left column (id/name + balance) paired with a right-aligned
|
|
column (priority/weight + response/test time). */}
|
|
<div className='flex items-start justify-between gap-3'>
|
|
{/* Left column */}
|
|
<div className='flex min-w-0 flex-1 flex-col gap-3 overflow-hidden'>
|
|
<div className='min-w-0 text-sm'>
|
|
{!isTagRow && <div className={labelClass}>#{row.original.id}</div>}
|
|
{nameCell}
|
|
</div>
|
|
<div className='min-w-0'>
|
|
<div className={cn('mb-1', labelClass)}>{fieldLabels.balance}</div>
|
|
<div className='min-w-0 overflow-hidden text-sm'>
|
|
{balanceCell ?? <span className='text-muted-foreground'>-</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right column (sits on the right, content left-aligned) */}
|
|
<div className='flex flex-shrink-0 flex-col gap-3'>
|
|
<div className='grid grid-cols-2 items-center gap-x-3'>
|
|
<span className={labelClass}>{t('Priority')}</span>
|
|
<span className={labelClass}>{t('Weight')}</span>
|
|
<div className='flex justify-start'>{priorityCell}</div>
|
|
<div className='flex justify-start'>{weightCell}</div>
|
|
</div>
|
|
<div className='grid grid-cols-2 gap-x-3'>
|
|
<div className={cn('mb-1', labelClass)}>
|
|
{fieldLabels.response_time}
|
|
</div>
|
|
<div className={cn('mb-1', labelClass)}>{fieldLabels.test_time}</div>
|
|
<div className='overflow-hidden text-sm'>
|
|
{responseCell ?? <span className='text-muted-foreground'>-</span>}
|
|
</div>
|
|
<div className='overflow-hidden text-sm'>
|
|
{testCell ?? <span className='text-muted-foreground'>-</span>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Last row: groups span the full width, showing every group (no label) */}
|
|
<div className='min-w-0'>
|
|
{groups.length > 0 ? (
|
|
<div className='-ml-1.5 flex flex-wrap gap-1'>
|
|
{groups.map((g) => (
|
|
<GroupBadge key={g} group={g} size='sm' />
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className='text-muted-foreground text-sm'>-</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|