fix(model-pricing): polish sync channel dialog layout
- keep input focus and invalid rings inside the shared Input component to avoid clipping in constrained containers. - make the sync channel selector table use a fixed header, internal body scrolling, and fixed pagination inside the dialog. - align status cells with their header and translate channel status labels through existing i18n keys.
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|||||||
type={type}
|
type={type}
|
||||||
data-slot='input'
|
data-slot='input'
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
|
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 focus-visible:ring-inset disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 aria-invalid:ring-inset md:text-sm',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
+39
-17
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
import { type ColumnDef, type RowSelectionState } from '@tanstack/react-table'
|
import type { ColumnDef, RowSelectionState } from '@tanstack/react-table'
|
||||||
import { Search } from 'lucide-react'
|
import { Search } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -119,6 +119,8 @@ export function ChannelSelectorDialog({
|
|||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
id: 'select',
|
id: 'select',
|
||||||
|
size: 44,
|
||||||
|
minSize: 44,
|
||||||
header: ({ table }) => (
|
header: ({ table }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={table.getIsAllPageRowsSelected()}
|
checked={table.getIsAllPageRowsSelected()}
|
||||||
@@ -142,6 +144,8 @@ export function ChannelSelectorDialog({
|
|||||||
{
|
{
|
||||||
accessorKey: 'name',
|
accessorKey: 'name',
|
||||||
header: t('Name'),
|
header: t('Name'),
|
||||||
|
size: 300,
|
||||||
|
minSize: 220,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const name = row.getValue('name') as string
|
const name = row.getValue('name') as string
|
||||||
const channel = row.original
|
const channel = row.original
|
||||||
@@ -165,6 +169,8 @@ export function ChannelSelectorDialog({
|
|||||||
{
|
{
|
||||||
accessorKey: 'base_url',
|
accessorKey: 'base_url',
|
||||||
header: t('Base URL'),
|
header: t('Base URL'),
|
||||||
|
size: 340,
|
||||||
|
minSize: 260,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const url = row.getValue('base_url') as string
|
const url = row.getValue('base_url') as string
|
||||||
return (
|
return (
|
||||||
@@ -180,6 +186,8 @@ export function ChannelSelectorDialog({
|
|||||||
{
|
{
|
||||||
accessorKey: 'status',
|
accessorKey: 'status',
|
||||||
header: t('Status'),
|
header: t('Status'),
|
||||||
|
size: 140,
|
||||||
|
minSize: 120,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const status = row.getValue('status') as number
|
const status = row.getValue('status') as number
|
||||||
const config =
|
const config =
|
||||||
@@ -198,7 +206,7 @@ export function ChannelSelectorDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
label={config.label}
|
label={t(config.label)}
|
||||||
variant={config.variant}
|
variant={config.variant}
|
||||||
size='sm'
|
size='sm'
|
||||||
copyable={false}
|
copyable={false}
|
||||||
@@ -209,6 +217,8 @@ export function ChannelSelectorDialog({
|
|||||||
{
|
{
|
||||||
id: 'endpoint',
|
id: 'endpoint',
|
||||||
header: t('Sync Endpoint'),
|
header: t('Sync Endpoint'),
|
||||||
|
size: 460,
|
||||||
|
minSize: 360,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const channel = row.original
|
const channel = row.original
|
||||||
const currentEndpoint =
|
const currentEndpoint =
|
||||||
@@ -224,14 +234,12 @@ export function ChannelSelectorDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex min-w-0 items-center gap-2'>
|
||||||
<Select
|
<Select
|
||||||
items={[
|
items={ENDPOINT_OPTIONS.map((option) => ({
|
||||||
...ENDPOINT_OPTIONS.map((option) => ({
|
value: option.value,
|
||||||
value: option.value,
|
label: option.label,
|
||||||
label: option.label,
|
}))}
|
||||||
})),
|
|
||||||
]}
|
|
||||||
value={endpointType}
|
value={endpointType}
|
||||||
onValueChange={(v) => v !== null && handleTypeChange(v)}
|
onValueChange={(v) => v !== null && handleTypeChange(v)}
|
||||||
>
|
>
|
||||||
@@ -253,7 +261,7 @@ export function ChannelSelectorDialog({
|
|||||||
value={currentEndpoint}
|
value={currentEndpoint}
|
||||||
onChange={(e) => updateEndpoint(channel.id, e.target.value)}
|
onChange={(e) => updateEndpoint(channel.id, e.target.value)}
|
||||||
placeholder={t('/your/endpoint')}
|
placeholder={t('/your/endpoint')}
|
||||||
className='h-8 w-40 font-mono text-xs'
|
className='h-8 min-w-0 flex-1 font-mono text-xs'
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -315,7 +323,7 @@ export function ChannelSelectorDialog({
|
|||||||
)}
|
)}
|
||||||
contentClassName='flex max-h-[90vh] max-w-[calc(100%-2rem)] flex-col sm:max-w-[90vw] xl:max-w-[1400px]'
|
contentClassName='flex max-h-[90vh] max-w-[calc(100%-2rem)] flex-col sm:max-w-[90vw] xl:max-w-[1400px]'
|
||||||
contentHeight='min(72vh, 720px)'
|
contentHeight='min(72vh, 720px)'
|
||||||
bodyClassName='space-y-4'
|
bodyClassName='flex h-full min-h-0 flex-col overflow-hidden'
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||||
@@ -325,27 +333,41 @@ export function ChannelSelectorDialog({
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='flex flex-1 flex-col gap-4 overflow-hidden'>
|
<div className='flex h-full min-h-0 flex-col gap-4 overflow-hidden'>
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex shrink-0 items-center gap-2'>
|
||||||
<div className='relative flex-1'>
|
<div className='relative flex-1'>
|
||||||
<Search className='text-muted-foreground absolute top-1/2 left-2 h-4 w-4 -translate-y-1/2' />
|
<Search className='text-muted-foreground pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2' />
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('Search by name or URL...')}
|
placeholder={t('Search by name or URL...')}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
className='ps-8'
|
className='ps-9'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataTableView
|
<DataTableView
|
||||||
table={table}
|
table={table}
|
||||||
containerClassName='flex-1 overflow-auto rounded-md'
|
containerClassName='min-h-0 flex-1 rounded-md'
|
||||||
|
tableContainerClassName='h-full min-h-0'
|
||||||
|
tableHeaderClassName='[background-color:var(--table-header)]'
|
||||||
|
splitHeaderScrollClassName='h-full'
|
||||||
|
bodyContainerClassName='[scrollbar-gutter:stable]'
|
||||||
|
splitHeader
|
||||||
|
getColumnClassName={(columnId, part) => {
|
||||||
|
if (columnId === 'select') return 'w-11 text-center align-middle'
|
||||||
|
if (columnId === 'status') {
|
||||||
|
return part === 'header' ? 'h-11 align-middle' : 'align-middle'
|
||||||
|
}
|
||||||
|
return part === 'header' ? 'h-11 align-middle' : 'align-middle'
|
||||||
|
}}
|
||||||
emptyContent={t('No channels found')}
|
emptyContent={t('No channels found')}
|
||||||
emptyCellClassName='h-24 text-center'
|
emptyCellClassName='h-24 text-center'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DataTablePagination table={table} />
|
<div className='shrink-0'>
|
||||||
|
<DataTablePagination table={table} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,5 +73,5 @@ export const RATIO_TYPE_OPTIONS = [
|
|||||||
export const CHANNEL_STATUS_CONFIG = {
|
export const CHANNEL_STATUS_CONFIG = {
|
||||||
1: { label: 'Enabled', variant: 'success' as const },
|
1: { label: 'Enabled', variant: 'success' as const },
|
||||||
2: { label: 'Disabled', variant: 'danger' as const },
|
2: { label: 'Disabled', variant: 'danger' as const },
|
||||||
3: { label: 'Auto-Disabled', variant: 'warning' as const },
|
3: { label: 'Auto Disabled', variant: 'warning' as const },
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
Reference in New Issue
Block a user