fix(ui): привести таблицы к стилю ReUI PRO
Docker / build (push) Failing after 23s

Статусы через ReUI Badge, колонка действий — outline-меню без pin-slab.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-09-01 00:14:36 +07:00
co-authored by Cursor
parent 573ca097aa
commit 29325179f4
14 changed files with 284 additions and 146 deletions
+40 -3
View File
@@ -1,7 +1,9 @@
import type { LucideIcon } from 'lucide-react'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { cn } from '@cfdm/ui/lib/utils' import { IconTile } from '@/components/reui/icon-tile'
import { TruncatedText } from '@/components/truncated-text' import { TruncatedText } from '@/components/truncated-text'
import { cn } from '@cfdm/ui/lib/utils'
export function dataGridCellStack( export function dataGridCellStack(
primary: ReactNode, primary: ReactNode,
@@ -26,14 +28,49 @@ export function dataGridCellStack(
) )
} }
/**
* Name cell DNA — IconTile elevated size-10.5 + truncate.
* Preview: https://reui.io/preview/base/stats-12
* Docs: https://reui.io/docs/components/base/icon-tile
*/
export function DataGridNameCell({
icon: Icon,
title,
subtitle,
iconClassName = 'text-muted-foreground',
className,
}: {
icon: LucideIcon
title: ReactNode
subtitle?: ReactNode
iconClassName?: string
className?: string
}) {
return (
<div className={cn('flex min-w-0 items-center gap-2.5', className)}>
<IconTile variant="elevated" className="size-10.5" aria-hidden>
<Icon className={iconClassName} />
</IconTile>
<div className="flex min-w-0 flex-col gap-0.5">
<span className="truncate font-medium">{title}</span>
{subtitle ? (
<span className="truncate text-xs text-muted-foreground">{subtitle}</span>
) : null}
</div>
</div>
)
}
export function dataGridCellWithIcon( export function dataGridCellWithIcon(
icon: ReactNode, icon: ReactNode,
children: ReactNode, children: ReactNode,
className?: string, className?: string,
) { ) {
return ( return (
<div className={cn('flex items-center gap-2', className)}> <div className={cn('flex min-w-0 items-center gap-2.5', className)}>
<span className="shrink-0 text-muted-foreground">{icon}</span> <IconTile variant="elevated" className="size-10.5" aria-hidden>
{icon}
</IconTile>
{children} {children}
</div> </div>
) )
+67 -16
View File
@@ -1,8 +1,25 @@
import type { ReactNode } from 'react' import { useState, type ReactNode } from 'react'
import { PencilIcon, Trash2Icon } from 'lucide-react' import type { LucideIcon } from 'lucide-react'
import { MoreHorizontalIcon, PencilIcon, Trash2Icon } from 'lucide-react'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@cfdm/ui/components/dropdown-menu'
import { cn } from '@cfdm/ui/lib/utils'
import { ConfirmDialog } from './confirm-dialog' import { ConfirmDialog } from './confirm-dialog'
export interface RowActionExtra {
label: string
icon?: LucideIcon
onSelect: () => void
disabled?: boolean
}
interface RowActionsProps { interface RowActionsProps {
onEdit?: () => void onEdit?: () => void
onDelete?: () => void onDelete?: () => void
@@ -10,10 +27,15 @@ interface RowActionsProps {
deleteTitle?: string deleteTitle?: string
deleteDescription?: ReactNode deleteDescription?: ReactNode
deleteLabel?: string deleteLabel?: string
extra?: ReactNode extra?: RowActionExtra[]
className?: string className?: string
} }
/**
* Data-grid row actions — outline ⋯ menu.
* Preview: https://reui.io/preview/base/components/c-dropdown-menu-12
* Docs: https://reui.io/docs/components/base/dropdown-menu
*/
export function RowActions({ export function RowActions({
onEdit, onEdit,
onDelete, onDelete,
@@ -24,23 +46,52 @@ export function RowActions({
extra, extra,
className, className,
}: RowActionsProps) { }: RowActionsProps) {
if (!onEdit && !onDelete && !extra) return null const [deleteOpen, setDeleteOpen] = useState(false)
const extras = extra ?? []
if (!onEdit && !onDelete && extras.length === 0) return null
return ( return (
<div className={`flex justify-end gap-1 ${className ?? ''}`}> <div className={cn('flex justify-end', className)}>
{extra} <DropdownMenu>
{onEdit ? ( <DropdownMenuTrigger
<Button variant="ghost" size="icon-sm" onClick={onEdit} aria-label={editLabel}> render={
<PencilIcon /> <Button type="button" variant="outline" size="icon-sm" aria-label="Действия" />
</Button> }
) : null} >
<MoreHorizontalIcon className="size-4" aria-hidden />
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-40">
{extras.map((item) => {
const Icon = item.icon
return (
<DropdownMenuItem
key={item.label}
disabled={item.disabled}
onClick={item.onSelect}
>
{Icon ? <Icon aria-hidden /> : null}
{item.label}
</DropdownMenuItem>
)
})}
{onEdit ? (
<DropdownMenuItem onClick={onEdit}>
<PencilIcon aria-hidden />
{editLabel}
</DropdownMenuItem>
) : null}
{onDelete ? (
<DropdownMenuItem variant="destructive" onClick={() => setDeleteOpen(true)}>
<Trash2Icon aria-hidden />
{deleteLabel}
</DropdownMenuItem>
) : null}
</DropdownMenuContent>
</DropdownMenu>
{onDelete ? ( {onDelete ? (
<ConfirmDialog <ConfirmDialog
trigger={ open={deleteOpen}
<Button variant="ghost" size="icon-sm" aria-label="Удалить"> onOpenChange={setDeleteOpen}
<Trash2Icon />
</Button>
}
title={deleteTitle} title={deleteTitle}
description={deleteDescription} description={deleteDescription}
destructive destructive
+39 -17
View File
@@ -1,40 +1,62 @@
import type { ComponentProps } from 'react' import type { ComponentProps } from 'react'
import { cn } from '@cfdm/ui/lib/utils'
import { Badge } from '@/components/reui/badge' import { Badge } from '@/components/reui/badge'
type BadgeVariant = NonNullable<ComponentProps<typeof Badge>['variant']> type BadgeVariant = NonNullable<ComponentProps<typeof Badge>['variant']>
const STATUS_VARIANT: Record<string, BadgeVariant> = { const STATUS_VARIANT: Record<string, BadgeVariant> = {
active: 'success', active: 'success-light',
ok: 'success', ok: 'success-light',
paid: 'success', up: 'success-light',
available: 'success', paid: 'success-light',
complete: 'success', available: 'success-light',
paused: 'secondary', complete: 'success-light',
paused: 'invert-light',
archived: 'outline', archived: 'outline',
error: 'destructive', error: 'destructive-light',
denied: 'destructive', denied: 'destructive-light',
blocked: 'destructive', blocked: 'destructive-light',
running: 'info', down: 'destructive-light',
overdue: 'warning', running: 'info-light',
stale: 'warning', overdue: 'warning-light',
timeout: 'warning', stale: 'warning-light',
redirected: 'warning', timeout: 'warning-light',
partial: 'warning', redirected: 'warning-light',
partial: 'warning-light',
}
const DOT_COLOR: Record<string, string> = {
'success-light': 'bg-success',
success: 'bg-success',
'info-light': 'bg-info',
info: 'bg-info',
'warning-light': 'bg-warning',
warning: 'bg-warning',
'destructive-light': 'bg-destructive',
destructive: 'bg-destructive',
'invert-light': 'bg-muted-foreground',
secondary: 'bg-muted-foreground',
outline: 'bg-muted-foreground',
} }
export function StatusBadge({ export function StatusBadge({
status, status,
label, label,
size = 'default', size = 'sm',
className,
}: { }: {
status: string status: string
label?: string label?: string
size?: NonNullable<ComponentProps<typeof Badge>['size']> size?: NonNullable<ComponentProps<typeof Badge>['size']>
className?: string
}) { }) {
const variant = STATUS_VARIANT[status] ?? 'outline' const variant = STATUS_VARIANT[status] ?? 'outline'
const dotColor = DOT_COLOR[variant] ?? 'bg-muted-foreground'
return ( return (
<Badge variant={variant} size={size}> <Badge variant={variant} size={size} radius="full" className={cn('gap-1.5', className)}>
<span className={cn('size-1.5 shrink-0 rounded-full', dotColor)} aria-hidden />
{label ?? status} {label ?? status}
</Badge> </Badge>
) )
+2 -2
View File
@@ -8,7 +8,7 @@ export {
} from '@cfdm/shared/contracts/custom-fields' } from '@cfdm/shared/contracts/custom-fields'
import type { ReactNode } from 'react' import type { ReactNode } from 'react'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { import {
type CustomFieldDef, type CustomFieldDef,
formatCustomFieldValue, formatCustomFieldValue,
@@ -39,7 +39,7 @@ export function buildCustomFieldColumns<T extends { customData?: unknown }>(
} }
if (def.type === 'bool') { if (def.type === 'bool') {
return ( return (
<Badge variant={val ? 'default' : 'outline'}> <Badge variant={val ? 'success-light' : 'outline'} size="sm" radius="full">
{formatCustomFieldValue(def, val)} {formatCustomFieldValue(def, val)}
</Badge> </Badge>
) )
+31 -24
View File
@@ -20,10 +20,10 @@ import { buildApiCredentials } from '@cfdm/shared/utils/api-credentials'
import { snapshotQueryOptions } from '@/queries/snapshot' import { snapshotQueryOptions } from '@/queries/snapshot'
import { api, ApiError } from '@/lib/api-client' import { api, ApiError } from '@/lib/api-client'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { ResourcePage, columnDefFromDataGrid, KpiStatGrid } from '@/components/reui-kit' import { ResourcePage, columnDefFromDataGrid, KpiStatGrid } from '@/components/reui-kit'
import type { DataGridColumn } from '@/components/data-grid-types' import type { DataGridColumn } from '@/components/data-grid-types'
import { dataGridCellStack } from '@/components/data-grid-cells' import { dataGridCellStack, DataGridNameCell } from '@/components/data-grid-cells'
import { CrudListPage } from '@/components/crud-list-page' import { CrudListPage } from '@/components/crud-list-page'
import { RowActions } from '@/components/row-actions' import { RowActions } from '@/components/row-actions'
import { HealthModeBanner } from '@/components/health-mode-banner' import { HealthModeBanner } from '@/components/health-mode-banner'
@@ -62,10 +62,13 @@ const accountsSearchSchema = z.object({
health: z.string().optional(), health: z.string().optional(),
}) })
const HEALTH_BADGE_VARIANT: Record<AccountHealthFlag, 'default' | 'secondary' | 'destructive' | 'outline'> = { const HEALTH_BADGE_VARIANT: Record<
'stale-sync': 'secondary', AccountHealthFlag,
'low-balance': 'destructive', 'warning-light' | 'destructive-light' | 'outline'
'balance-mismatch': 'outline', > = {
'stale-sync': 'warning-light',
'low-balance': 'destructive-light',
'balance-mismatch': 'warning-light',
'no-creds': 'outline', 'no-creds': 'outline',
} }
@@ -267,7 +270,13 @@ function AccountsPage() {
key: 'name', key: 'name',
header: 'Аккаунт', header: 'Аккаунт',
icon: UserRoundIcon, icon: UserRoundIcon,
cell: (a) => dataGridCellStack(a.name, providerById.get(a.providerId)?.name ?? '—'), cell: (a) => (
<DataGridNameCell
icon={UserRoundIcon}
title={a.name}
subtitle={providerById.get(a.providerId)?.name ?? '—'}
/>
),
}, },
{ {
key: 'login', key: 'login',
@@ -285,12 +294,12 @@ function AccountsPage() {
cell: (a) => { cell: (a) => {
const flags = getAccountHealthFlags(a, healthCtx) const flags = getAccountHealthFlags(a, healthCtx)
if (!flags.length) { if (!flags.length) {
return <Badge variant="outline">OK</Badge> return <Badge variant="success-light" size="sm" radius="full">OK</Badge>
} }
return ( return (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{flags.map((flag) => ( {flags.map((flag) => (
<Badge key={flag} variant={HEALTH_BADGE_VARIANT[flag]}> <Badge key={flag} variant={HEALTH_BADGE_VARIANT[flag]} size="sm" radius="full">
{ACCOUNT_HEALTH_LABELS[flag]} {ACCOUNT_HEALTH_LABELS[flag]}
</Badge> </Badge>
))} ))}
@@ -303,7 +312,7 @@ function AccountsPage() {
header: 'API-доступ', header: 'API-доступ',
icon: PlugIcon, icon: PlugIcon,
cell: (a) => ( cell: (a) => (
<Badge variant={a.apiCredentialsSet ? 'default' : 'outline'}> <Badge variant={a.apiCredentialsSet ? 'success-light' : 'outline'} size="sm" radius="full">
{a.apiCredentialsSet ? 'установлены' : 'нет'} {a.apiCredentialsSet ? 'установлены' : 'нет'}
</Badge> </Badge>
), ),
@@ -323,7 +332,7 @@ function AccountsPage() {
sortValue: (a) => vpsCountByAccount.get(a.id) ?? 0, sortValue: (a) => vpsCountByAccount.get(a.id) ?? 0,
cell: (a) => { cell: (a) => {
const count = vpsCountByAccount.get(a.id) ?? 0 const count = vpsCountByAccount.get(a.id) ?? 0
return count ? <Badge variant="secondary">{count}</Badge> : <span className="text-muted-foreground">0</span> return count ? <Badge variant="secondary" size="sm" radius="full">{count}</Badge> : <span className="text-muted-foreground">0</span>
}, },
}, },
{ {
@@ -358,7 +367,7 @@ function AccountsPage() {
key: 'actions', key: 'actions',
header: '', header: '',
sortable: false, sortable: false,
className: 'w-32 text-right', className: 'w-12 text-right',
cell: (a) => { cell: (a) => {
const provider = providerById.get(a.providerId) const provider = providerById.get(a.providerId)
const canSync = accountBillmanagerUiReady(a, provider) const canSync = accountBillmanagerUiReady(a, provider)
@@ -369,17 +378,16 @@ function AccountsPage() {
deleteTitle="Удалить аккаунт?" deleteTitle="Удалить аккаунт?"
deleteDescription={`«${a.name}» будет удалён.`} deleteDescription={`«${a.name}» будет удалён.`}
extra={ extra={
canSync ? ( canSync
<Button ? [
variant="ghost" {
size="icon-sm" label: 'Синхронизировать',
aria-label="Синхронизировать" icon: RefreshCwIcon,
disabled={syncMut.isPending && syncMut.variables === a.id} disabled: syncMut.isPending && syncMut.variables === a.id,
onClick={() => syncMut.mutate(a.id)} onSelect: () => syncMut.mutate(a.id),
> },
<RefreshCwIcon /> ]
</Button> : undefined
) : null
} }
/> />
) )
@@ -450,7 +458,6 @@ function AccountsPage() {
columns={columnDefFromDataGrid(columns)} columns={columnDefFromDataGrid(columns)}
data={filteredAccounts} data={filteredAccounts}
getRowId={(a) => a.id} getRowId={(a) => a.id}
pinLastColumn
emptyTitle={health || hasActiveAccountFilters(filters) ? 'Нет аккаунтов с этими фильтрами' : 'Нет записей'} emptyTitle={health || hasActiveAccountFilters(filters) ? 'Нет аккаунтов с этими фильтрами' : 'Нет записей'}
emptyDescription={ emptyDescription={
health || hasActiveAccountFilters(filters) health || hasActiveAccountFilters(filters)
+6 -3
View File
@@ -19,7 +19,7 @@ import { toast } from 'sonner'
import { snapshotQueryOptions, ratesQueryOptions } from '@/queries/snapshot' import { snapshotQueryOptions, ratesQueryOptions } from '@/queries/snapshot'
import { api, ApiError } from '@/lib/api-client' import { api, ApiError } from '@/lib/api-client'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit' import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit'
import type { DataGridColumn } from '@/components/data-grid-types' import type { DataGridColumn } from '@/components/data-grid-types'
import { dataGridCellStack } from '@/components/data-grid-cells' import { dataGridCellStack } from '@/components/data-grid-cells'
@@ -100,7 +100,11 @@ function BalancePage() {
header: 'Движение', header: 'Движение',
icon: ArrowLeftRightIcon, icon: ArrowLeftRightIcon,
cell: (r) => ( cell: (r) => (
<Badge variant={r.direction === 'credit' ? 'default' : 'destructive'}> <Badge
variant={r.direction === 'credit' ? 'success-light' : 'destructive-light'}
size="sm"
radius="full"
>
<ArrowDownUpIcon data-icon="inline-start" /> <ArrowDownUpIcon data-icon="inline-start" />
{r.direction === 'credit' ? 'Приход' : 'Списание'} {r.direction === 'credit' ? 'Приход' : 'Списание'}
</Badge> </Badge>
@@ -236,7 +240,6 @@ function BalancePage() {
columns={columnDefFromDataGrid(columns)} columns={columnDefFromDataGrid(columns)}
data={rows} data={rows}
getRowId={(r) => r.id} getRowId={(r) => r.id}
pinLastColumn
footerContent={ footerContent={
<div className="flex flex-wrap justify-end gap-6 px-3 py-2 text-sm tabular-nums"> <div className="flex flex-wrap justify-end gap-6 px-3 py-2 text-sm tabular-nums">
<span> <span>
+13 -7
View File
@@ -277,7 +277,7 @@ function DashboardPage() {
iconClassName: 'text-primary', iconClassName: 'text-primary',
to: '/vps', to: '/vps',
footer: ( footer: (
<Badge variant="outline" size="sm"> <Badge variant="outline" size="sm" radius="full">
{totalCount} всего {totalCount} всего
</Badge> </Badge>
), ),
@@ -290,7 +290,7 @@ function DashboardPage() {
iconClassName: 'text-info', iconClassName: 'text-info',
to: '/reports', to: '/reports',
footer: ( footer: (
<Badge variant="info-light" size="sm"> <Badge variant="info-light" size="sm" radius="full">
оценка оценка
</Badge> </Badge>
), ),
@@ -308,7 +308,7 @@ function DashboardPage() {
iconClassName: 'text-success', iconClassName: 'text-success',
to: '/accounts', to: '/accounts',
footer: ( footer: (
<Badge variant="success-light" size="sm"> <Badge variant="success-light" size="sm" radius="full">
API API
</Badge> </Badge>
), ),
@@ -322,11 +322,11 @@ function DashboardPage() {
variant: runwayLow ? 'warning' : 'default', variant: runwayLow ? 'warning' : 'default',
to: '/accounts', to: '/accounts',
footer: runwayLow ? ( footer: runwayLow ? (
<Badge variant="warning-light" size="sm"> <Badge variant="warning-light" size="sm" radius="full">
&lt; 14 дн &lt; 14 дн
</Badge> </Badge>
) : ( ) : (
<Badge variant="outline" size="sm"> <Badge variant="outline" size="sm" radius="full">
запас запас
</Badge> </Badge>
), ),
@@ -343,6 +343,7 @@ function DashboardPage() {
<Badge <Badge
variant={expiringCount > 0 ? 'warning-light' : 'success-light'} variant={expiringCount > 0 ? 'warning-light' : 'success-light'}
size="sm" size="sm"
radius="full"
> >
{expiringCount > 0 ? 'скоро' : 'в норме'} {expiringCount > 0 ? 'скоро' : 'в норме'}
</Badge> </Badge>
@@ -360,6 +361,7 @@ function DashboardPage() {
<Badge <Badge
variant={issuesCount > 0 ? 'destructive-light' : 'success-light'} variant={issuesCount > 0 ? 'destructive-light' : 'success-light'}
size="sm" size="sm"
radius="full"
> >
{issuesCount > 0 ? 'требует внимания' : 'в норме'} {issuesCount > 0 ? 'требует внимания' : 'в норме'}
</Badge> </Badge>
@@ -406,7 +408,9 @@ function DashboardPage() {
<TabsTrigger value="issues" className={cn(DASHBOARD_TAB_TRIGGER_CLASS, 'gap-2')}> <TabsTrigger value="issues" className={cn(DASHBOARD_TAB_TRIGGER_CLASS, 'gap-2')}>
Проблемы Проблемы
{issues.length > 0 ? ( {issues.length > 0 ? (
<Badge variant="secondary">{issues.length}</Badge> <Badge variant="secondary" size="sm" radius="full">
{issues.length}
</Badge>
) : null} ) : null}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="recent" className={DASHBOARD_TAB_TRIGGER_CLASS}> <TabsTrigger value="recent" className={DASHBOARD_TAB_TRIGGER_CLASS}>
@@ -415,7 +419,9 @@ function DashboardPage() {
<TabsTrigger value="risk" className={cn(DASHBOARD_TAB_TRIGGER_CLASS, 'gap-2')}> <TabsTrigger value="risk" className={cn(DASHBOARD_TAB_TRIGGER_CLASS, 'gap-2')}>
Аккаунты Аккаунты
{atRisk.length > 0 ? ( {atRisk.length > 0 ? (
<Badge variant="outline">{atRisk.length}</Badge> <Badge variant="outline" size="sm" radius="full">
{atRisk.length}
</Badge>
) : null} ) : null}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
+1 -2
View File
@@ -143,7 +143,7 @@ function PaymentsPage() {
key: 'actions', key: 'actions',
header: '', header: '',
sortable: false, sortable: false,
className: 'w-24 text-right', className: 'w-12 text-right',
cell: (p) => ( cell: (p) => (
<RowActions <RowActions
onEdit={() => openEdit(p)} onEdit={() => openEdit(p)}
@@ -250,7 +250,6 @@ function PaymentsPage() {
columns={columnDefFromDataGrid(columns)} columns={columnDefFromDataGrid(columns)}
data={sorted} data={sorted}
getRowId={(p) => p.id} getRowId={(p) => p.id}
pinLastColumn
virtualization={sorted.length > 200} virtualization={sorted.length > 200}
height={560} height={560}
footerContent={ footerContent={
+3 -4
View File
@@ -25,7 +25,7 @@ import {
type ProjectFiltersState, type ProjectFiltersState,
} from '@/components/project-filters' } from '@/components/project-filters'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { EmptyState } from '@/components/empty-state' import { EmptyState } from '@/components/empty-state'
import { ProjectColorDot } from '@/components/project-color-dot' import { ProjectColorDot } from '@/components/project-color-dot'
import { ProjectEditSheet, projectFormDefaults } from '@/components/domain/project-edit-sheet' import { ProjectEditSheet, projectFormDefaults } from '@/components/domain/project-edit-sheet'
@@ -153,7 +153,7 @@ function ProjectsPage() {
className: 'text-right tabular-nums', className: 'text-right tabular-nums',
sortValue: (row) => row.vpsTotal, sortValue: (row) => row.vpsTotal,
cell: (row) => ( cell: (row) => (
<Badge variant="secondary"> <Badge variant="secondary" size="sm" radius="full">
{row.vpsActive}/{row.vpsTotal} {row.vpsActive}/{row.vpsTotal}
</Badge> </Badge>
), ),
@@ -183,7 +183,7 @@ function ProjectsPage() {
key: 'actions', key: 'actions',
header: '', header: '',
sortable: false, sortable: false,
className: 'w-24 text-right', className: 'w-12 text-right',
cell: (row) => ( cell: (row) => (
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<RowActions <RowActions
@@ -298,7 +298,6 @@ function ProjectsPage() {
columns={columnDefFromDataGrid(columns)} columns={columnDefFromDataGrid(columns)}
data={rows} data={rows}
getRowId={(r) => r.id} getRowId={(r) => r.id}
pinLastColumn
onRowClick={(row) => onRowClick={(row) =>
void navigate({ void navigate({
to: '/projects/$projectId', to: '/projects/$projectId',
+11 -13
View File
@@ -7,15 +7,14 @@ import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot' import { snapshotQueryOptions } from '@/queries/snapshot'
import { api, ApiError } from '@/lib/api-client' import { api, ApiError } from '@/lib/api-client'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit' import { ResourcePage, columnDefFromDataGrid } from '@/components/reui-kit'
import type { DataGridColumn } from '@/components/data-grid-types' import type { DataGridColumn } from '@/components/data-grid-types'
import { dataGridCellWithIcon } from '@/components/data-grid-cells' import { DataGridNameCell } from '@/components/data-grid-cells'
import { CrudListPage } from '@/components/crud-list-page' import { CrudListPage } from '@/components/crud-list-page'
import { RowActions } from '@/components/row-actions' import { RowActions } from '@/components/row-actions'
import { ProviderEditSheet, providerFormDefaults } from '@/components/domain/provider-edit-sheet' import { ProviderEditSheet, providerFormDefaults } from '@/components/domain/provider-edit-sheet'
import type { ProviderFormValues } from '@/lib/schemas' import type { ProviderFormValues } from '@/lib/schemas'
import { faviconUrlFromWebsite } from '@/lib/format'
import type { Provider } from '@/types/entities' import type { Provider } from '@/types/entities'
export const Route = createFileRoute('/_auth/providers')({ export const Route = createFileRoute('/_auth/providers')({
@@ -79,20 +78,19 @@ function ProvidersPage() {
key: 'name', key: 'name',
header: 'Хостер', header: 'Хостер',
icon: BuildingIcon, icon: BuildingIcon,
cell: (p) => { cell: (p) => (
const icon = p.website ? ( <DataGridNameCell
<img src={faviconUrlFromWebsite(p.website)} alt="" className="size-4 rounded-sm" /> icon={BuildingIcon}
) : ( title={p.name}
<BuildingIcon /> subtitle={p.website || undefined}
) />
return dataGridCellWithIcon(icon, <span className="font-medium">{p.name}</span>) ),
},
}, },
{ {
key: 'api', key: 'api',
header: 'API', header: 'API',
icon: PlugIcon, icon: PlugIcon,
cell: (p) => <Badge variant="outline">{p.apiType}</Badge>, cell: (p) => <Badge variant="outline" size="sm" radius="full">{p.apiType}</Badge>,
}, },
{ {
key: 'cur', key: 'cur',
@@ -104,7 +102,7 @@ function ProvidersPage() {
key: 'actions', key: 'actions',
header: '', header: '',
sortable: false, sortable: false,
className: 'w-24 text-right', className: 'w-12 text-right',
cell: (p) => ( cell: (p) => (
<RowActions <RowActions
onEdit={() => openEdit(p)} onEdit={() => openEdit(p)}
+6 -2
View File
@@ -16,7 +16,7 @@ import {
FramePanel, FramePanel,
FrameTitle, FrameTitle,
} from '@/components/reui/frame' } from '@/components/reui/frame'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { SelectField } from '@/components/select-field' import { SelectField } from '@/components/select-field'
import { getPaidUntilDate } from '@/lib/paid-until' import { getPaidUntilDate } from '@/lib/paid-until'
@@ -197,7 +197,11 @@ function RenewalsPage() {
<span className="text-xs text-muted-foreground">{item.sublabel}</span> <span className="text-xs text-muted-foreground">{item.sublabel}</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{item.overdue ? <Badge variant="destructive">Просрочено</Badge> : null} {item.overdue ? (
<Badge variant="destructive-light" size="sm" radius="full">
Просрочено
</Badge>
) : null}
<span className="tabular-nums text-sm">{item.date.toLocaleDateString('ru-RU')}</span> <span className="tabular-nums text-sm">{item.date.toLocaleDateString('ru-RU')}</span>
</div> </div>
</div> </div>
+2 -2
View File
@@ -6,7 +6,7 @@ import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot' import { snapshotQueryOptions } from '@/queries/snapshot'
import { api, ApiError } from '@/lib/api-client' import { api, ApiError } from '@/lib/api-client'
import { Alert, AlertDescription, AlertTitle } from '@cfdm/ui/components/alert' import { Alert, AlertDescription, AlertTitle } from '@cfdm/ui/components/alert'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { ResourcePage, columnDefFromDataGrid, loadStoredColumnVisibility, dataGridColumnVisibilityOptions } from '@/components/reui-kit' import { ResourcePage, columnDefFromDataGrid, loadStoredColumnVisibility, dataGridColumnVisibilityOptions } from '@/components/reui-kit'
import type { ColumnVisibilityState } from '@tanstack/react-table' import type { ColumnVisibilityState } from '@tanstack/react-table'
import type { DataGridColumn } from '@/components/data-grid-types' import type { DataGridColumn } from '@/components/data-grid-types'
@@ -224,7 +224,7 @@ function TariffsPage() {
header: 'Диск', header: 'Диск',
icon: HardDriveIcon, icon: HardDriveIcon,
sortValue: (t) => t.diskType ?? '', sortValue: (t) => t.diskType ?? '',
cell: (t) => <Badge variant="outline">{t.diskType ?? '—'}</Badge>, cell: (t) => <Badge variant="outline" size="sm" radius="full">{t.diskType ?? '—'}</Badge>,
}, },
{ {
key: 'location', key: 'location',
+11 -3
View File
@@ -17,7 +17,7 @@ import { QueryState } from '@/components/query-state'
import { StatusBadge } from '@/components/status-badge' import { StatusBadge } from '@/components/status-badge'
import { Skeleton } from '@cfdm/ui/components/skeleton' import { Skeleton } from '@cfdm/ui/components/skeleton'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@cfdm/ui/components/tabs'
import { import {
Frame, Frame,
@@ -200,8 +200,16 @@ function VpsDetailPage() {
<TabsContent value="overview" className="flex flex-col gap-4"> <TabsContent value="overview" className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<StatusBadge status={row.status} label={vpsStatusLabel(row.status)} /> <StatusBadge status={row.status} label={vpsStatusLabel(row.status)} />
{row.project ? <Badge variant="outline">{row.project}</Badge> : null} {row.project ? (
{row.environment ? <Badge variant="outline">{row.environment}</Badge> : null} <Badge variant="outline" size="sm" radius="full">
{row.project}
</Badge>
) : null}
{row.environment ? (
<Badge variant="outline" size="sm" radius="full">
{row.environment}
</Badge>
) : null}
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<DetailFrame title="Сеть" icon={<GlobeIcon className="size-4" />}> <DetailFrame title="Сеть" icon={<GlobeIcon className="size-4" />}>
+52 -48
View File
@@ -10,11 +10,12 @@ import { normalizeRatesPayload, effectiveVpsTariffCurrency, formatInProviderCurr
import { PageShell } from '@/components/page-shell' import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header' import { PageHeader } from '@/components/page-header'
import { Button } from '@cfdm/ui/components/button' import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge' import { Badge } from '@/components/reui/badge'
import { ResourcePage, columnDefFromDataGrid, loadStoredColumnVisibility, dataGridColumnVisibilityOptions } from '@/components/reui-kit' import { ResourcePage, columnDefFromDataGrid, loadStoredColumnVisibility, dataGridColumnVisibilityOptions } from '@/components/reui-kit'
import type { ColumnVisibilityState } from '@tanstack/react-table' import type { ColumnVisibilityState } from '@tanstack/react-table'
import type { DataGridColumn } from '@/components/data-grid-types' import type { DataGridColumn } from '@/components/data-grid-types'
import { dataGridCellStack, dataGridCellWithFlag } from '@/components/data-grid-cells' import { DataGridNameCell, dataGridCellStack, dataGridCellWithFlag } from '@/components/data-grid-cells'
import { StatusBadge } from '@/components/status-badge'
import { CountryFlag } from '@/components/country-flag' import { CountryFlag } from '@/components/country-flag'
import { QueryState } from '@/components/query-state' import { QueryState } from '@/components/query-state'
import { EmptyState } from '@/components/empty-state' import { EmptyState } from '@/components/empty-state'
@@ -344,30 +345,35 @@ function VpsPage() {
header: 'IP / DNS', header: 'IP / DNS',
icon: GlobeIcon, icon: GlobeIcon,
sortValue: (v) => v.ip || v.dns || '', sortValue: (v) => v.ip || v.dns || '',
cell: (v) => cell: (v) => (
dataGridCellStack( <DataGridNameCell
v.ip ? ( icon={GlobeIcon}
<Button title={
type="button" v.ip ? (
variant="link" <button
className="h-auto p-0 font-normal" type="button"
onClick={() => void copyText(v.ip, 'IP скопирован')} className="truncate text-left font-medium hover:underline"
> onClick={() => void copyText(v.ip, 'IP скопирован')}
{v.ip} >
</Button> {v.ip}
) : ( </button>
<span className="text-muted-foreground"></span> ) : (
), <span className="text-muted-foreground"></span>
v.dns ? ( )
<Button }
variant="link" subtitle={
className="text-muted-foreground h-auto p-0 text-xs font-normal" v.dns ? (
render={<Link to="/vps/$vpsId" params={{ vpsId: v.id }} />} <Link
> to="/vps/$vpsId"
{v.dns} params={{ vpsId: v.id }}
</Button> className="truncate hover:underline"
) : undefined, >
), {v.dns}
</Link>
) : undefined
}
/>
),
}, },
{ {
key: 'domains', key: 'domains',
@@ -439,11 +445,11 @@ function VpsPage() {
cell: (v) => ( cell: (v) => (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{v.access === 'shared' ? ( {v.access === 'shared' ? (
<Badge variant="outline">Общий</Badge> <Badge variant="outline" size="sm" radius="full">
Общий
</Badge>
) : null} ) : null}
<Badge variant={v.status === 'active' ? 'default' : v.status === 'archived' ? 'outline' : 'secondary'}> <StatusBadge status={v.status} label={vpsStatusLabel(v.status)} />
{vpsStatusLabel(v.status)}
</Badge>
</div> </div>
), ),
}, },
@@ -484,9 +490,9 @@ function VpsPage() {
cell: (v) => { cell: (v) => {
const ext = v as Vps & { lastHealthStatus?: string; monitoringEnabled?: boolean } const ext = v as Vps & { lastHealthStatus?: string; monitoringEnabled?: boolean }
if (!ext.monitoringEnabled) return <span className="text-muted-foreground"></span> if (!ext.monitoringEnabled) return <span className="text-muted-foreground"></span>
if (ext.lastHealthStatus === 'up') return <Badge variant="default">up</Badge> if (ext.lastHealthStatus === 'up') return <StatusBadge status="up" label="up" />
if (ext.lastHealthStatus === 'down') return <Badge variant="destructive">down</Badge> if (ext.lastHealthStatus === 'down') return <StatusBadge status="down" label="down" />
return <Badge variant="outline"></Badge> return <span className="text-muted-foreground"></span>
}, },
}, },
{ {
@@ -529,7 +535,7 @@ function VpsPage() {
header: '', header: '',
sortable: false, sortable: false,
enableHiding: false, enableHiding: false,
className: 'w-24 text-right', className: 'w-12 text-right',
cell: (v) => ( cell: (v) => (
<RowActions <RowActions
onEdit={v.access === 'shared' && v.grantPermission !== 'write' ? undefined : () => openEdit(v)} onEdit={v.access === 'shared' && v.grantPermission !== 'write' ? undefined : () => openEdit(v)}
@@ -537,19 +543,18 @@ function VpsPage() {
deleteTitle="Удалить VPS?" deleteTitle="Удалить VPS?"
deleteDescription={`IP ${v.ip} будет удалён безвозвратно.`} deleteDescription={`IP ${v.ip} будет удалён безвозвратно.`}
extra={ extra={
v.access !== 'shared' ? ( v.access !== 'shared'
<Button ? [
variant="ghost" {
size="icon-sm" label: 'Доступ',
aria-label="Доступ" icon: Share2Icon,
onClick={() => { onSelect: () => {
setAccessVps(v) setAccessVps(v)
setAccessOpen(true) setAccessOpen(true)
}} },
> },
<Share2Icon /> ]
</Button> : undefined
) : null
} }
/> />
), ),
@@ -683,7 +688,6 @@ function VpsPage() {
data={section.items} data={section.items}
getRowId={(v) => v.id} getRowId={(v) => v.id}
emptyTitle="VPS не найдены" emptyTitle="VPS не найдены"
pinLastColumn
dense={filters.tableCompact} dense={filters.tableCompact}
enableRowSelection enableRowSelection
onRowSelectionChange={setSelectedIds} onRowSelectionChange={setSelectedIds}