Статусы через ReUI Badge, колонка действий — outline-меню без pin-slab. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
< 14 дн
|
< 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>
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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" />}>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user