feat: refactor components to enhance UI consistency and functionality
CI / changes (push) Successful in 12s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m6s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m27s

Updated multiple components to improve user interface consistency by replacing traditional badge implementations with the new CategoryBadge and DataGridPrimaryCell components. Enhanced the StatusBadge component to support additional status variants and integrated it across various grids, including AccessApiKeysGrid, DashboardRecentJobsGrid, and OperationsJobsGrid. This refactor streamlines the presentation of data and improves the overall user experience across the application.
This commit is contained in:
Denozordec
2026-07-09 13:21:09 +07:00
parent 642db1a83a
commit d0bd4d661d
21 changed files with 302 additions and 187 deletions
@@ -4,8 +4,9 @@ import { useMemo } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { Badge } from '@/components/reui/badge'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
@@ -33,16 +34,14 @@ export function AccessApiKeysGrid({
{
accessorKey: 'name',
header: ({ column }) => <DataGridColumnHeader column={column} title="Имя" />,
cell: ({ row }) => <span className="font-medium">{row.original.name}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.name} accent="primary" />,
meta: { headerTitle: 'Имя' },
},
{
accessorKey: 'role',
header: ({ column }) => <DataGridColumnHeader column={column} title="Роль" />,
cell: ({ row }) => (
<Badge variant="outline" className="font-mono text-xs">
{row.original.role}
</Badge>
<CategoryBadge className="font-mono text-xs">{row.original.role}</CategoryBadge>
),
meta: { headerTitle: 'Роль' },
},
@@ -71,9 +70,7 @@ export function AccessApiKeysGrid({
accessorFn: (row) => row.expires_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Истекает" />,
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{formatApiKeyDate(row.original.expires_at)}
</span>
<DataGridMutedCell>{formatApiKeyDate(row.original.expires_at)}</DataGridMutedCell>
),
meta: { headerTitle: 'Истекает' },
},
@@ -82,9 +79,7 @@ export function AccessApiKeysGrid({
accessorFn: (row) => row.last_used_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Последнее использование" />,
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{formatApiKeyDate(row.original.last_used_at)}
</span>
<DataGridMutedCell>{formatApiKeyDate(row.original.last_used_at)}</DataGridMutedCell>
),
meta: { headerTitle: 'Последнее использование' },
},
@@ -0,0 +1,50 @@
import type { ComponentProps, ReactNode } from 'react'
import { Badge } from '@/components/reui/badge'
type BadgeVariant = NonNullable<ComponentProps<typeof Badge>['variant']>
const TONE_VARIANT: Record<string, BadgeVariant> = {
neutral: 'outline',
info: 'info-light',
warning: 'warning-light',
success: 'success-light',
}
export function ModeBadge({
enabled,
onLabel = 'включён',
offLabel = 'выключен',
className,
}: {
enabled: boolean
onLabel?: string
offLabel?: string
className?: string
}) {
return enabled ? (
<Badge variant="success-light" size="sm" radius="full" className={className}>
{onLabel}
</Badge>
) : (
<Badge variant="secondary" size="sm" radius="full" className={className}>
{offLabel}
</Badge>
)
}
export function CategoryBadge({
children,
tone = 'neutral',
className,
}: {
children: ReactNode
tone?: keyof typeof TONE_VARIANT
className?: string
}) {
return (
<Badge variant={TONE_VARIANT[tone]} size="sm" radius="full" className={className}>
{children}
</Badge>
)
}
@@ -1,22 +1,14 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { DATA_GRID_DENSE_LAYOUT } from '@/lib/data-grid-defaults'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { JobRow } from '@/types/api'
function StatusText({ status }: { status: string }) {
const cls =
status === 'succeeded'
? 'text-success'
: status === 'failed' || status === 'cancelled'
? 'text-destructive'
: 'text-muted-foreground'
return <span className={`text-xs font-medium ${cls}`}>{status}</span>
}
export function DashboardRecentJobsGrid({
jobs,
nameById,
@@ -34,21 +26,22 @@ export function DashboardRecentJobsGrid({
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => (
<div className="min-w-0">
<div className="truncate font-mono text-xs text-muted-foreground">{row.original.kind}</div>
{row.original.meta?.module_id ? (
<div className="truncate text-xs">
{nameById.get(String(row.original.meta.module_id)) ?? ''}
</div>
) : null}
</div>
<DataGridPrimaryCell
title={row.original.kind}
accent="mono"
subtitle={
row.original.meta?.module_id
? (nameById.get(String(row.original.meta.module_id)) ?? undefined)
: undefined
}
/>
),
meta: { headerTitle: 'Вид' },
},
{
accessorKey: 'status',
header: ({ column }) => <DataGridColumnHeader column={column} title="Статус" />,
cell: ({ row }) => <StatusText status={row.original.status} />,
cell: ({ row }) => <StatusBadge status={row.original.status} />,
meta: { headerTitle: 'Статус' },
},
],
@@ -1,6 +1,7 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { DATA_GRID_DENSE_LAYOUT } from '@/lib/data-grid-defaults'
@@ -23,9 +24,7 @@ export function DashboardRecentRevisionsGrid({
accessorFn: (row) => row.id,
header: ({ column }) => <DataGridColumnHeader column={column} title="ID" />,
cell: ({ row }) => (
<span className="truncate font-mono text-xs text-muted-foreground">
{row.original.id.slice(0, 10)}
</span>
<DataGridPrimaryCell title={`${row.original.id.slice(0, 10)}`} accent="mono" />
),
meta: { headerTitle: 'ID' },
},
@@ -33,9 +32,9 @@ export function DashboardRecentRevisionsGrid({
accessorKey: 'created_at',
header: ({ column }) => <DataGridColumnHeader column={column} title="Создана" />,
cell: ({ row }) => (
<span className="text-xs text-muted-foreground">
<DataGridMutedCell>
{new Date(row.original.created_at).toLocaleString('ru-RU')}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Создана' },
},
@@ -0,0 +1,44 @@
import type { ReactNode } from 'react'
import { cn } from '@evobgp/ui/lib/utils'
const ACCENT_CLASS = {
primary: 'font-medium text-primary',
default: 'font-medium text-foreground',
mono: 'font-mono text-sm text-primary',
} as const
export function DataGridPrimaryCell({
title,
subtitle,
accent = 'default',
className,
}: {
title: ReactNode
subtitle?: ReactNode
accent?: keyof typeof ACCENT_CLASS
className?: string
}) {
return (
<div className={cn('flex min-w-0 flex-col gap-0.5', className)}>
<span className={cn('truncate', ACCENT_CLASS[accent])}>{title}</span>
{subtitle ? (
<span className="truncate text-xs text-muted-foreground">{subtitle}</span>
) : null}
</div>
)
}
export function DataGridMutedCell({
children,
className,
}: {
children: ReactNode
className?: string
}) {
return (
<span className={cn('whitespace-nowrap text-xs text-muted-foreground', className)}>
{children}
</span>
)
}
@@ -1,8 +1,8 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { Badge } from '@evobgp/ui/components/badge'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
@@ -20,20 +20,22 @@ export function DirectoriesCommunitiesGrid({
{
accessorKey: 'title',
header: ({ column }) => <DataGridColumnHeader column={column} title="Название" />,
cell: ({ row }) => <span className="font-medium">{row.original.title}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.title} accent="primary" />,
meta: { headerTitle: 'Название' },
},
{
accessorKey: 'community',
header: ({ column }) => <DataGridColumnHeader column={column} title="Значение" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.community}</span>,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.community} accent="mono" />
),
meta: { headerTitle: 'Значение' },
},
{
id: 'type',
enableSorting: false,
header: 'Тип',
cell: () => <Badge variant="outline">community</Badge>,
cell: () => <CategoryBadge>community</CategoryBadge>,
meta: { headerTitle: 'Тип' },
},
],
@@ -1,8 +1,8 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { Badge } from '@evobgp/ui/components/badge'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
@@ -21,20 +21,26 @@ export function DirectoriesDohGrid({
id: 'name',
accessorFn: (row) => row.name ?? row.url,
header: ({ column }) => <DataGridColumnHeader column={column} title="Название" />,
cell: ({ row }) => <span className="font-medium">{row.original.name ?? row.original.url}</span>,
cell: ({ row }) => (
<DataGridPrimaryCell
title={row.original.name ?? row.original.url}
subtitle={row.original.name ? row.original.url : undefined}
accent="primary"
/>
),
meta: { headerTitle: 'Название' },
},
{
accessorKey: 'url',
header: ({ column }) => <DataGridColumnHeader column={column} title="URL" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.url}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.url} accent="mono" />,
meta: { headerTitle: 'URL' },
},
{
id: 'default',
enableSorting: false,
header: 'По умолчанию',
cell: () => <Badge variant="outline"></Badge>,
cell: () => <CategoryBadge></CategoryBadge>,
meta: { headerTitle: 'По умолчанию' },
},
],
@@ -3,6 +3,7 @@ import { useMemo } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { StatusBadge } from '@/components/status-badge'
@@ -42,12 +43,11 @@ export function FirewallClientsGrid({
accessorKey: 'name',
header: ({ column }) => <DataGridColumnHeader column={column} title="Имя" />,
cell: ({ row }) => (
<div>
<div className="font-medium">{row.original.name}</div>
<div className="text-muted-foreground text-xs">
{row.original.hostname || row.original.token_prefix}
</div>
</div>
<DataGridPrimaryCell
title={row.original.name}
subtitle={row.original.hostname || row.original.token_prefix}
accent="primary"
/>
),
meta: { headerTitle: 'Имя' },
},
@@ -62,7 +62,7 @@ export function FirewallClientsGrid({
accessorFn: (row) => row.last_seen_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Last seen" />,
cell: ({ row }) => (
<span className="text-xs">{row.original.last_seen_at?.slice(0, 19) ?? '—'}</span>
<DataGridMutedCell>{row.original.last_seen_at?.slice(0, 19) ?? '—'}</DataGridMutedCell>
),
sortingFn: (a, b) => {
const av = a.original.last_seen_at ?? ''
@@ -4,6 +4,8 @@ import { useMemo } from 'react'
import { Button } from '@evobgp/ui/components/button'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { formatDateTime } from '@/lib/modules/display'
@@ -68,7 +70,7 @@ export function ModuleEntriesGrid({
<DataGridColumnHeader column={column as never} title="FQDN" />
),
cell: ({ row }: { row: { original: DomainEntry } }) => (
<span className="font-mono text-sm">{row.original.fqdn}</span>
<DataGridPrimaryCell title={row.original.fqdn} accent="mono" />
),
},
{
@@ -102,7 +104,7 @@ export function ModuleEntriesGrid({
<DataGridColumnHeader column={column as never} title="Префикс (CIDR)" />
),
cell: ({ row }: { row: { original: IpRangeEntry } }) => (
<span className="font-mono text-sm">{row.original.prefix}</span>
<DataGridPrimaryCell title={row.original.prefix} accent="mono" />
),
},
{
@@ -136,14 +138,14 @@ export function ModuleEntriesGrid({
<DataGridColumnHeader column={column as never} title="URL" />
),
cell: ({ row }: { row: { original: CdnSource } }) => (
<span className="max-w-xs truncate font-mono text-xs">{row.original.url}</span>
<DataGridPrimaryCell title={row.original.url} accent="mono" className="max-w-xs" />
),
},
{
accessorKey: 'source_kind',
header: 'Тип',
cell: ({ row }: { row: { original: CdnSource } }) => (
<span className="text-sm">{row.original.source_kind}</span>
<CategoryBadge>{row.original.source_kind}</CategoryBadge>
),
},
{
@@ -162,9 +164,7 @@ export function ModuleEntriesGrid({
<DataGridColumnHeader column={column as never} title="Обновлено" />
),
cell: ({ row }: { row: { original: CdnSource } }) => (
<span className="text-xs text-muted-foreground">
{formatDateTime(row.original.last_refreshed_at)}
</span>
<DataGridMutedCell>{formatDateTime(row.original.last_refreshed_at)}</DataGridMutedCell>
),
},
{
@@ -3,10 +3,10 @@ import { useNavigate } from '@tanstack/react-router'
import { Boxes } from 'lucide-react'
import { useMemo } from 'react'
import { CategoryBadge, ModeBadge } from '@/components/category-badge'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { Badge } from '@/components/reui/badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { TruncatedText } from '@/components/truncated-text'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { ModuleRow } from '@/types/api'
@@ -26,8 +26,8 @@ export function ModulesListGrid({
header: ({ column }) => <DataGridColumnHeader column={column} title="Название" />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<Boxes className="size-4 text-muted-foreground" />
<TruncatedText className="max-w-[280px] font-medium">{row.original.name}</TruncatedText>
<Boxes className="size-4 shrink-0 text-muted-foreground" />
<DataGridPrimaryCell title={row.original.name} accent="primary" className="max-w-[280px]" />
</div>
),
meta: { headerTitle: 'Название' },
@@ -35,7 +35,7 @@ export function ModulesListGrid({
{
accessorKey: 'type',
header: ({ column }) => <DataGridColumnHeader column={column} title="Тип" />,
cell: ({ row }) => <Badge variant="outline">{row.original.type}</Badge>,
cell: ({ row }) => <CategoryBadge>{row.original.type}</CategoryBadge>,
meta: { headerTitle: 'Тип' },
},
{
@@ -50,12 +50,7 @@ export function ModulesListGrid({
id: 'enabled',
accessorFn: (row) => (row.enabled ? 'enabled' : 'disabled'),
header: ({ column }) => <DataGridColumnHeader column={column} title="Состояние" />,
cell: ({ row }) =>
row.original.enabled ? (
<Badge variant="success">включён</Badge>
) : (
<Badge variant="secondary">выключен</Badge>
),
cell: ({ row }) => <ModeBadge enabled={row.original.enabled} />,
meta: { headerTitle: 'Состояние' },
},
{
@@ -63,11 +58,11 @@ export function ModulesListGrid({
accessorFn: (row) => row.last_refreshed_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Обновлено" />,
cell: ({ row }) => (
<span className="text-xs text-muted-foreground">
<DataGridMutedCell>
{row.original.last_refreshed_at
? new Date(row.original.last_refreshed_at).toLocaleString('ru-RU')
: '—'}
</span>
</DataGridMutedCell>
),
sortingFn: (a, b) => {
const av = a.original.last_refreshed_at ?? ''
@@ -2,9 +2,9 @@ import { ColumnDef } from '@tanstack/react-table'
import { Database, HardDrive, HeartPulse, ListTodo, ShieldCheck } from 'lucide-react'
import { useMemo } from 'react'
import { Badge } from '@evobgp/ui/components/badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { ReadyStatus } from '@/queries/monitoring'
@@ -20,9 +20,8 @@ interface ReadyCheckRow {
label: string
subtitle?: string
icon: typeof Database
ok: boolean
status: string
statusLabel: string
variant: 'default' | 'destructive' | 'secondary'
}
export function MonitoringReadyGrid({
@@ -40,18 +39,16 @@ export function MonitoringReadyGrid({
label: 'Liveness',
subtitle: '/v1/health',
icon: HeartPulse,
ok: health?.ok === true,
status: health?.ok ? 'ok' : 'error',
statusLabel: health?.ok ? 'OK' : 'Ошибка',
variant: health?.ok ? 'default' : 'destructive',
},
{
id: 'readiness',
label: 'Readiness',
subtitle: '/v1/ready',
icon: ShieldCheck,
ok: ready.status === 'ok',
status: ready.status === 'ok' ? 'ok' : 'warning',
statusLabel: ready.status ?? '—',
variant: ready.status === 'ok' ? 'default' : 'secondary',
},
]
for (const key of Object.keys(checks)) {
@@ -61,9 +58,8 @@ export function MonitoringReadyGrid({
id: key,
label: key,
icon: READY_CHECK_ICONS[key] ?? ListTodo,
ok,
status: ok ? 'ok' : 'error',
statusLabel: ok ? 'OK' : 'Ошибка',
variant: ok ? 'default' : 'destructive',
})
}
return rows
@@ -79,12 +75,10 @@ export function MonitoringReadyGrid({
return (
<div className="flex items-center gap-2">
<Icon className="size-4 shrink-0 text-muted-foreground" />
<div>
<p className="text-sm font-medium">{row.original.label}</p>
{row.original.subtitle ? (
<p className="text-xs text-muted-foreground">{row.original.subtitle}</p>
) : null}
</div>
<DataGridPrimaryCell
title={row.original.label}
subtitle={row.original.subtitle}
/>
</div>
)
},
@@ -95,7 +89,7 @@ export function MonitoringReadyGrid({
enableSorting: false,
header: 'Статус',
cell: ({ row }) => (
<Badge variant={row.original.variant}>{row.original.statusLabel}</Badge>
<StatusBadge status={row.original.status} label={row.original.statusLabel} />
),
meta: { headerTitle: 'Статус' },
},
@@ -1,12 +1,13 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { Badge } from '@/components/reui/badge'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { PeerRow } from '@/types/api'
import { StatusBadge } from '@/components/status-badge'
export function NetworkPeersGrid({
items,
@@ -22,14 +23,20 @@ export function NetworkPeersGrid({
accessorFn: (row) => row.name ?? row.neighbor,
header: ({ column }) => <DataGridColumnHeader column={column} title="Имя" />,
cell: ({ row }) => (
<span className="font-medium">{row.original.name ?? row.original.neighbor}</span>
<DataGridPrimaryCell
title={row.original.name ?? row.original.neighbor}
subtitle={row.original.name ? row.original.neighbor : undefined}
accent="primary"
/>
),
meta: { headerTitle: 'Имя' },
},
{
accessorKey: 'neighbor',
header: ({ column }) => <DataGridColumnHeader column={column} title="Neighbor" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.neighbor}</span>,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.neighbor} accent="mono" />
),
meta: { headerTitle: 'Neighbor' },
},
{
@@ -47,9 +54,7 @@ export function NetworkPeersGrid({
<div className="flex items-center gap-1">
<StatusBadge status={row.original.session_state} />
{row.original.session_mismatch ? (
<Badge variant="warning" className="ml-1">
mismatch
</Badge>
<CategoryBadge tone="warning">mismatch</CategoryBadge>
) : null}
</div>
),
@@ -1,6 +1,8 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { CategoryBadge } from '@/components/category-badge'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { Badge } from '@/components/reui/badge'
@@ -20,13 +22,15 @@ export function NetworkSpeakersGrid({
{
accessorKey: 'endpoint',
header: ({ column }) => <DataGridColumnHeader column={column} title="Endpoint" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.endpoint}</span>,
cell: ({ row }) => (
<DataGridPrimaryCell title={row.original.endpoint} accent="mono" />
),
meta: { headerTitle: 'Endpoint' },
},
{
accessorKey: 'role',
header: ({ column }) => <DataGridColumnHeader column={column} title="Роль" />,
cell: ({ row }) => <Badge variant="outline">{row.original.role}</Badge>,
cell: ({ row }) => <CategoryBadge>{row.original.role}</CategoryBadge>,
meta: { headerTitle: 'Роль' },
},
{
@@ -37,7 +41,7 @@ export function NetworkSpeakersGrid({
const live = row.original.live
if (live?.agent_ok === true) return <StatusBadge status="ok" label="online" />
if (live?.agent_ok === false) return <StatusBadge status="error" label="offline" />
return <Badge variant="outline"></Badge>
return <Badge variant="outline" size="sm" radius="full"></Badge>
},
meta: { headerTitle: 'Agent' },
},
@@ -5,23 +5,15 @@ import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import { apiMutate } from '@/lib/api-client'
import type { JobRow } from '@/types/api'
import type { QueryClient } from '@tanstack/react-query'
function StatusBadgeColored({ status }: { status: string }) {
const cls =
status === 'succeeded'
? 'text-success'
: status === 'failed' || status === 'cancelled'
? 'text-destructive'
: 'text-info'
return <span className={`text-sm font-medium ${cls}`}>{status}</span>
}
export function OperationsJobsGrid({
items,
nameById,
@@ -48,22 +40,29 @@ export function OperationsJobsGrid({
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => (
<div className="flex flex-col gap-0.5">
<span className="font-medium">{row.original.kind}</span>
{row.original.meta?.module_id ? (
<span className="text-xs text-muted-foreground">
{nameById.get(String(row.original.meta.module_id)) ??
String(row.original.meta.module_id)}
</span>
) : null}
</div>
<DataGridPrimaryCell
title={row.original.kind}
accent="mono"
subtitle={
row.original.meta?.module_id
? (nameById.get(String(row.original.meta.module_id)) ??
String(row.original.meta.module_id))
: undefined
}
/>
),
meta: { headerTitle: 'Вид' },
},
{
accessorKey: 'status',
header: ({ column }) => <DataGridColumnHeader column={column} title="Статус" />,
cell: ({ row }) => <StatusBadgeColored status={row.original.status} />,
cell: ({ row }) => {
const finished = row.original.finished_at
const hint = finished
? new Date(finished).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
: undefined
return <StatusBadge status={row.original.status} hint={hint} />
},
meta: { headerTitle: 'Статус' },
},
{
@@ -71,11 +70,11 @@ export function OperationsJobsGrid({
accessorFn: (row) => row.created_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Создана" />,
cell: ({ row }) => (
<span className="whitespace-nowrap text-xs text-muted-foreground">
<DataGridMutedCell>
{row.original.created_at
? new Date(row.original.created_at).toLocaleString('ru-RU')
: '—'}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Создана' },
},
@@ -84,11 +83,11 @@ export function OperationsJobsGrid({
accessorFn: (row) => row.finished_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Завершена" />,
cell: ({ row }) => (
<span className="whitespace-nowrap text-xs text-muted-foreground">
<DataGridMutedCell>
{row.original.finished_at
? new Date(row.original.finished_at).toLocaleString('ru-RU')
: '—'}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Завершена' },
},
@@ -6,6 +6,7 @@ import { toast } from 'sonner'
import { Button } from '@evobgp/ui/components/button'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { ConfirmDialog } from '@/components/confirm-dialog'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
@@ -40,7 +41,7 @@ export function OperationsRevisionsGrid({
accessorFn: (row) => row.id,
header: ({ column }) => <DataGridColumnHeader column={column} title="ID" />,
cell: ({ row }) => (
<span className="font-mono text-xs">{row.original.id.slice(0, 12)}</span>
<DataGridPrimaryCell title={`${row.original.id.slice(0, 12)}`} accent="mono" />
),
meta: { headerTitle: 'ID' },
},
@@ -48,9 +49,9 @@ export function OperationsRevisionsGrid({
accessorKey: 'created_at',
header: ({ column }) => <DataGridColumnHeader column={column} title="Создана" />,
cell: ({ row }) => (
<span className="text-xs text-muted-foreground">
<DataGridMutedCell>
{new Date(row.original.created_at).toLocaleString('ru-RU')}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Создана' },
},
@@ -1,9 +1,9 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { Badge } from '@evobgp/ui/components/badge'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
import type { JobRow } from '@/types/api'
@@ -20,25 +20,19 @@ export function ScheduleJobsGrid({
{
accessorKey: 'kind',
header: ({ column }) => <DataGridColumnHeader column={column} title="Вид" />,
cell: ({ row }) => <span className="font-medium">{row.original.kind}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.kind} accent="mono" />,
meta: { headerTitle: 'Вид' },
},
{
accessorKey: 'status',
header: ({ column }) => <DataGridColumnHeader column={column} title="Статус" />,
cell: ({ row }) => (
<Badge
variant={
row.original.status === 'succeeded'
? 'default'
: row.original.status === 'failed'
? 'destructive'
: 'secondary'
}
>
{row.original.status}
</Badge>
),
cell: ({ row }) => {
const finished = row.original.finished_at
const hint = finished
? new Date(finished).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })
: undefined
return <StatusBadge status={row.original.status} hint={hint} />
},
meta: { headerTitle: 'Статус' },
},
{
@@ -46,11 +40,11 @@ export function ScheduleJobsGrid({
accessorFn: (row) => row.created_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Создана" />,
cell: ({ row }) => (
<span className="whitespace-nowrap text-xs text-muted-foreground">
<DataGridMutedCell>
{row.original.created_at
? new Date(row.original.created_at).toLocaleString('ru-RU')
: '—'}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Создана' },
},
@@ -59,11 +53,11 @@ export function ScheduleJobsGrid({
accessorFn: (row) => row.finished_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Завершена" />,
cell: ({ row }) => (
<span className="whitespace-nowrap text-xs text-muted-foreground">
<DataGridMutedCell>
{row.original.finished_at
? new Date(row.original.finished_at).toLocaleString('ru-RU')
: '—'}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Завершена' },
},
@@ -2,8 +2,8 @@ import { ColumnDef } from '@tanstack/react-table'
import { RefreshCw } from 'lucide-react'
import { useMemo } from 'react'
import { Badge } from '@evobgp/ui/components/badge'
import { CategoryBadge, ModeBadge } from '@/components/category-badge'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { LoadingButton } from '@/components/loading-button'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
@@ -26,13 +26,13 @@ export function ScheduleModulesGrid({
{
accessorKey: 'name',
header: ({ column }) => <DataGridColumnHeader column={column} title="Модуль" />,
cell: ({ row }) => <span className="font-medium">{row.original.name}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.name} accent="primary" />,
meta: { headerTitle: 'Модуль' },
},
{
accessorKey: 'type',
header: ({ column }) => <DataGridColumnHeader column={column} title="Тип" />,
cell: ({ row }) => <Badge variant="outline">{row.original.type}</Badge>,
cell: ({ row }) => <CategoryBadge>{row.original.type}</CategoryBadge>,
meta: { headerTitle: 'Тип' },
},
{
@@ -52,11 +52,11 @@ export function ScheduleModulesGrid({
accessorFn: (row) => row.last_refreshed_at ?? '',
header: ({ column }) => <DataGridColumnHeader column={column} title="Обновлено" />,
cell: ({ row }) => (
<span className="whitespace-nowrap text-xs text-muted-foreground">
<DataGridMutedCell>
{row.original.last_refreshed_at
? new Date(row.original.last_refreshed_at).toLocaleString('ru-RU')
: '—'}
</span>
</DataGridMutedCell>
),
meta: { headerTitle: 'Обновлено' },
},
@@ -64,12 +64,9 @@ export function ScheduleModulesGrid({
id: 'enabled',
accessorFn: (row) => (row.enabled ? 'on' : 'off'),
header: ({ column }) => <DataGridColumnHeader column={column} title="Статус" />,
cell: ({ row }) =>
row.original.enabled ? (
<Badge variant="default">Вкл</Badge>
) : (
<Badge variant="secondary">Выкл</Badge>
),
cell: ({ row }) => (
<ModeBadge enabled={row.original.enabled} onLabel="Вкл" offLabel="Выкл" />
),
meta: { headerTitle: 'Статус' },
},
{
@@ -1,6 +1,7 @@
import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { useClientDataGrid } from '@/hooks/use-client-data-grid'
@@ -23,13 +24,13 @@ export function SettingsKvGrid({
{
accessorKey: 'key',
header: ({ column }) => <DataGridColumnHeader column={column} title="Ключ" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.key}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.key} accent="mono" />,
meta: { headerTitle: 'Ключ' },
},
{
accessorKey: 'value',
header: ({ column }) => <DataGridColumnHeader column={column} title="Значение" />,
cell: ({ row }) => <span className="font-mono text-xs">{row.original.value}</span>,
cell: ({ row }) => <DataGridPrimaryCell title={row.original.value} accent="mono" />,
meta: { headerTitle: 'Значение' },
},
],
+60 -24
View File
@@ -1,35 +1,71 @@
import type { ComponentProps } from 'react'
import { cn } from '@evobgp/ui/lib/utils'
import { Badge } from '@/components/reui/badge'
type BadgeVariant = NonNullable<ComponentProps<typeof Badge>['variant']>
const STATUS_VARIANT: Record<string, BadgeVariant> = {
active: 'success',
ok: 'success',
paid: 'success',
established: 'success',
succeeded: 'success',
healthy: 'success',
paused: 'secondary',
disabled: 'secondary',
active: 'success-light',
ok: 'success-light',
paid: 'success-light',
established: 'success-light',
succeeded: 'success-light',
healthy: 'success-light',
approved: 'success-light',
accept: 'success-light',
paused: 'invert-light',
disabled: 'invert-light',
archived: 'outline',
error: 'destructive',
failed: 'destructive',
running: 'info',
queued: 'info',
overdue: 'warning',
stale: 'warning',
warning: 'warning',
mismatch: 'warning',
pending: 'warning',
approved: 'success',
revoked: 'destructive',
block: 'destructive',
accept: 'success',
error: 'destructive-light',
failed: 'destructive-light',
revoked: 'destructive-light',
block: 'destructive-light',
cancelled: 'destructive-light',
running: 'info-light',
queued: 'info-light',
overdue: 'warning-light',
stale: 'warning-light',
warning: 'warning-light',
mismatch: 'warning-light',
pending: 'warning-light',
}
export function StatusBadge({ status, label }: { status: string; label?: string }) {
const variant = STATUS_VARIANT[status.toLowerCase()] ?? 'outline'
return <Badge variant={variant}>{label ?? status}</Badge>
const DOT_COLOR: Record<string, string> = {
'success-light': 'bg-success',
'info-light': 'bg-info',
'warning-light': 'bg-warning',
'destructive-light': 'bg-destructive',
'invert-light': 'bg-muted-foreground',
outline: 'bg-muted-foreground',
}
export function jobStatusVariant(status: string): BadgeVariant {
return STATUS_VARIANT[status.toLowerCase()] ?? 'outline'
}
export function StatusBadge({
status,
label,
hint,
className,
}: {
status: string
label?: string
hint?: string
className?: string
}) {
const variant = jobStatusVariant(status)
const dotColor = DOT_COLOR[variant] ?? 'bg-muted-foreground'
return (
<div className={cn('flex flex-col gap-0.5', className)}>
<Badge variant={variant} size="sm" radius="full" className="gap-1.5">
<span className={cn('size-1.5 shrink-0 rounded-full', dotColor)} aria-hidden />
{label ?? status}
</Badge>
{hint ? <span className="text-xs text-muted-foreground">{hint}</span> : null}
</div>
)
}
+2 -2
View File
@@ -3,11 +3,11 @@ import { useQuery } from '@tanstack/react-query'
import { Activity, AlertTriangle, Bird, Database, HeartPulse, Info, ListTodo, RefreshCw } from 'lucide-react'
import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert'
import { Badge } from '@evobgp/ui/components/badge'
import { Button } from '@evobgp/ui/components/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card'
import { Separator } from '@evobgp/ui/components/separator'
import { BadgeTabs, TabsContent } from '@/components/badge-tabs'
import { StatusBadge } from '@/components/status-badge'
import {
DashboardOperationsFlowCard,
MonitoringHealthCard,
@@ -199,7 +199,7 @@ function MonitoringComponent() {
<li key={job.job_id} className="rounded-lg border px-3 py-2 text-sm">
<div className="flex items-start justify-between gap-2">
<p className="font-medium">{job.kind}</p>
<Badge variant="destructive">{job.status}</Badge>
<StatusBadge status={job.status} />
</div>
{job.error ? (
<p className="mt-1 text-xs text-muted-foreground">
File diff suppressed because one or more lines are too long