feat(web): иконки заголовков и rich cells во всех таблицах

Lucide-иконки в заголовках, двухстрочные ячейки аккаунтов, флаги локации VPS, favicon хостеров.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-06-27 01:42:45 +07:00
co-authored by Cursor
parent a6f202f659
commit 4c7dfc45d6
7 changed files with 281 additions and 124 deletions
+18 -9
View File
@@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { PlusIcon, PencilIcon, Trash2Icon, RefreshCwIcon } from 'lucide-react'
import { PlusIcon, PencilIcon, Trash2Icon, RefreshCwIcon, UserRoundIcon, KeyRoundIcon, PlugIcon, ReceiptIcon, WalletIcon } from 'lucide-react'
import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot'
@@ -12,6 +12,7 @@ import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellStack } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
import { ConfirmDialog } from '@/components/confirm-dialog'
@@ -97,37 +98,45 @@ function AccountsPage() {
{
key: 'name',
header: 'Аккаунт',
cell: (a) => (
<div className="flex flex-col">
<span className="font-medium">{a.name}</span>
<span className="text-xs text-muted-foreground">{providerById.get(a.providerId)?.name ?? '—'}</span>
</div>
),
icon: UserRoundIcon,
cell: (a) => dataGridCellStack(a.name, providerById.get(a.providerId)?.name ?? '—'),
},
{
key: 'login',
header: 'Логин',
icon: KeyRoundIcon,
cell: (a) => <span className="text-muted-foreground">{a.login || '—'}</span>,
},
{ key: 'login', header: 'Логин', cell: (a) => <span className="text-muted-foreground">{a.login || '—'}</span> },
{
key: 'creds',
header: 'API-доступ',
icon: PlugIcon,
cell: (a) => <Badge variant={a.apiCredentialsSet ? 'default' : 'outline'}>{a.apiCredentialsSet ? 'установлены' : 'нет'}</Badge>,
},
{
key: 'mode',
header: 'Биллинг',
icon: ReceiptIcon,
cell: (a) => <span>{billingModeLabel(a.billingMode ?? 'monthly')}</span>,
},
{
key: 'balance',
header: 'Баланс (API)',
icon: WalletIcon,
headerClassName: 'text-right',
className: 'text-right',
sortValue: (a) => Number(a.balance_api ?? 0),
cell: (a) => {
const provider = providerById.get(a.providerId)
if (!accountBillmanagerUiReady(a, provider)) return <span className="text-muted-foreground"></span>
const cur = a.balance_currency || a.currency || provider?.baseCurrency || 'USD'
return <span className="tabular-nums">{formatCurrency(Number(a.balance_api ?? 0), cur)}</span>
return <span className="tabular-nums font-medium">{formatCurrency(Number(a.balance_api ?? 0), cur)}</span>
},
},
{
key: 'actions',
header: '',
sortable: false,
className: 'w-32 text-right',
cell: (a) => {
const provider = providerById.get(a.providerId)
+29 -5
View File
@@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { PlusIcon, Trash2Icon, ArrowDownUpIcon } from 'lucide-react'
import { PlusIcon, Trash2Icon, ArrowDownUpIcon, CalendarIcon, UserRoundIcon, ArrowLeftRightIcon, CoinsIcon, StickyNoteIcon } from 'lucide-react'
import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot'
@@ -12,6 +12,7 @@ import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellStack } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { SectionCardsSkeleton } from '@/components/skeletons'
import { SectionCards } from '@/components/section-cards'
@@ -73,18 +74,31 @@ function BalancePage() {
const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
const columns: DataTableColumn<BalanceLedgerRow>[] = [
{ key: 'date', header: 'Дата', cell: (r) => <span className="tabular-nums">{r.date}</span> },
{
key: 'date',
header: 'Дата',
icon: CalendarIcon,
cell: (r) => <span className="tabular-nums">{r.date}</span>,
},
{
key: 'account',
header: 'Аккаунт',
icon: UserRoundIcon,
sortValue: (r) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === r.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : ''
},
cell: (r) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === r.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : '—'
if (!acc) return '—'
const providerName = providerById.get(acc.providerId)?.name ?? '—'
return dataGridCellStack(acc.name, providerName)
},
},
{
key: 'dir',
header: 'Движение',
icon: ArrowLeftRightIcon,
cell: (r) => (
<Badge variant={r.direction === 'credit' ? 'default' : 'destructive'}>
<ArrowDownUpIcon data-icon="inline-start" />
@@ -95,16 +109,26 @@ function BalancePage() {
{
key: 'amount',
header: 'Сумма',
icon: CoinsIcon,
headerClassName: 'text-right',
className: 'text-right',
sortValue: (r) => Number(r.amount),
cell: (r) => (
<span className={`tabular-nums ${r.direction === 'credit' ? '' : 'text-destructive'}`}>
<span className={`tabular-nums font-medium ${r.direction === 'credit' ? '' : 'text-destructive'}`}>
{r.direction === 'credit' ? '+' : ''}{formatCurrency(Number(r.amount), r.currency ?? 'RUB')}
</span>
),
},
{ key: 'note', header: 'Заметка', cell: (r) => <span className="text-muted-foreground">{r.note || '—'}</span> },
{
key: 'note',
header: 'Заметка',
icon: StickyNoteIcon,
cell: (r) => <span className="text-muted-foreground">{r.note || '—'}</span>,
},
{
key: 'actions',
header: '',
sortable: false,
className: 'w-16 text-right',
cell: (r) => (
<ConfirmDialog
+104 -74
View File
@@ -1,21 +1,34 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { ServerIcon, AlertTriangleIcon, WalletIcon, TrendingUpIcon } from 'lucide-react'
import type { ColumnDef } from '@tanstack/react-table'
import {
ServerIcon,
AlertTriangleIcon,
WalletIcon,
TrendingUpIcon,
HashIcon,
ExternalLinkIcon,
GlobeIcon,
FolderKanbanIcon,
CircleDotIcon,
CoinsIcon,
} from 'lucide-react'
import { snapshotQueryOptions, ratesQueryOptions } from '@/queries/snapshot'
import { PageShell } from '@/components/page-shell'
import { PageHeader } from '@/components/page-header'
import { SectionCards } from '@/components/section-cards'
import { QueryState } from '@/components/query-state'
import { DataGridCard } from '@/components/data-grid-card'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellStack } from '@/components/data-grid-cells'
import { SectionCardsSkeleton } from '@/components/skeletons'
import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge'
import { computeInventoryHealth } from '@/lib/inventory-health'
import { formatInBaseCurrency, normalizeRatesPayload } from '@/lib/format'
import { vpsStatusLabel } from '@/lib/format'
import { formatInBaseCurrency, normalizeRatesPayload, vpsStatusLabel } from '@/lib/format'
import type { Vps } from '@/types/entities'
export const Route = createFileRoute('/_auth/dashboard')({
loader: ({ context: { queryClient } }) =>
@@ -23,6 +36,8 @@ export const Route = createFileRoute('/_auth/dashboard')({
component: DashboardPage,
})
type InventoryIssue = { key: string; title: string; count: number; to: string; hint?: string }
function DashboardPage() {
const navigate = useNavigate()
const { data: snapshot, isLoading, isError, error, refetch } = useQuery(snapshotQueryOptions())
@@ -56,6 +71,88 @@ function DashboardPage() {
0,
)
const issueColumns: DataTableColumn<InventoryIssue>[] = [
{
key: 'title',
header: 'Проблема',
icon: AlertTriangleIcon,
cell: (row) => (
<div className="flex items-center gap-2">
<AlertTriangleIcon className="size-4 text-destructive" />
{dataGridCellStack(row.title, row.hint)}
</div>
),
},
{
key: 'count',
header: 'Кол-во',
icon: HashIcon,
headerClassName: 'text-right',
className: 'text-right',
cell: (row) => <span className="tabular-nums font-medium">{row.count}</span>,
},
{
key: 'action',
header: 'Действие',
icon: ExternalLinkIcon,
sortable: false,
cell: (row) => (
<Button variant="outline" size="sm" onClick={() => navigate({ to: row.to })}>
Открыть
</Button>
),
},
]
const vpsColumns: DataTableColumn<Vps>[] = [
{
key: 'ip',
header: 'IP / DNS',
icon: GlobeIcon,
sortValue: (v) => v.ip || v.dns || '',
cell: (v) => dataGridCellStack(v.ip || v.dns || '—', v.dns && v.ip ? v.dns : undefined),
},
{
key: 'project',
header: 'Проект',
icon: FolderKanbanIcon,
cell: (v) => <span className="text-muted-foreground">{v.project || '—'}</span>,
},
{
key: 'status',
header: 'Статус',
icon: CircleDotIcon,
cell: (v) => (
<Badge variant={v.status === 'active' ? 'default' : 'secondary'}>
{vpsStatusLabel(v.status)}
</Badge>
),
},
{
key: 'rate',
header: 'Ставка/мес',
icon: CoinsIcon,
headerClassName: 'text-right',
className: 'text-right',
sortValue: (v) =>
v.tariffType === 'daily'
? Number(v.dailyRate || 0) * 30
: Number(v.monthlyRate || 0),
cell: (v) => (
<span className="tabular-nums font-medium">
{formatInBaseCurrency(
v.tariffType === 'daily'
? Number(v.dailyRate || 0) * 30
: Number(v.monthlyRate || 0),
v.currency,
snap.settings,
ratesData,
)}
</span>
),
},
]
return (
<>
<SectionCards
@@ -95,38 +192,7 @@ function DashboardPage() {
<Badge variant="secondary">всё ок</Badge>
)
}
columns={[
{
id: 'title',
header: 'Проблема',
cell: ({ row }) => (
<div className="flex items-center gap-2">
<AlertTriangleIcon className="size-4 text-destructive" />
<div className="flex flex-col">
<span>{row.original.title}</span>
{row.original.hint ? (
<span className="text-xs text-muted-foreground">{row.original.hint}</span>
) : null}
</div>
</div>
),
},
{
id: 'count',
header: 'Кол-во',
cell: ({ row }) => <span className="text-right tabular-nums">{row.original.count}</span>,
meta: { align: 'right' },
},
{
id: 'action',
header: 'Действие',
cell: ({ row }) => (
<Button variant="outline" size="sm" onClick={() => navigate({ to: row.original.to })}>
Открыть
</Button>
),
},
] as ColumnDef<{ key: string; title: string; count: number; to: string; hint?: string }>[]}
columns={columnDefFromDataTable(issueColumns)}
data={issues}
rowId={(i) => i.key}
emptyTitle="Проблем не найдено"
@@ -136,43 +202,7 @@ function DashboardPage() {
<DataGridCard
title="Последние VPS"
description="Активные серверы"
columns={[
{
id: 'ip',
header: 'IP / DNS',
cell: ({ row }) => <span className="font-medium">{row.original.ip || row.original.dns}</span>,
},
{
id: 'project',
header: 'Проект',
cell: ({ row }) => <span className="text-muted-foreground">{row.original.project || '—'}</span>,
},
{
id: 'status',
header: 'Статус',
cell: ({ row }) => (
<Badge variant={row.original.status === 'active' ? 'default' : 'secondary'}>
{vpsStatusLabel(row.original.status)}
</Badge>
),
},
{
id: 'rate',
header: 'Ставка/мес',
cell: ({ row }) => (
<span className="text-right tabular-nums">
{formatInBaseCurrency(
row.original.tariffType === 'daily'
? Number(row.original.dailyRate || 0) * 30
: Number(row.original.monthlyRate || 0),
row.original.currency,
snap.settings,
ratesData,
)}
</span>
),
},
] as ColumnDef<typeof activeVps[number]>[]}
columns={columnDefFromDataTable(vpsColumns)}
data={activeVps.slice(0, 8)}
rowId={(v) => v.id}
pagination={false}
+32 -5
View File
@@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { PlusIcon, PencilIcon, Trash2Icon } from 'lucide-react'
import { PlusIcon, PencilIcon, Trash2Icon, CalendarIcon, UserRoundIcon, TagIcon, CoinsIcon, StickyNoteIcon } from 'lucide-react'
import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot'
@@ -11,6 +11,7 @@ import { PageHeader } from '@/components/page-header'
import { Button } from '@cfdm/ui/components/button'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellStack } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
import { ConfirmDialog } from '@/components/confirm-dialog'
@@ -81,29 +82,55 @@ function PaymentsPage() {
const providerById = snapshot ? providerByIdMap(snapshot.providers) : new Map()
const columns: DataTableColumn<Payment>[] = [
{ key: 'date', header: 'Дата', cell: (p) => <span className="tabular-nums">{p.date}</span> },
{
key: 'date',
header: 'Дата',
icon: CalendarIcon,
cell: (p) => <span className="tabular-nums">{p.date}</span>,
},
{
key: 'account',
header: 'Аккаунт',
icon: UserRoundIcon,
sortValue: (p) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === p.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : ''
},
cell: (p) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === p.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : '—'
if (!acc) return '—'
const providerName = providerById.get(acc.providerId)?.name ?? '—'
return dataGridCellStack(acc.name, providerName)
},
},
{
key: 'type',
header: 'Тип',
icon: TagIcon,
cell: (p) => <span>{paymentTypeLabel(p.type)}</span>,
},
{
key: 'amount',
header: 'Сумма',
cell: (p) => <span className="tabular-nums">{formatCurrency(p.amount, p.currency)}</span>,
icon: CoinsIcon,
headerClassName: 'text-right',
className: 'text-right',
cell: (p) => (
<span className="tabular-nums font-medium">
{formatCurrency(p.amount, p.currency)}
</span>
),
},
{
key: 'note',
header: 'Заметка',
icon: StickyNoteIcon,
cell: (p) => <span className="text-muted-foreground">{p.note || '—'}</span>,
},
{ key: 'note', header: 'Заметка', cell: (p) => <span className="text-muted-foreground">{p.note || '—'}</span> },
{
key: 'actions',
header: '',
sortable: false,
className: 'w-24 text-right',
cell: (p) => (
<div className="flex justify-end gap-1">
+24 -13
View File
@@ -1,7 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState } from 'react'
import { PlusIcon, PencilIcon, Trash2Icon, BuildingIcon } from 'lucide-react'
import { PlusIcon, PencilIcon, Trash2Icon, BuildingIcon, PlugIcon, CircleDollarSignIcon } from 'lucide-react'
import { toast } from 'sonner'
import { snapshotQueryOptions } from '@/queries/snapshot'
@@ -12,6 +12,7 @@ import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellWithIcon } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
import { ConfirmDialog } from '@/components/confirm-dialog'
@@ -86,22 +87,32 @@ function ProvidersPage() {
{
key: 'name',
header: 'Хостер',
cell: (p) => (
<div className="flex items-center gap-2">
{p.website ? (
<img src={faviconUrlFromWebsite(p.website)} alt="" className="size-4 rounded-sm" />
) : (
<BuildingIcon className="size-4 text-muted-foreground" />
)}
<span className="font-medium">{p.name}</span>
</div>
),
icon: BuildingIcon,
cell: (p) => {
const icon = p.website ? (
<img src={faviconUrlFromWebsite(p.website)} alt="" className="size-4 rounded-sm" />
) : (
<BuildingIcon />
)
return dataGridCellWithIcon(icon, <span className="font-medium">{p.name}</span>)
},
},
{
key: 'api',
header: 'API',
icon: PlugIcon,
cell: (p) => <Badge variant="outline">{p.apiType}</Badge>,
},
{
key: 'cur',
header: 'Валюта',
icon: CircleDollarSignIcon,
cell: (p) => <span className="tabular-nums">{p.baseCurrency ?? '—'}</span>,
},
{ key: 'api', header: 'API', cell: (p) => <Badge variant="outline">{p.apiType}</Badge> },
{ key: 'cur', header: 'Валюта', cell: (p) => <span className="tabular-nums">{p.baseCurrency ?? '—'}</span> },
{
key: 'actions',
header: '',
sortable: false,
className: 'w-24 text-right',
cell: (p) => (
<div className="flex justify-end gap-1">
+24 -4
View File
@@ -7,10 +7,11 @@ import { PageHeader } from '@/components/page-header'
import { Badge } from '@cfdm/ui/components/badge'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellStack } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
import { EmptyState } from '@/components/empty-state'
import { ServerCogIcon } from 'lucide-react'
import { ServerCogIcon, ServerIcon, UserRoundIcon, CpuIcon, CoinsIcon, HardDriveIcon } from 'lucide-react'
import type { ActiveTariff } from '@/types/entities'
import { providerByIdMap, accountSelectLabel } from '@/lib/billmanager'
@@ -30,19 +31,29 @@ function TariffsPage() {
{
key: 'name',
header: 'Тариф',
icon: ServerIcon,
cell: (t) => <span className="font-medium">{t.name || `#${t.pricelistId ?? t.id}`}</span>,
},
{
key: 'account',
header: 'Аккаунт',
icon: UserRoundIcon,
sortValue: (t) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === t.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : ''
},
cell: (t) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === t.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : '—'
if (!acc) return '—'
const providerName = providerById.get(acc.providerId)?.name ?? '—'
return dataGridCellStack(acc.name, providerName)
},
},
{
key: 'specs',
header: 'Ресурсы',
icon: CpuIcon,
sortValue: (t) => t.vcpu ?? 0,
cell: (t) => (
<span className="tabular-nums text-muted-foreground">
{t.vcpu ?? '—'} vCPU / {t.ramGb ?? '—'} GB / {t.diskGb ?? '—'} GB
@@ -52,9 +63,18 @@ function TariffsPage() {
{
key: 'price',
header: 'Цена/мес',
cell: (t) => <span className="tabular-nums">{formatCurrency(Number(t.monthlyRate ?? 0), t.currency ?? 'RUB')}</span>,
icon: CoinsIcon,
headerClassName: 'text-right',
className: 'text-right',
sortValue: (t) => Number(t.monthlyRate ?? 0),
cell: (t) => <span className="tabular-nums font-medium">{formatCurrency(Number(t.monthlyRate ?? 0), t.currency ?? 'RUB')}</span>,
},
{
key: 'disk',
header: 'Диск',
icon: HardDriveIcon,
cell: (t) => <Badge variant="outline">{t.diskType ?? '—'}</Badge>,
},
{ key: 'disk', header: 'Диск', cell: (t) => <Badge variant="outline">{t.diskType ?? '—'}</Badge> },
]
return (
+50 -14
View File
@@ -2,7 +2,7 @@ import { createFileRoute } from '@tanstack/react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { useState, useMemo } from 'react'
import { Controller } from 'react-hook-form'
import { PlusIcon, PencilIcon, Trash2Icon } from 'lucide-react'
import { PlusIcon, PencilIcon, Trash2Icon, GlobeIcon, UserRoundIcon, FolderKanbanIcon, CpuIcon, CircleDotIcon, CreditCardIcon, MapPinIcon } from 'lucide-react'
import { toast } from 'sonner'
import { format, parseISO, isValid } from 'date-fns'
@@ -16,6 +16,7 @@ import { Button } from '@cfdm/ui/components/button'
import { Badge } from '@cfdm/ui/components/badge'
import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card'
import type { DataTableColumn } from '@/components/data-table-card'
import { dataGridCellStack, dataGridCellWithFlag } from '@/components/data-grid-cells'
import { QueryState } from '@/components/query-state'
import { TableSkeleton } from '@/components/skeletons'
import { ConfirmDialog } from '@/components/confirm-dialog'
@@ -216,25 +217,58 @@ function VpsPage() {
{
key: 'ip',
header: 'IP / DNS',
cell: (v) => (
<div className="flex flex-col">
<span className="font-medium">{v.ip || '—'}</span>
{v.dns ? <span className="text-xs text-muted-foreground">{v.dns}</span> : null}
</div>
),
icon: GlobeIcon,
sortValue: (v) => v.ip || v.dns || '',
cell: (v) => dataGridCellStack(v.ip || '—', v.dns || undefined),
},
{
key: 'account',
header: 'Аккаунт',
icon: UserRoundIcon,
sortValue: (v) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === v.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : ''
},
cell: (v) => {
const acc = snapshot?.providerAccounts.find((a) => a.id === v.providerAccountId)
return acc ? accountSelectLabel(acc, providerById) : '—'
if (!acc) return '—'
const providerName = providerById.get(acc.providerId)?.name ?? '—'
return dataGridCellStack(acc.name, providerName)
},
},
{ key: 'project', header: 'Проект', cell: (v) => <span className="text-muted-foreground">{v.project || '—'}</span> },
{
key: 'location',
header: 'Локация',
icon: MapPinIcon,
sortValue: (v) => [v.country, v.city].filter(Boolean).join(', '),
cell: (v) => {
const country = v.country?.trim()
const city = v.city?.trim()
if (!country && !city) return <span className="text-muted-foreground"></span>
const countryEntry = country ? COUNTRY_BY_NAME_RU[country] : undefined
const flag = countryEntry
? getCountryFlagEmojiByCode(countryEntry.code)
: country
? getCountryFlagEmoji(country)
: null
const primary = country || city || '—'
const secondary = country && city ? city : undefined
return flag
? dataGridCellWithFlag(flag, primary, secondary)
: dataGridCellStack(primary, secondary)
},
},
{
key: 'project',
header: 'Проект',
icon: FolderKanbanIcon,
cell: (v) => <span className="text-muted-foreground">{v.project || '—'}</span>,
},
{
key: 'specs',
header: 'Ресурсы',
icon: CpuIcon,
sortValue: (v) => v.vcpu,
cell: (v) => (
<span className="tabular-nums text-muted-foreground">
{v.vcpu} vCPU / {v.ramGb} GB / {v.diskGb} GB
@@ -244,6 +278,7 @@ function VpsPage() {
{
key: 'status',
header: 'Статус',
icon: CircleDotIcon,
cell: (v) => (
<Badge variant={v.status === 'active' ? 'default' : v.status === 'archived' ? 'outline' : 'secondary'}>
{vpsStatusLabel(v.status)}
@@ -253,21 +288,22 @@ function VpsPage() {
{
key: 'tariff',
header: 'Тариф',
icon: CreditCardIcon,
sortValue: (v) => (v.tariffType === 'daily' ? Number(v.dailyRate || 0) * 30 : Number(v.monthlyRate || 0)),
cell: (v) => {
const provider = providerById.get(v.providerId)
const currency = effectiveVpsTariffCurrency(v, provider)
const amount = v.tariffType === 'daily' ? Number(v.dailyRate || 0) * 30 : Number(v.monthlyRate || 0)
return (
<span className="tabular-nums">
{formatInProviderCurrency(amount, currency, provider, snapshot?.settings ?? [], ratesData)}
<span className="ml-1 text-xs text-muted-foreground">{tariffTypeLabel(v.tariffType)}</span>
</span>
return dataGridCellStack(
formatInProviderCurrency(amount, currency, provider, snapshot?.settings ?? [], ratesData),
tariffTypeLabel(v.tariffType),
)
},
},
{
key: 'actions',
header: '',
sortable: false,
className: 'w-24 text-right',
cell: (v) => (
<div className="flex justify-end gap-1">