perf(web): streamline table actions and destructive dialogs (#5645)

* perf(data-table): autosize action columns

- exclude actions columns from shared table width calculations so action cells size to their content.
- remove fixed size and w-* width overrides from feature action columns to preserve content-based layout.

* perf(data-table): streamline row action controls

- expose common edit and status actions directly while moving secondary actions into overflow menus.
- add shared row action menu helpers so static and table rows use consistent action controls.
- let action columns size to their content instead of relying on fixed widths.

* fix(web): localize destructive dialog copy

- route delete, reset, and batch update confirmation text through i18n.
- add locale entries for affected channel, model, system settings, and user dialogs.

* perf(web): unify destructive dialog actions

- align delete and cleanup confirmation buttons with the shared destructive variant.
- replace custom destructive color overrides with semantic button variants.
- clean up lint errors in touched dialog files before committing.

* fix(web): add user action success translations

- add localized success messages for user delete, status, and role changes.
- keep user management toast copy available across all frontend locales.

* fix(data-table): prevent mobile badge clipping

- expose badge cell slots so mobile card styles can target nested badge wrappers.
- reset badge margins in card rows to keep provider icons fully visible on small screens.
This commit is contained in:
QuentinHsu
2026-06-25 13:13:41 +08:00
committed by GitHub
parent b191f47375
commit 9ba251ce5f
61 changed files with 1340 additions and 1131 deletions
@@ -18,18 +18,20 @@ For commercial licensing, please contact support@quantumnous.com
*/
import { useState } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { type Row } from '@tanstack/react-table'
import { MoreHorizontal, Pencil, Power, PowerOff, Trash2 } from 'lucide-react'
import type { Row } from '@tanstack/react-table'
import { Pencil, Power, PowerOff, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { ConfirmDialog } from '@/components/confirm-dialog'
import {
handleDeleteModel,
@@ -61,80 +63,77 @@ export function DataTableRowActions({ row }: DataTableRowActionsProps) {
handleToggleModelStatus(model.id, model.status, queryClient)
}
const toggleLabel = isEnabled ? t('Disable') : t('Enable')
return (
<div className='-ml-2'>
<DropdownMenu>
<DropdownMenuTrigger
<div className='-ml-1.5 flex items-center gap-1'>
<Tooltip>
<TooltipTrigger
render={
<Button
variant='ghost'
className='data-popup-open:bg-muted flex h-8 w-8 p-0'
size='icon-sm'
onClick={handleEdit}
aria-label={t('Edit')}
/>
}
>
<MoreHorizontal className='h-4 w-4' />
<span className='sr-only'>{t('Open menu')}</span>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-48'>
{/* Edit */}
<DropdownMenuItem onClick={handleEdit}>
{t('Edit')}
<DropdownMenuShortcut>
<Pencil size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<Pencil />
</TooltipTrigger>
<TooltipContent>{t('Edit')}</TooltipContent>
</Tooltip>
<DropdownMenuSeparator />
<Tooltip>
<TooltipTrigger
render={
<Button
variant='ghost'
size='icon-sm'
onClick={handleToggleStatus}
aria-label={toggleLabel}
className={
isEnabled
? 'text-destructive hover:text-destructive'
: 'text-success hover:text-success'
}
/>
}
>
{isEnabled ? <PowerOff /> : <Power />}
</TooltipTrigger>
<TooltipContent>{toggleLabel}</TooltipContent>
</Tooltip>
{/* Enable/Disable */}
<DropdownMenuItem onClick={handleToggleStatus}>
{isEnabled ? (
<>
{t('Disable')}
<DropdownMenuShortcut>
<PowerOff size={16} />
</DropdownMenuShortcut>
</>
) : (
<>
{t('Enable')}
<DropdownMenuShortcut>
<Power size={16} />
</DropdownMenuShortcut>
</>
)}
</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>
<ConfirmDialog
open={deleteConfirmOpen}
onOpenChange={setDeleteConfirmOpen}
title={t('Delete Model')}
desc={`Are you sure you want to delete "${model.model_name}"? This action cannot be undone.`}
confirmText='Delete'
destructive
handleConfirm={() => {
handleDeleteModel(model.id, queryClient)
setDeleteConfirmOpen(false)
<DataTableRowActionMenu ariaLabel={t('Open menu')}>
<DropdownMenuItem
onSelect={(e) => {
e.preventDefault()
setDeleteConfirmOpen(true)
}}
/>
</DropdownMenu>
className='text-destructive focus:text-destructive'
>
{t('Delete')}
<DropdownMenuShortcut>
<Trash2 size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DataTableRowActionMenu>
<ConfirmDialog
open={deleteConfirmOpen}
onOpenChange={setDeleteConfirmOpen}
title={t('Delete Model')}
desc={t(
'Are you sure you want to delete model "{{name}}"? This action cannot be undone.',
{ name: model.model_name }
)}
confirmText={t('Delete')}
destructive
handleConfirm={() => {
handleDeleteModel(model.id, queryClient)
setDeleteConfirmOpen(false)
}}
/>
</div>
)
}
@@ -16,13 +16,21 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { type ColumnDef } from '@tanstack/react-table'
import type { ColumnDef } from '@tanstack/react-table'
import { Eye, Info, Pencil, Settings2, Timer, Trash2 } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { formatTimestampToDate } from '@/lib/format'
import { Button } from '@/components/ui/button'
import { DataTableRowActionMenu } from '@/components/data-table/core/row-action-menu'
import { StatusBadge } from '@/components/status-badge'
import { TableId } from '@/components/table-id'
import { Button } from '@/components/ui/button'
import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
} from '@/components/ui/dropdown-menu'
import { formatTimestampToDate } from '@/lib/format'
import { getDeploymentStatusConfig } from '../constants'
import {
formatRemainingMinutes,
@@ -113,8 +121,9 @@ export function useDeploymentsColumns(opts: {
header: t('Provider'),
cell: ({ row }) => {
const provider = row.original.provider
if (!provider)
if (!provider) {
return <span className='text-muted-foreground text-xs'>-</span>
}
return (
<StatusBadge
label={String(provider)}
@@ -194,8 +203,9 @@ export function useDeploymentsColumns(opts: {
typeof row.original.hardware_quantity === 'number'
? row.original.hardware_quantity
: null
if (!hardware)
if (!hardware) {
return <span className='text-muted-foreground text-xs'>-</span>
}
return (
<div className='flex max-w-full min-w-0 flex-nowrap items-center gap-2 overflow-hidden'>
<StatusBadge
@@ -218,12 +228,12 @@ export function useDeploymentsColumns(opts: {
header: t('Created'),
meta: { mobileHidden: true },
cell: ({ row }) => {
const ts =
typeof row.original.created_at === 'number'
? row.original.created_at
: typeof row.original.created_at === 'string'
? Number(row.original.created_at)
: undefined
let ts: number | undefined
if (typeof row.original.created_at === 'number') {
ts = row.original.created_at
} else if (typeof row.original.created_at === 'string') {
ts = Number(row.original.created_at)
}
return (
<div className='min-w-[140px] font-mono text-sm'>
{formatTimestampToDate(ts)}
@@ -249,56 +259,51 @@ export function useDeploymentsColumns(opts: {
<div className='-ml-2.5 flex items-center gap-1'>
<Button
variant='ghost'
size='sm'
size='icon-sm'
onClick={() => opts.onViewLogs(id)}
title={t('View logs')}
aria-label={t('View logs')}
>
<Eye className='h-4 w-4' />
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => opts.onViewDetails(id)}
title={t('View details')}
>
<Info className='h-4 w-4' />
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => opts.onUpdateConfig(id)}
title={t('Update configuration')}
>
<Settings2 className='h-4 w-4' />
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => opts.onExtend(id)}
title={t('Extend deployment')}
>
<Timer className='h-4 w-4' />
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => opts.onRename(id, String(currentName))}
title={t('Rename deployment')}
>
<Pencil className='h-4 w-4' />
</Button>
<Button
variant='ghost'
size='sm'
onClick={() => opts.onDelete(row.original)}
title={t('Delete')}
>
<Trash2 className='h-4 w-4 text-red-500' />
<Eye />
</Button>
<DataTableRowActionMenu ariaLabel={t('Open menu')}>
<DropdownMenuItem onClick={() => opts.onViewDetails(id)}>
{t('View details')}
<DropdownMenuShortcut>
<Info size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => opts.onUpdateConfig(id)}>
{t('Update configuration')}
<DropdownMenuShortcut>
<Settings2 size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => opts.onExtend(id)}>
{t('Extend deployment')}
<DropdownMenuShortcut>
<Timer size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => opts.onRename(id, currentName)}>
{t('Rename deployment')}
<DropdownMenuShortcut>
<Pencil size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => opts.onDelete(row.original)}
className='text-destructive focus:text-destructive'
>
{t('Delete')}
<DropdownMenuShortcut>
<Trash2 size={16} />
</DropdownMenuShortcut>
</DropdownMenuItem>
</DataTableRowActionMenu>
</div>
)
},
size: 180,
meta: { pinned: 'right' as const },
},
]
@@ -308,7 +308,7 @@ export function DeploymentsTable() {
<AlertDialogAction
onClick={handleDelete}
disabled={isDeleting}
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
variant='destructive'
>
{isDeleting ? t('Deleting...') : t('Delete')}
</AlertDialogAction>
@@ -16,7 +16,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useState, type ReactNode } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
Layers3,
@@ -28,8 +28,13 @@ import {
} from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { cn } from '@/lib/utils'
import { useIsMobile } from '@/hooks/use-mobile'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { StaticDataTable } from '@/components/data-table/static/static-data-table'
import { StaticRowActions } from '@/components/data-table/static/static-row-actions'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge'
import { TableId } from '@/components/table-id'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import {
@@ -46,11 +51,9 @@ import {
EmptyMedia,
EmptyTitle,
} from '@/components/ui/empty'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { StaticDataTable } from '@/components/data-table'
import { Dialog } from '@/components/dialog'
import { StatusBadge } from '@/components/status-badge'
import { TableId } from '@/components/table-id'
import { useIsMobile } from '@/hooks/use-mobile'
import { cn } from '@/lib/utils'
import { deletePrefillGroup, getPrefillGroups } from '../../api'
import { prefillGroupsQueryKeys } from '../../lib'
import type { PrefillGroup } from '../../types'
@@ -140,21 +143,245 @@ export function PrefillGroupManagementDialog({
try {
const response = await deletePrefillGroup(deleteState.group.id)
if (response.success) {
toast.success(`Deleted "${deleteState.group.name}"`)
toast.success(
t('Deleted "{{name}}"', { name: deleteState.group.name })
)
queryClient.invalidateQueries({
queryKey: prefillGroupsQueryKeys.lists(),
})
setDeleteState({ open: false, group: null })
} else {
toast.error(response.message || 'Failed to delete group')
toast.error(response.message || t('Failed to delete group'))
}
} catch (err: unknown) {
toast.error((err as Error)?.message || 'Failed to delete group')
toast.error((err as Error)?.message || t('Failed to delete group'))
} finally {
setIsDeleting(false)
}
}
let groupsContent: ReactNode
if (isLoading) {
groupsContent = (
<div className='flex flex-col items-center justify-center gap-2 py-12 text-center'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
<p className='text-muted-foreground text-sm'>
{t('Fetching prefill groups...')}
</p>
</div>
)
} else if (normalizedGroups.length === 0) {
groupsContent = (
<Empty className='border border-dashed py-10'>
<EmptyMedia variant='icon'>
<Layers3 className='h-6 w-6' />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{t('No prefill groups yet')}</EmptyTitle>
<EmptyDescription>
{t(
'Create your first group to reuse model, tag, or endpoint selections anywhere in the dashboard.'
)}
</EmptyDescription>
</EmptyHeader>
<EmptyDescription>
{t('Prefill groups help you keep complex configurations in sync.')}
</EmptyDescription>
</Empty>
)
} else if (isMobile) {
groupsContent = (
<div className='space-y-3'>
{normalizedGroups.map(({ group, meta, parsedItems }) => (
<Card key={group.id} className='border-border/60'>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div className='space-y-2'>
<CardTitle className='flex flex-wrap items-center gap-2'>
{group.name}
<StatusBadge variant={meta.badge} size='sm' copyable={false}>
{meta.label}
<span className='text-muted-foreground/30'>·</span>
<span className='text-muted-foreground font-mono'>
#{group.id}
</span>
</StatusBadge>
</CardTitle>
{group.description ? (
<CardDescription className='line-clamp-2'>
{group.description}
</CardDescription>
) : (
<CardDescription className='text-muted-foreground italic'>
No description provided
</CardDescription>
)}
</div>
<div className='flex items-center gap-2'>
<Button
size='icon'
variant='outline'
onClick={() => onEditGroup(group)}
>
<Pencil className='h-4 w-4' />
<span className='sr-only'>{t('Edit group')}</span>
</Button>
<Button
size='icon'
variant='ghost'
className='text-destructive hover:text-destructive'
onClick={() => handleDeleteClick(group)}
>
<Trash2 className='h-4 w-4' />
<span className='sr-only'>{t('Delete group')}</span>
</Button>
</div>
</CardHeader>
<CardContent className='space-y-3'>
<div className='text-muted-foreground flex flex-wrap items-center gap-2 text-xs font-medium tracking-wide uppercase'>
<span>Items</span>
<StatusBadge
label={`${parsedItems.length} item${parsedItems.length === 1 ? '' : 's'}`}
variant='neutral'
size='sm'
copyable={false}
/>
</div>
{parsedItems.length > 0 ? (
<div className='flex flex-wrap gap-2'>
{parsedItems.slice(0, 6).map((item) => (
<StatusBadge
key={item}
label={item}
autoColor={item}
size='sm'
/>
))}
{parsedItems.length > 6 && (
<StatusBadge
label={`+${parsedItems.length - 6} more`}
variant='neutral'
size='sm'
copyable={false}
/>
)}
</div>
) : (
<p className='text-muted-foreground text-sm'>
{group.type === 'endpoint'
? 'No endpoint mappings configured.'
: 'No items configured yet.'}
</p>
)}
</CardContent>
</Card>
))}
</div>
)
} else {
groupsContent = (
<StaticDataTable
tableClassName='min-w-[680px]'
data={normalizedGroups}
getRowKey={({ group }) => group.id}
columns={[
{
id: 'group',
header: t('Group'),
cellClassName: 'align-top whitespace-normal',
cell: ({ group }) => (
<div className='flex flex-col gap-1'>
<div className='flex flex-wrap items-center gap-2'>
<span className='font-medium'>{group.name}</span>
<TableId value={group.id} />
</div>
{group.description ? (
<p className='text-muted-foreground text-xs'>
{group.description}
</p>
) : (
<p className='text-muted-foreground text-xs italic'>
No description provided
</p>
)}
</div>
),
},
{
id: 'type',
header: t('Type'),
cellClassName: 'align-top',
cell: ({ meta }) => (
<StatusBadge
label={meta.label}
variant={meta.badge}
size='sm'
copyable={false}
/>
),
},
{
id: 'items',
header: t('Items'),
className: 'min-w-[240px]',
cellClassName: 'align-top whitespace-normal',
cell: ({ group, parsedItems }) => (
<>
<div className='flex flex-wrap gap-2'>
{parsedItems.length > 0 ? (
<>
{parsedItems.slice(0, 6).map((item) => (
<StatusBadge
key={item}
label={item}
autoColor={item}
size='sm'
/>
))}
{parsedItems.length > 6 && (
<StatusBadge
label={`+${parsedItems.length - 6} more`}
variant='neutral'
size='sm'
copyable={false}
/>
)}
</>
) : (
<p className='text-muted-foreground text-sm'>
{group.type === 'endpoint'
? 'No endpoint mappings configured.'
: 'No items configured yet.'}
</p>
)}
</div>
<div className='text-muted-foreground mt-2 text-xs font-medium tracking-wide uppercase'>
{parsedItems.length} item
{parsedItems.length === 1 ? '' : 's'}
</div>
</>
),
},
{
id: 'actions',
header: t('Actions'),
className: 'text-right',
cellClassName: 'align-top',
cell: ({ group }) => (
<StaticRowActions
editLabel={t('Edit group')}
deleteLabel={t('Delete group')}
menuLabel={t('Open menu')}
onEdit={() => onEditGroup(group)}
onDelete={() => handleDeleteClick(group)}
/>
),
},
]}
/>
)
}
return (
<>
<Dialog
@@ -219,236 +446,7 @@ export function PrefillGroupManagementDialog({
</Alert>
)}
{isLoading ? (
<div className='flex flex-col items-center justify-center gap-2 py-12 text-center'>
<Loader2 className='text-muted-foreground h-6 w-6 animate-spin' />
<p className='text-muted-foreground text-sm'>
{t('Fetching prefill groups...')}
</p>
</div>
) : normalizedGroups.length === 0 ? (
<Empty className='border border-dashed py-10'>
<EmptyMedia variant='icon'>
<Layers3 className='h-6 w-6' />
</EmptyMedia>
<EmptyHeader>
<EmptyTitle>{t('No prefill groups yet')}</EmptyTitle>
<EmptyDescription>
{t(
'Create your first group to reuse model, tag, or endpoint selections anywhere in the dashboard.'
)}
</EmptyDescription>
</EmptyHeader>
<EmptyDescription>
{t(
'Prefill groups help you keep complex configurations in sync.'
)}
</EmptyDescription>
</Empty>
) : isMobile ? (
<div className='space-y-3'>
{normalizedGroups.map(({ group, meta, parsedItems }) => (
<Card key={group.id} className='border-border/60'>
<CardHeader className='flex flex-row items-start justify-between gap-4'>
<div className='space-y-2'>
<CardTitle className='flex flex-wrap items-center gap-2'>
{group.name}
<StatusBadge
variant={meta.badge}
size='sm'
copyable={false}
>
{meta.label}
<span className='text-muted-foreground/30'>·</span>
<span className='text-muted-foreground font-mono'>
#{group.id}
</span>
</StatusBadge>
</CardTitle>
{group.description ? (
<CardDescription className='line-clamp-2'>
{group.description}
</CardDescription>
) : (
<CardDescription className='text-muted-foreground italic'>
No description provided
</CardDescription>
)}
</div>
<div className='flex items-center gap-2'>
<Button
size='icon'
variant='outline'
onClick={() => onEditGroup(group)}
>
<Pencil className='h-4 w-4' />
<span className='sr-only'>Edit group</span>
</Button>
<Button
size='icon'
variant='ghost'
className='text-destructive hover:text-destructive'
onClick={() => handleDeleteClick(group)}
>
<Trash2 className='h-4 w-4' />
<span className='sr-only'>Delete group</span>
</Button>
</div>
</CardHeader>
<CardContent className='space-y-3'>
<div className='text-muted-foreground flex flex-wrap items-center gap-2 text-xs font-medium tracking-wide uppercase'>
<span>Items</span>
<StatusBadge
label={`${parsedItems.length} item${parsedItems.length === 1 ? '' : 's'}`}
variant='neutral'
size='sm'
copyable={false}
/>
</div>
{parsedItems.length > 0 ? (
<div className='flex flex-wrap gap-2'>
{parsedItems.slice(0, 6).map((item) => (
<StatusBadge
key={item}
label={item}
autoColor={item}
size='sm'
/>
))}
{parsedItems.length > 6 && (
<StatusBadge
label={`+${parsedItems.length - 6} more`}
variant='neutral'
size='sm'
copyable={false}
/>
)}
</div>
) : (
<p className='text-muted-foreground text-sm'>
{group.type === 'endpoint'
? 'No endpoint mappings configured.'
: 'No items configured yet.'}
</p>
)}
</CardContent>
</Card>
))}
</div>
) : (
<StaticDataTable
tableClassName='min-w-[680px]'
data={normalizedGroups}
getRowKey={({ group }) => group.id}
columns={[
{
id: 'group',
header: t('Group'),
cellClassName: 'align-top whitespace-normal',
cell: ({ group }) => (
<div className='flex flex-col gap-1'>
<div className='flex flex-wrap items-center gap-2'>
<span className='font-medium'>{group.name}</span>
<TableId value={group.id} />
</div>
{group.description ? (
<p className='text-muted-foreground text-xs'>
{group.description}
</p>
) : (
<p className='text-muted-foreground text-xs italic'>
No description provided
</p>
)}
</div>
),
},
{
id: 'type',
header: t('Type'),
cellClassName: 'align-top',
cell: ({ meta }) => (
<StatusBadge
label={meta.label}
variant={meta.badge}
size='sm'
copyable={false}
/>
),
},
{
id: 'items',
header: t('Items'),
className: 'min-w-[240px]',
cellClassName: 'align-top whitespace-normal',
cell: ({ group, parsedItems }) => (
<>
<div className='flex flex-wrap gap-2'>
{parsedItems.length > 0 ? (
<>
{parsedItems.slice(0, 6).map((item) => (
<StatusBadge
key={item}
label={item}
autoColor={item}
size='sm'
/>
))}
{parsedItems.length > 6 && (
<StatusBadge
label={`+${parsedItems.length - 6} more`}
variant='neutral'
size='sm'
copyable={false}
/>
)}
</>
) : (
<p className='text-muted-foreground text-sm'>
{group.type === 'endpoint'
? 'No endpoint mappings configured.'
: 'No items configured yet.'}
</p>
)}
</div>
<div className='text-muted-foreground mt-2 text-xs font-medium tracking-wide uppercase'>
{parsedItems.length} item
{parsedItems.length === 1 ? '' : 's'}
</div>
</>
),
},
{
id: 'actions',
header: t('Actions'),
className: 'w-[120px] text-right',
cellClassName: 'align-top',
cell: ({ group }) => (
<div className='flex justify-end gap-2'>
<Button
size='icon'
variant='outline'
onClick={() => onEditGroup(group)}
>
<Pencil className='h-4 w-4' />
<span className='sr-only'>Edit group</span>
</Button>
<Button
size='icon'
variant='ghost'
className='text-destructive hover:text-destructive'
onClick={() => handleDeleteClick(group)}
>
<Trash2 className='h-4 w-4' />
<span className='sr-only'>Delete group</span>
</Button>
</div>
),
},
]}
/>
)}
{groupsContent}
</div>
</Dialog>
@@ -458,13 +456,14 @@ export function PrefillGroupManagementDialog({
title={t('Delete group')}
desc={
<p>
{t('Are you sure you want to delete')}{' '}
<span className='font-medium'>{deleteState.group?.name}</span>
{t('? This action cannot be undone.')}
{t(
'Are you sure you want to delete group "{{name}}"? This action cannot be undone.',
{ name: deleteState.group?.name ?? '' }
)}
</p>
}
destructive
confirmText={isDeleting ? 'Deleting...' : 'Delete'}
confirmText={isDeleting ? t('Deleting...') : t('Delete')}
isLoading={isDeleting}
handleConfirm={handleDeleteConfirm}
/>
@@ -470,7 +470,6 @@ export function useModelsColumns(vendors: Vendor[] = []): ColumnDef<Model>[] {
cell: ({ row }) => {
return <DataTableRowActions row={row} />
},
size: 100,
enableSorting: false,
enableHiding: false,
meta: { pinned: 'right' as const },