From d0bd4d661d73a071b7bd86920f5c6cc90e2cb2d2 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Jul 2026 13:21:09 +0700 Subject: [PATCH] feat: refactor components to enhance UI consistency and functionality 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. --- .../access/access-api-keys-grid.tsx | 17 ++-- apps/web/src/components/category-badge.tsx | 50 +++++++++++ .../dashboard/dashboard-recent-jobs-grid.tsx | 31 +++---- .../dashboard-recent-revisions-grid.tsx | 9 +- apps/web/src/components/data-grid-cell.tsx | 44 ++++++++++ .../directories-communities-grid.tsx | 12 +-- .../directories/directories-doh-grid.tsx | 16 ++-- .../firewall/firewall-clients-grid.tsx | 14 ++-- .../modules/module-entries-grid.tsx | 14 ++-- .../components/modules/modules-list-grid.tsx | 21 ++--- .../monitoring/monitoring-ready-grid.tsx | 28 +++---- .../components/network/network-peers-grid.tsx | 19 +++-- .../network/network-speakers-grid.tsx | 10 ++- .../operations/operations-jobs-grid.tsx | 47 +++++------ .../operations/operations-revisions-grid.tsx | 7 +- .../schedule/schedule-jobs-grid.tsx | 34 ++++---- .../schedule/schedule-modules-grid.tsx | 21 ++--- .../components/settings/settings-kv-grid.tsx | 5 +- apps/web/src/components/status-badge.tsx | 84 +++++++++++++------ apps/web/src/routes/_auth/monitoring.tsx | 4 +- apps/web/tsconfig.tsbuildinfo | 2 +- 21 files changed, 302 insertions(+), 187 deletions(-) create mode 100644 apps/web/src/components/category-badge.tsx create mode 100644 apps/web/src/components/data-grid-cell.tsx diff --git a/apps/web/src/components/access/access-api-keys-grid.tsx b/apps/web/src/components/access/access-api-keys-grid.tsx index aefb235..14c58aa 100644 --- a/apps/web/src/components/access/access-api-keys-grid.tsx +++ b/apps/web/src/components/access/access-api-keys-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.name}, + cell: ({ row }) => , meta: { headerTitle: 'Имя' }, }, { accessorKey: 'role', header: ({ column }) => , cell: ({ row }) => ( - - {row.original.role} - + {row.original.role} ), meta: { headerTitle: 'Роль' }, }, @@ -71,9 +70,7 @@ export function AccessApiKeysGrid({ accessorFn: (row) => row.expires_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - - {formatApiKeyDate(row.original.expires_at)} - + {formatApiKeyDate(row.original.expires_at)} ), meta: { headerTitle: 'Истекает' }, }, @@ -82,9 +79,7 @@ export function AccessApiKeysGrid({ accessorFn: (row) => row.last_used_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - - {formatApiKeyDate(row.original.last_used_at)} - + {formatApiKeyDate(row.original.last_used_at)} ), meta: { headerTitle: 'Последнее использование' }, }, diff --git a/apps/web/src/components/category-badge.tsx b/apps/web/src/components/category-badge.tsx new file mode 100644 index 0000000..5e56463 --- /dev/null +++ b/apps/web/src/components/category-badge.tsx @@ -0,0 +1,50 @@ +import type { ComponentProps, ReactNode } from 'react' + +import { Badge } from '@/components/reui/badge' + +type BadgeVariant = NonNullable['variant']> + +const TONE_VARIANT: Record = { + 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 ? ( + + {onLabel} + + ) : ( + + {offLabel} + + ) +} + +export function CategoryBadge({ + children, + tone = 'neutral', + className, +}: { + children: ReactNode + tone?: keyof typeof TONE_VARIANT + className?: string +}) { + return ( + + {children} + + ) +} diff --git a/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx b/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx index 4f0099c..12be1c4 100644 --- a/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx @@ -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 {status} -} - export function DashboardRecentJobsGrid({ jobs, nameById, @@ -34,21 +26,22 @@ export function DashboardRecentJobsGrid({ accessorKey: 'kind', header: ({ column }) => , cell: ({ row }) => ( -
-
{row.original.kind}
- {row.original.meta?.module_id ? ( -
- {nameById.get(String(row.original.meta.module_id)) ?? ''} -
- ) : null} -
+ ), meta: { headerTitle: 'Вид' }, }, { accessorKey: 'status', header: ({ column }) => , - cell: ({ row }) => , + cell: ({ row }) => , meta: { headerTitle: 'Статус' }, }, ], diff --git a/apps/web/src/components/dashboard/dashboard-recent-revisions-grid.tsx b/apps/web/src/components/dashboard/dashboard-recent-revisions-grid.tsx index ea6e0e6..b63718e 100644 --- a/apps/web/src/components/dashboard/dashboard-recent-revisions-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-recent-revisions-grid.tsx @@ -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 }) => , cell: ({ row }) => ( - - {row.original.id.slice(0, 10)}… - + ), meta: { headerTitle: 'ID' }, }, @@ -33,9 +32,9 @@ export function DashboardRecentRevisionsGrid({ accessorKey: 'created_at', header: ({ column }) => , cell: ({ row }) => ( - + {new Date(row.original.created_at).toLocaleString('ru-RU')} - + ), meta: { headerTitle: 'Создана' }, }, diff --git a/apps/web/src/components/data-grid-cell.tsx b/apps/web/src/components/data-grid-cell.tsx new file mode 100644 index 0000000..7c3ed7f --- /dev/null +++ b/apps/web/src/components/data-grid-cell.tsx @@ -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 ( +
+ {title} + {subtitle ? ( + {subtitle} + ) : null} +
+ ) +} + +export function DataGridMutedCell({ + children, + className, +}: { + children: ReactNode + className?: string +}) { + return ( + + {children} + + ) +} diff --git a/apps/web/src/components/directories/directories-communities-grid.tsx b/apps/web/src/components/directories/directories-communities-grid.tsx index 3e44b6a..d9f3dc8 100644 --- a/apps/web/src/components/directories/directories-communities-grid.tsx +++ b/apps/web/src/components/directories/directories-communities-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.title}, + cell: ({ row }) => , meta: { headerTitle: 'Название' }, }, { accessorKey: 'community', header: ({ column }) => , - cell: ({ row }) => {row.original.community}, + cell: ({ row }) => ( + + ), meta: { headerTitle: 'Значение' }, }, { id: 'type', enableSorting: false, header: 'Тип', - cell: () => community, + cell: () => community, meta: { headerTitle: 'Тип' }, }, ], diff --git a/apps/web/src/components/directories/directories-doh-grid.tsx b/apps/web/src/components/directories/directories-doh-grid.tsx index 8398aa8..e049dd2 100644 --- a/apps/web/src/components/directories/directories-doh-grid.tsx +++ b/apps/web/src/components/directories/directories-doh-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.name ?? row.original.url}, + cell: ({ row }) => ( + + ), meta: { headerTitle: 'Название' }, }, { accessorKey: 'url', header: ({ column }) => , - cell: ({ row }) => {row.original.url}, + cell: ({ row }) => , meta: { headerTitle: 'URL' }, }, { id: 'default', enableSorting: false, header: 'По умолчанию', - cell: () => , + cell: () => , meta: { headerTitle: 'По умолчанию' }, }, ], diff --git a/apps/web/src/components/firewall/firewall-clients-grid.tsx b/apps/web/src/components/firewall/firewall-clients-grid.tsx index 64338f2..8168719 100644 --- a/apps/web/src/components/firewall/firewall-clients-grid.tsx +++ b/apps/web/src/components/firewall/firewall-clients-grid.tsx @@ -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 }) => , cell: ({ row }) => ( -
-
{row.original.name}
-
- {row.original.hostname || row.original.token_prefix} -
-
+ ), meta: { headerTitle: 'Имя' }, }, @@ -62,7 +62,7 @@ export function FirewallClientsGrid({ accessorFn: (row) => row.last_seen_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - {row.original.last_seen_at?.slice(0, 19) ?? '—'} + {row.original.last_seen_at?.slice(0, 19) ?? '—'} ), sortingFn: (a, b) => { const av = a.original.last_seen_at ?? '' diff --git a/apps/web/src/components/modules/module-entries-grid.tsx b/apps/web/src/components/modules/module-entries-grid.tsx index e0993e5..ed5eaa3 100644 --- a/apps/web/src/components/modules/module-entries-grid.tsx +++ b/apps/web/src/components/modules/module-entries-grid.tsx @@ -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({ ), cell: ({ row }: { row: { original: DomainEntry } }) => ( - {row.original.fqdn} + ), }, { @@ -102,7 +104,7 @@ export function ModuleEntriesGrid({ ), cell: ({ row }: { row: { original: IpRangeEntry } }) => ( - {row.original.prefix} + ), }, { @@ -136,14 +138,14 @@ export function ModuleEntriesGrid({ ), cell: ({ row }: { row: { original: CdnSource } }) => ( - {row.original.url} + ), }, { accessorKey: 'source_kind', header: 'Тип', cell: ({ row }: { row: { original: CdnSource } }) => ( - {row.original.source_kind} + {row.original.source_kind} ), }, { @@ -162,9 +164,7 @@ export function ModuleEntriesGrid({ ), cell: ({ row }: { row: { original: CdnSource } }) => ( - - {formatDateTime(row.original.last_refreshed_at)} - + {formatDateTime(row.original.last_refreshed_at)} ), }, { diff --git a/apps/web/src/components/modules/modules-list-grid.tsx b/apps/web/src/components/modules/modules-list-grid.tsx index bff6c44..b223275 100644 --- a/apps/web/src/components/modules/modules-list-grid.tsx +++ b/apps/web/src/components/modules/modules-list-grid.tsx @@ -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 }) => , cell: ({ row }) => (
- - {row.original.name} + +
), meta: { headerTitle: 'Название' }, @@ -35,7 +35,7 @@ export function ModulesListGrid({ { accessorKey: 'type', header: ({ column }) => , - cell: ({ row }) => {row.original.type}, + cell: ({ row }) => {row.original.type}, meta: { headerTitle: 'Тип' }, }, { @@ -50,12 +50,7 @@ export function ModulesListGrid({ id: 'enabled', accessorFn: (row) => (row.enabled ? 'enabled' : 'disabled'), header: ({ column }) => , - cell: ({ row }) => - row.original.enabled ? ( - включён - ) : ( - выключен - ), + cell: ({ row }) => , meta: { headerTitle: 'Состояние' }, }, { @@ -63,11 +58,11 @@ export function ModulesListGrid({ accessorFn: (row) => row.last_refreshed_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - + {row.original.last_refreshed_at ? new Date(row.original.last_refreshed_at).toLocaleString('ru-RU') : '—'} - + ), sortingFn: (a, b) => { const av = a.original.last_refreshed_at ?? '' diff --git a/apps/web/src/components/monitoring/monitoring-ready-grid.tsx b/apps/web/src/components/monitoring/monitoring-ready-grid.tsx index 536f056..fbe7375 100644 --- a/apps/web/src/components/monitoring/monitoring-ready-grid.tsx +++ b/apps/web/src/components/monitoring/monitoring-ready-grid.tsx @@ -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 (
-
-

{row.original.label}

- {row.original.subtitle ? ( -

{row.original.subtitle}

- ) : null} -
+
) }, @@ -95,7 +89,7 @@ export function MonitoringReadyGrid({ enableSorting: false, header: 'Статус', cell: ({ row }) => ( - {row.original.statusLabel} + ), meta: { headerTitle: 'Статус' }, }, diff --git a/apps/web/src/components/network/network-peers-grid.tsx b/apps/web/src/components/network/network-peers-grid.tsx index 20f354c..af941ca 100644 --- a/apps/web/src/components/network/network-peers-grid.tsx +++ b/apps/web/src/components/network/network-peers-grid.tsx @@ -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 }) => , cell: ({ row }) => ( - {row.original.name ?? row.original.neighbor} + ), meta: { headerTitle: 'Имя' }, }, { accessorKey: 'neighbor', header: ({ column }) => , - cell: ({ row }) => {row.original.neighbor}, + cell: ({ row }) => ( + + ), meta: { headerTitle: 'Neighbor' }, }, { @@ -47,9 +54,7 @@ export function NetworkPeersGrid({
{row.original.session_mismatch ? ( - - mismatch - + mismatch ) : null}
), diff --git a/apps/web/src/components/network/network-speakers-grid.tsx b/apps/web/src/components/network/network-speakers-grid.tsx index 027046d..2797e8c 100644 --- a/apps/web/src/components/network/network-speakers-grid.tsx +++ b/apps/web/src/components/network/network-speakers-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.endpoint}, + cell: ({ row }) => ( + + ), meta: { headerTitle: 'Endpoint' }, }, { accessorKey: 'role', header: ({ column }) => , - cell: ({ row }) => {row.original.role}, + cell: ({ row }) => {row.original.role}, meta: { headerTitle: 'Роль' }, }, { @@ -37,7 +41,7 @@ export function NetworkSpeakersGrid({ const live = row.original.live if (live?.agent_ok === true) return if (live?.agent_ok === false) return - return + return }, meta: { headerTitle: 'Agent' }, }, diff --git a/apps/web/src/components/operations/operations-jobs-grid.tsx b/apps/web/src/components/operations/operations-jobs-grid.tsx index 4b4ab43..f53b2a7 100644 --- a/apps/web/src/components/operations/operations-jobs-grid.tsx +++ b/apps/web/src/components/operations/operations-jobs-grid.tsx @@ -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 {status} -} - export function OperationsJobsGrid({ items, nameById, @@ -48,22 +40,29 @@ export function OperationsJobsGrid({ accessorKey: 'kind', header: ({ column }) => , cell: ({ row }) => ( -
- {row.original.kind} - {row.original.meta?.module_id ? ( - - {nameById.get(String(row.original.meta.module_id)) ?? - String(row.original.meta.module_id)} - - ) : null} -
+ ), meta: { headerTitle: 'Вид' }, }, { accessorKey: 'status', header: ({ column }) => , - cell: ({ row }) => , + 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 + }, meta: { headerTitle: 'Статус' }, }, { @@ -71,11 +70,11 @@ export function OperationsJobsGrid({ accessorFn: (row) => row.created_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - + {row.original.created_at ? new Date(row.original.created_at).toLocaleString('ru-RU') : '—'} - + ), meta: { headerTitle: 'Создана' }, }, @@ -84,11 +83,11 @@ export function OperationsJobsGrid({ accessorFn: (row) => row.finished_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - + {row.original.finished_at ? new Date(row.original.finished_at).toLocaleString('ru-RU') : '—'} - + ), meta: { headerTitle: 'Завершена' }, }, diff --git a/apps/web/src/components/operations/operations-revisions-grid.tsx b/apps/web/src/components/operations/operations-revisions-grid.tsx index a61113c..0d20355 100644 --- a/apps/web/src/components/operations/operations-revisions-grid.tsx +++ b/apps/web/src/components/operations/operations-revisions-grid.tsx @@ -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 }) => , cell: ({ row }) => ( - {row.original.id.slice(0, 12)}… + ), meta: { headerTitle: 'ID' }, }, @@ -48,9 +49,9 @@ export function OperationsRevisionsGrid({ accessorKey: 'created_at', header: ({ column }) => , cell: ({ row }) => ( - + {new Date(row.original.created_at).toLocaleString('ru-RU')} - + ), meta: { headerTitle: 'Создана' }, }, diff --git a/apps/web/src/components/schedule/schedule-jobs-grid.tsx b/apps/web/src/components/schedule/schedule-jobs-grid.tsx index 3b69396..3fa0e2f 100644 --- a/apps/web/src/components/schedule/schedule-jobs-grid.tsx +++ b/apps/web/src/components/schedule/schedule-jobs-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.kind}, + cell: ({ row }) => , meta: { headerTitle: 'Вид' }, }, { accessorKey: 'status', header: ({ column }) => , - cell: ({ row }) => ( - - {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 + }, meta: { headerTitle: 'Статус' }, }, { @@ -46,11 +40,11 @@ export function ScheduleJobsGrid({ accessorFn: (row) => row.created_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - + {row.original.created_at ? new Date(row.original.created_at).toLocaleString('ru-RU') : '—'} - + ), meta: { headerTitle: 'Создана' }, }, @@ -59,11 +53,11 @@ export function ScheduleJobsGrid({ accessorFn: (row) => row.finished_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - + {row.original.finished_at ? new Date(row.original.finished_at).toLocaleString('ru-RU') : '—'} - + ), meta: { headerTitle: 'Завершена' }, }, diff --git a/apps/web/src/components/schedule/schedule-modules-grid.tsx b/apps/web/src/components/schedule/schedule-modules-grid.tsx index c860b82..cb81c2c 100644 --- a/apps/web/src/components/schedule/schedule-modules-grid.tsx +++ b/apps/web/src/components/schedule/schedule-modules-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.name}, + cell: ({ row }) => , meta: { headerTitle: 'Модуль' }, }, { accessorKey: 'type', header: ({ column }) => , - cell: ({ row }) => {row.original.type}, + cell: ({ row }) => {row.original.type}, meta: { headerTitle: 'Тип' }, }, { @@ -52,11 +52,11 @@ export function ScheduleModulesGrid({ accessorFn: (row) => row.last_refreshed_at ?? '', header: ({ column }) => , cell: ({ row }) => ( - + {row.original.last_refreshed_at ? new Date(row.original.last_refreshed_at).toLocaleString('ru-RU') : '—'} - + ), meta: { headerTitle: 'Обновлено' }, }, @@ -64,12 +64,9 @@ export function ScheduleModulesGrid({ id: 'enabled', accessorFn: (row) => (row.enabled ? 'on' : 'off'), header: ({ column }) => , - cell: ({ row }) => - row.original.enabled ? ( - Вкл - ) : ( - Выкл - ), + cell: ({ row }) => ( + + ), meta: { headerTitle: 'Статус' }, }, { diff --git a/apps/web/src/components/settings/settings-kv-grid.tsx b/apps/web/src/components/settings/settings-kv-grid.tsx index 0310314..1c39472 100644 --- a/apps/web/src/components/settings/settings-kv-grid.tsx +++ b/apps/web/src/components/settings/settings-kv-grid.tsx @@ -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 }) => , - cell: ({ row }) => {row.original.key}, + cell: ({ row }) => , meta: { headerTitle: 'Ключ' }, }, { accessorKey: 'value', header: ({ column }) => , - cell: ({ row }) => {row.original.value}, + cell: ({ row }) => , meta: { headerTitle: 'Значение' }, }, ], diff --git a/apps/web/src/components/status-badge.tsx b/apps/web/src/components/status-badge.tsx index 5941a46..78cdee9 100644 --- a/apps/web/src/components/status-badge.tsx +++ b/apps/web/src/components/status-badge.tsx @@ -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['variant']> const STATUS_VARIANT: Record = { - 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 {label ?? status} +const DOT_COLOR: Record = { + '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 ( +
+ + + {label ?? status} + + {hint ? {hint} : null} +
+ ) } diff --git a/apps/web/src/routes/_auth/monitoring.tsx b/apps/web/src/routes/_auth/monitoring.tsx index cfafed0..f2e0753 100644 --- a/apps/web/src/routes/_auth/monitoring.tsx +++ b/apps/web/src/routes/_auth/monitoring.tsx @@ -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() {
  • {job.kind}

    - {job.status} +
    {job.error ? (

    diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index b0478ef..5914b0e 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file