From f66d68d1c7aaa65e3b161967298fee33e4ad8a93 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Jul 2026 12:48:52 +0700 Subject: [PATCH] feat: refactor data grid components to utilize DataGridSection for enhanced functionality Updated multiple components to replace DataGridShell with DataGridSection, integrating search functionality and improved data handling. This change enhances user experience by providing a consistent interface across various grids, including AccessApiKeysGrid, DashboardRecentJobsGrid, and others. Additionally, introduced global filtering capabilities to streamline data retrieval and presentation, ensuring a more efficient user interaction with the data grids. --- .../access/access-api-keys-grid.tsx | 18 +- apps/web/src/components/badge-tabs.tsx | 62 +++++++ .../dashboard/dashboard-recent-jobs-grid.tsx | 22 ++- .../dashboard-recent-revisions-grid.tsx | 17 +- apps/web/src/components/data-grid-shell.tsx | 34 ++++ apps/web/src/components/data-grid-toolbar.tsx | 73 ++++++++ .../directories-communities-grid.tsx | 17 +- .../directories/directories-doh-grid.tsx | 17 +- .../components/examples/c-input-group-37.tsx | 107 +++++++++++ apps/web/src/components/examples/c-tabs-6.tsx | 46 +++++ apps/web/src/components/examples/c-tabs-7.tsx | 61 ++++++ .../firewall/firewall-clients-grid.tsx | 18 +- .../firewall/firewall-rules-grid.tsx | 18 +- .../modules/module-entries-grid.tsx | 22 ++- .../components/modules/modules-list-grid.tsx | 17 +- .../monitoring/monitoring-ready-grid.tsx | 20 +- .../components/network/network-peers-card.tsx | 68 +++++++ .../components/network/network-peers-grid.tsx | 20 +- .../network/network-speakers-card.tsx | 62 +++++++ .../network/network-speakers-grid.tsx | 18 +- .../components/network/peer-form-dialog.tsx | 175 ++++++++++++++++++ .../network/speaker-form-dialog.tsx | 169 +++++++++++++++++ .../operations/operations-jobs-grid.tsx | 22 ++- .../operations/operations-revisions-grid.tsx | 17 +- apps/web/src/components/reui/badge.tsx | 22 ++- .../schedule/schedule-jobs-grid.tsx | 17 +- .../schedule/schedule-modules-grid.tsx | 17 +- .../components/settings/settings-kv-grid.tsx | 20 +- apps/web/src/hooks/use-client-data-grid.ts | 61 ++++++ apps/web/src/lib/data-grid-defaults.ts | 21 ++- apps/web/src/queries/network.ts | 101 +++++++++- apps/web/src/routes/_auth/directories.tsx | 22 +-- apps/web/src/routes/_auth/firewall.tsx | 31 ++-- apps/web/src/routes/_auth/monitoring.tsx | 30 +-- apps/web/src/routes/_auth/network.tsx | 95 ++++------ apps/web/src/routes/_auth/operations.tsx | 32 ++-- apps/web/src/routes/_auth/schedule.tsx | 25 ++- apps/web/src/routes/_auth/tenant-settings.tsx | 38 ++-- apps/web/tsconfig.tsbuildinfo | 2 +- packages/ui/src/components/checkbox.tsx | 2 + packages/ui/src/components/tabs.tsx | 11 +- 41 files changed, 1392 insertions(+), 275 deletions(-) create mode 100644 apps/web/src/components/badge-tabs.tsx create mode 100644 apps/web/src/components/data-grid-toolbar.tsx create mode 100644 apps/web/src/components/examples/c-input-group-37.tsx create mode 100644 apps/web/src/components/examples/c-tabs-6.tsx create mode 100644 apps/web/src/components/examples/c-tabs-7.tsx create mode 100644 apps/web/src/components/network/network-peers-card.tsx create mode 100644 apps/web/src/components/network/network-speakers-card.tsx create mode 100644 apps/web/src/components/network/peer-form-dialog.tsx create mode 100644 apps/web/src/components/network/speaker-form-dialog.tsx create mode 100644 apps/web/src/hooks/use-client-data-grid.ts 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 5452c7d..aefb235 100644 --- a/apps/web/src/components/access/access-api-keys-grid.tsx +++ b/apps/web/src/components/access/access-api-keys-grid.tsx @@ -1,16 +1,16 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { RefreshCw, Trash2 } from 'lucide-react' import { useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { DataGridShell } from '@/components/data-grid-shell' +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' import { formatApiKeyDate } from '@/lib/access/api-key-labels' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { ApiKey } from '@/types/api' export function AccessApiKeysGrid({ @@ -140,19 +140,23 @@ export function AccessApiKeysGrid({ [onRevoke, onRotate, revokePending, rotatePending], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => + `${row.name} ${row.role} ${row.prefix} ${row.revoked_at ? 'отозван' : 'активен'}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/badge-tabs.tsx b/apps/web/src/components/badge-tabs.tsx new file mode 100644 index 0000000..5463bd0 --- /dev/null +++ b/apps/web/src/components/badge-tabs.tsx @@ -0,0 +1,62 @@ +import type { ComponentProps, ReactNode } from 'react' + +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' + +import { Badge } from '@/components/reui/badge' + +export type BadgeTabItem = { + value: string + label: string + count?: number + badgeVariant?: ComponentProps['variant'] + icon?: ReactNode +} + +interface BadgeTabsProps { + items: BadgeTabItem[] + value?: string + defaultValue?: string + onValueChange?: (value: string) => void + children: ReactNode + className?: string + listClassName?: string + contentClassName?: string +} + +/** Underline tabs with optional badge counts (ReUI c-tabs-7 pattern). */ +export function BadgeTabs({ + items, + value, + defaultValue, + onValueChange, + children, + className, + listClassName, + contentClassName, +}: BadgeTabsProps) { + return ( + + + {items.map((item) => ( + + {item.icon} + {item.label} + {item.count !== undefined ? ( + + {item.count} + + ) : null} + + ))} + +
{children}
+
+ ) +} + +export { TabsContent } 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 e81a241..4f0099c 100644 --- a/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-recent-jobs-grid.tsx @@ -1,9 +1,10 @@ -import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' -import { DataGridShell } from '@/components/data-grid-shell' +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' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { JobRow } from '@/types/api' function StatusText({ status }: { status: string }) { @@ -54,21 +55,30 @@ export function DashboardRecentJobsGrid({ [nameById], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data, columns, - getCoreRowModel: getCoreRowModel(), + getSearchText: (row) => { + const moduleName = row.meta?.module_id + ? (nameById.get(String(row.meta.module_id)) ?? '') + : '' + return `${row.kind} ${row.status} ${moduleName}` + }, getRowId: (row) => row.job_id, + pageSize: 8, }) return ( - ) } 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 ec494f7..ea6e0e6 100644 --- a/apps/web/src/components/dashboard/dashboard-recent-revisions-grid.tsx +++ b/apps/web/src/components/dashboard/dashboard-recent-revisions-grid.tsx @@ -1,9 +1,10 @@ -import { ColumnDef, getCoreRowModel, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' -import { DataGridShell } from '@/components/data-grid-shell' +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' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { RevisionRow } from '@/types/api' export function DashboardRecentRevisionsGrid({ @@ -42,21 +43,25 @@ export function DashboardRecentRevisionsGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data, columns, - getCoreRowModel: getCoreRowModel(), + getSearchText: (row) => row.id, getRowId: (row) => row.id, + pageSize: 8, }) return ( - ) } diff --git a/apps/web/src/components/data-grid-shell.tsx b/apps/web/src/components/data-grid-shell.tsx index 8afb73c..390d597 100644 --- a/apps/web/src/components/data-grid-shell.tsx +++ b/apps/web/src/components/data-grid-shell.tsx @@ -3,6 +3,7 @@ import type { Table } from '@tanstack/react-table' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' +import { DataGridToolbar } from '@/components/data-grid-toolbar' import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination' import { DataGrid, DataGridContainer } from '@/components/reui/data-grid/data-grid' import { DataGridTable } from '@/components/reui/data-grid/data-grid-table' @@ -75,3 +76,36 @@ export function DataGridCard({ title, description, actions, children, className ) } + +interface DataGridSectionProps extends DataGridShellProps { + searchValue: string + onSearchChange: (value: string) => void + searchPlaceholder?: string + toolbarFilters?: ReactNode + toolbarActions?: ReactNode + beforeGrid?: ReactNode +} + +export function DataGridSection({ + searchValue, + onSearchChange, + searchPlaceholder, + toolbarFilters, + toolbarActions, + beforeGrid, + ...shellProps +}: DataGridSectionProps) { + return ( + <> + + {beforeGrid} + + + ) +} diff --git a/apps/web/src/components/data-grid-toolbar.tsx b/apps/web/src/components/data-grid-toolbar.tsx new file mode 100644 index 0000000..e734b2f --- /dev/null +++ b/apps/web/src/components/data-grid-toolbar.tsx @@ -0,0 +1,73 @@ +import type { ReactNode } from 'react' + +import { Field } from '@evobgp/ui/components/field' +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from '@evobgp/ui/components/input-group' +import { ListFilterIcon, SearchIcon, XIcon } from 'lucide-react' + +interface DataGridToolbarProps { + searchValue: string + onSearchChange: (value: string) => void + searchPlaceholder?: string + filters?: ReactNode + actions?: ReactNode + className?: string +} + +/** Search row for data grids (ReUI c-input-group-37 pattern). */ +export function DataGridToolbar({ + searchValue, + onSearchChange, + searchPlaceholder = 'Поиск…', + filters, + actions, + className, +}: DataGridToolbarProps) { + return ( +
+ + + + + onSearchChange(event.target.value)} + aria-label={searchPlaceholder} + /> + + {searchValue.length > 0 ? ( + onSearchChange('')} + > + + ) : null} + {filters ? ( + filters + ) : ( + + )} + + + + {actions ?
{actions}
: null} +
+ ) +} diff --git a/apps/web/src/components/directories/directories-communities-grid.tsx b/apps/web/src/components/directories/directories-communities-grid.tsx index 94d3cd1..3e44b6a 100644 --- a/apps/web/src/components/directories/directories-communities-grid.tsx +++ b/apps/web/src/components/directories/directories-communities-grid.tsx @@ -1,11 +1,11 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' import { Badge } from '@evobgp/ui/components/badge' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { BgpCommunity } from '@/types/api' export function DirectoriesCommunitiesGrid({ @@ -40,19 +40,22 @@ export function DirectoriesCommunitiesGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => `${row.title} ${row.community}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/directories/directories-doh-grid.tsx b/apps/web/src/components/directories/directories-doh-grid.tsx index 13e7d7d..8398aa8 100644 --- a/apps/web/src/components/directories/directories-doh-grid.tsx +++ b/apps/web/src/components/directories/directories-doh-grid.tsx @@ -1,11 +1,11 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' import { Badge } from '@evobgp/ui/components/badge' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { DohProfile } from '@/types/api' export function DirectoriesDohGrid({ @@ -41,19 +41,22 @@ export function DirectoriesDohGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => `${row.name ?? ''} ${row.url}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/examples/c-input-group-37.tsx b/apps/web/src/components/examples/c-input-group-37.tsx new file mode 100644 index 0000000..1471604 --- /dev/null +++ b/apps/web/src/components/examples/c-input-group-37.tsx @@ -0,0 +1,107 @@ +"use client" + +import { useState } from "react" + +import { Checkbox } from "@evobgp/ui/components/checkbox" +import { Field } from "@evobgp/ui/components/field" +import { + InputGroup, + InputGroupAddon, + InputGroupButton, + InputGroupInput, +} from "@evobgp/ui/components/input-group" +import { Label } from "@evobgp/ui/components/label" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@evobgp/ui/components/popover" +import { SearchIcon, XIcon, ListFilterIcon } from "lucide-react" + +const statuses = ["Pending", "Shipped", "Cancelled"] as const + +type Status = (typeof statuses)[number] + +function toggleStatus(values: Status[], value: Status) { + return values.includes(value) + ? values.filter((item) => item !== value) + : [...values, value] +} + +export function Pattern() { + const [searchQuery, setSearchQuery] = useState("") + const [selectedStatuses, setSelectedStatuses] = useState([]) + + return ( + + + + + + setSearchQuery(event.target.value)} + /> + + + {searchQuery.length > 0 ? ( + setSearchQuery("")} + > + + ) : null} + + + + } + > + + +
+ {statuses.map((status) => ( +
+ + setSelectedStatuses((previous) => + toggleStatus(previous, status) + ) + } + /> + +
+ ))} +
+
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/examples/c-tabs-6.tsx b/apps/web/src/components/examples/c-tabs-6.tsx new file mode 100644 index 0000000..5da2fe8 --- /dev/null +++ b/apps/web/src/components/examples/c-tabs-6.tsx @@ -0,0 +1,46 @@ +import { Card, CardContent } from "@evobgp/ui/components/card" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@evobgp/ui/components/tabs" +import { LayoutDashboardIcon, BarChart3Icon, SettingsIcon } from "lucide-react" + +export function Pattern() { + return ( +
+ + + + + Overview + + + + Analytics + + + + Settings + + + + + Overview dashboard content goes here. + + + + + Analytics charts and metrics. + + + + + Application settings and preferences. + + + +
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/examples/c-tabs-7.tsx b/apps/web/src/components/examples/c-tabs-7.tsx new file mode 100644 index 0000000..fd63379 --- /dev/null +++ b/apps/web/src/components/examples/c-tabs-7.tsx @@ -0,0 +1,61 @@ +import { Badge } from "@/components/reui/badge" + +import { Card, CardContent } from "@evobgp/ui/components/card" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@evobgp/ui/components/tabs" + +export function Pattern() { + return ( +
+ + + + Inbox + + 12 + + + + Drafts + + 3 + + + + Sent + + + Spam + + 24 + + + + + + 12 unread messages in your inbox. + + + + + 3 drafts waiting to be sent. + + + + + All sent messages appear here. + + + + + 24 spam messages detected. + + + +
+ ) +} \ No newline at end of file diff --git a/apps/web/src/components/firewall/firewall-clients-grid.tsx b/apps/web/src/components/firewall/firewall-clients-grid.tsx index ec03e07..64338f2 100644 --- a/apps/web/src/components/firewall/firewall-clients-grid.tsx +++ b/apps/web/src/components/firewall/firewall-clients-grid.tsx @@ -1,13 +1,13 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { ConfirmDialog } from '@/components/confirm-dialog' import { StatusBadge } from '@/components/status-badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { FirewallClient } from '@/types/api' function formatPacketCount(value?: number | null): string | null { @@ -173,19 +173,23 @@ export function FirewallClientsGrid({ [approvePending, onApprove, onReject, rejectPending], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: clients, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => + `${row.name} ${row.hostname ?? ''} ${row.token_prefix} ${row.status ?? ''}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/firewall/firewall-rules-grid.tsx b/apps/web/src/components/firewall/firewall-rules-grid.tsx index 55d5f22..d6bd550 100644 --- a/apps/web/src/components/firewall/firewall-rules-grid.tsx +++ b/apps/web/src/components/firewall/firewall-rules-grid.tsx @@ -1,13 +1,13 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { ConfirmDialog } from '@/components/confirm-dialog' import { StatusBadge } from '@/components/status-badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import { communityLabel } from '@/lib/modules/helpers' import type { BgpCommunity, FirewallRule } from '@/types/api' @@ -100,19 +100,23 @@ export function FirewallRulesGrid({ [communities, deletePending, onDelete], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: rules, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => + `${row.priority} ${row.action} ${row.comment ?? ''} ${communityLabel(row.community_id, communities)}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/modules/module-entries-grid.tsx b/apps/web/src/components/modules/module-entries-grid.tsx index df47c51..e0993e5 100644 --- a/apps/web/src/components/modules/module-entries-grid.tsx +++ b/apps/web/src/components/modules/module-entries-grid.tsx @@ -1,14 +1,14 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { Pencil, Trash2 } from 'lucide-react' import { useMemo } from 'react' import { Button } from '@evobgp/ui/components/button' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { formatDateTime } from '@/lib/modules/display' import { communityLabel } from '@/lib/modules/helpers' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { AsEntry, BgpCommunity, @@ -231,19 +231,27 @@ export function ModuleEntriesGrid({ type RowType = DomainEntry | IpRangeEntry | CdnSource | AsEntry const data = rows as unknown as RowType[] - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data, columns: columns as ColumnDef[], - ...createClientDataGridOptions(), + getSearchText: (row) => { + const r = row as Record + return Object.values(r) + .filter((v) => typeof v === 'string' || typeof v === 'number') + .join(' ') + }, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/modules/modules-list-grid.tsx b/apps/web/src/components/modules/modules-list-grid.tsx index 3cd3f97..bff6c44 100644 --- a/apps/web/src/components/modules/modules-list-grid.tsx +++ b/apps/web/src/components/modules/modules-list-grid.tsx @@ -1,13 +1,13 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useNavigate } from '@tanstack/react-router' import { Boxes } from 'lucide-react' import { useMemo } from 'react' -import { DataGridShell } from '@/components/data-grid-shell' +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 { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { ModuleRow } from '@/types/api' export function ModulesListGrid({ @@ -80,19 +80,22 @@ export function ModulesListGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => `${row.name} ${row.type} ${row.enabled ? 'включён' : 'выключен'}`, getRowId: (row) => row.id, }) return ( - void navigate({ to: '/modules/$moduleId', params: { moduleId: row.id } })} /> ) diff --git a/apps/web/src/components/monitoring/monitoring-ready-grid.tsx b/apps/web/src/components/monitoring/monitoring-ready-grid.tsx index f6caa85..536f056 100644 --- a/apps/web/src/components/monitoring/monitoring-ready-grid.tsx +++ b/apps/web/src/components/monitoring/monitoring-ready-grid.tsx @@ -1,12 +1,12 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +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 { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { ReadyStatus } from '@/queries/monitoring' const READY_CHECK_ICONS: Record = { @@ -103,21 +103,23 @@ export function MonitoringReadyGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data, columns, - ...createClientDataGridOptions({ - initialState: { pagination: { pageSize: 20 } }, - }), + getSearchText: (row) => `${row.label} ${row.subtitle ?? ''} ${row.statusLabel}`, getRowId: (row) => row.id, + pageSize: 20, }) return ( - ) } diff --git a/apps/web/src/components/network/network-peers-card.tsx b/apps/web/src/components/network/network-peers-card.tsx new file mode 100644 index 0000000..57c976b --- /dev/null +++ b/apps/web/src/components/network/network-peers-card.tsx @@ -0,0 +1,68 @@ +import { useState } from 'react' +import { Plus } from 'lucide-react' + +import { Button } from '@evobgp/ui/components/button' + +import { DataGridCard } from '@/components/data-grid-shell' +import { NetworkPeersGrid } from '@/components/network/network-peers-grid' +import { PeerFormDialog } from '@/components/network/peer-form-dialog' +import { QueryState } from '@/components/query-state' +import { TableSkeleton } from '@/components/skeletons' +import type { PeerRow, SpeakerRow } from '@/types/api' + +interface NetworkPeersCardProps { + items: PeerRow[] + speakers: SpeakerRow[] + isLoading: boolean + isError: boolean + error: unknown + onRetry: () => void +} + +export function NetworkPeersCard({ + items, + speakers, + isLoading, + isError, + error, + onRetry, +}: NetworkPeersCardProps) { + const [dialogOpen, setDialogOpen] = useState(false) + + return ( + <> + setDialogOpen(true)}> + + Добавить пира + + } + > + } + onRetry={onRetry} + > + {(data) => ( + 0} /> + )} + + + + + + ) +} diff --git a/apps/web/src/components/network/network-peers-grid.tsx b/apps/web/src/components/network/network-peers-grid.tsx index c21cf05..20f354c 100644 --- a/apps/web/src/components/network/network-peers-grid.tsx +++ b/apps/web/src/components/network/network-peers-grid.tsx @@ -1,12 +1,12 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' -import { DataGridShell } from '@/components/data-grid-shell' -import { StatusBadge } from '@/components/status-badge' +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 { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { PeerRow } from '@/types/api' +import { StatusBadge } from '@/components/status-badge' export function NetworkPeersGrid({ items, @@ -59,19 +59,23 @@ export function NetworkPeersGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => + `${row.name ?? ''} ${row.neighbor} ${row.remote_asn ?? ''} ${row.session_state ?? ''}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/network/network-speakers-card.tsx b/apps/web/src/components/network/network-speakers-card.tsx new file mode 100644 index 0000000..288ad3f --- /dev/null +++ b/apps/web/src/components/network/network-speakers-card.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react' +import { Plus } from 'lucide-react' + +import { Button } from '@evobgp/ui/components/button' + +import { DataGridCard } from '@/components/data-grid-shell' +import { NetworkSpeakersGrid } from '@/components/network/network-speakers-grid' +import { SpeakerFormDialog } from '@/components/network/speaker-form-dialog' +import { QueryState } from '@/components/query-state' +import { TableSkeleton } from '@/components/skeletons' +import type { SpeakerRow } from '@/types/api' + +interface NetworkSpeakersCardProps { + items: SpeakerRow[] + isLoading: boolean + isError: boolean + error: unknown + onRetry: () => void +} + +export function NetworkSpeakersCard({ + items, + isLoading, + isError, + error, + onRetry, +}: NetworkSpeakersCardProps) { + const [dialogOpen, setDialogOpen] = useState(false) + + return ( + <> + setDialogOpen(true)}> + + Добавить спикера + + } + > + } + onRetry={onRetry} + > + {(data) => ( + 0} /> + )} + + + + + + ) +} diff --git a/apps/web/src/components/network/network-speakers-grid.tsx b/apps/web/src/components/network/network-speakers-grid.tsx index 2c855dc..027046d 100644 --- a/apps/web/src/components/network/network-speakers-grid.tsx +++ b/apps/web/src/components/network/network-speakers-grid.tsx @@ -1,11 +1,11 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' import { Badge } from '@/components/reui/badge' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { SpeakerRow } from '@/types/api' export function NetworkSpeakersGrid({ @@ -60,19 +60,23 @@ export function NetworkSpeakersGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => + `${row.endpoint} ${row.role} ${row.agent_domain ?? ''} ${row.node_ipv4 ?? ''}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/network/peer-form-dialog.tsx b/apps/web/src/components/network/peer-form-dialog.tsx new file mode 100644 index 0000000..f89ff2c --- /dev/null +++ b/apps/web/src/components/network/peer-form-dialog.tsx @@ -0,0 +1,175 @@ +import { useEffect, useState } from 'react' +import { toast } from 'sonner' + +import { Button } from '@evobgp/ui/components/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@evobgp/ui/components/dialog' +import { Input } from '@evobgp/ui/components/input' +import { Label } from '@evobgp/ui/components/label' +import { Checkbox } from '@evobgp/ui/components/checkbox' + +import { LoadingButton } from '@/components/loading-button' +import { SelectField } from '@/components/select-field' +import { useCreatePeerMutation, useUpdatePeerMutation } from '@/queries/network' +import type { BgpPeerCreate, PeerRow, SpeakerRow } from '@/types/api' + +interface PeerFormDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + speakers: SpeakerRow[] + editTarget?: PeerRow | null +} + +function speakerLabel(s: SpeakerRow): string { + if (s.role === 'master') { + const host = s.agent_domain ?? s.endpoint + return host ? `CP · ${host}` : 'CP (master)' + } + return s.agent_domain ?? s.endpoint ?? `${s.id.slice(0, 8)}…` +} + +export function PeerFormDialog({ + open, + onOpenChange, + speakers, + editTarget = null, +}: PeerFormDialogProps) { + const createMutation = useCreatePeerMutation() + const updateMutation = useUpdatePeerMutation() + const saving = createMutation.isPending || updateMutation.isPending + + const [name, setName] = useState('') + const [neighbor, setNeighbor] = useState('') + const [remoteAsn, setRemoteAsn] = useState('') + const [bgpSpeakerId, setBgpSpeakerId] = useState(null) + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + if (!open) return + if (editTarget) { + setName(editTarget.name ?? '') + setNeighbor(editTarget.neighbor) + setRemoteAsn(String(editTarget.remote_asn ?? '')) + setBgpSpeakerId(editTarget.bgp_speaker_id ?? null) + setEnabled(editTarget.enabled !== false) + } else { + setName('') + setNeighbor('') + setRemoteAsn('') + setBgpSpeakerId(null) + setEnabled(true) + } + }, [editTarget, open]) + + const speakerItems = [ + { value: '', label: 'Все спикеры' }, + ...speakers.map((s) => ({ value: s.id, label: speakerLabel(s) })), + ] + + async function save() { + if (!neighbor.trim()) { + toast.error('Укажите адрес соседа') + return + } + const asn = Number(remoteAsn) + if (!asn || asn <= 0) { + toast.error('Remote ASN должен быть больше 0') + return + } + const body: BgpPeerCreate = { + name: name.trim() || undefined, + neighbor: neighbor.trim(), + remote_asn: asn, + bgp_speaker_id: bgpSpeakerId || null, + enabled, + } + try { + if (editTarget) { + await updateMutation.mutateAsync({ id: editTarget.id, body }) + } else { + await createMutation.mutateAsync(body) + } + onOpenChange(false) + } catch { + // toast in mutation + } + } + + return ( + + + + {editTarget ? 'Редактировать пира' : 'Новый пир'} + BGP-сосед для установки сессии + +
+
+ + setName(e.target.value)} + /> +
+
+ + setNeighbor(e.target.value)} + required + /> +
+
+ + setRemoteAsn(e.target.value)} + required + /> +
+ setBgpSpeakerId(v || null)} + placeholder="Все спикеры" + /> +
+
+ +

+ Выключенный пир не попадает в конфиг BIRD до следующей ревизии. +

+
+ setEnabled(v === true)} + /> +
+
+ + + + {editTarget ? 'Сохранить' : 'Создать'} + + +
+
+ ) +} diff --git a/apps/web/src/components/network/speaker-form-dialog.tsx b/apps/web/src/components/network/speaker-form-dialog.tsx new file mode 100644 index 0000000..1db5689 --- /dev/null +++ b/apps/web/src/components/network/speaker-form-dialog.tsx @@ -0,0 +1,169 @@ +import { useEffect, useState } from 'react' +import { toast } from 'sonner' + +import { Button } from '@evobgp/ui/components/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@evobgp/ui/components/dialog' +import { Input } from '@evobgp/ui/components/input' +import { Label } from '@evobgp/ui/components/label' + +import { LoadingButton } from '@/components/loading-button' +import { SelectField } from '@/components/select-field' +import { useCreateSpeakerMutation } from '@/queries/network' +import type { BgpSpeakerCreate } from '@/types/api' + +interface SpeakerFormDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +function parseIpv4FromEndpoint(ep: string): string { + try { + const u = ep.includes('://') ? new URL(ep) : new URL(`https://${ep}`) + const host = u.hostname + if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return host + } catch { + /* ignore */ + } + return '' +} + +function buildMetaJson(agentDomain: string, nodeIpv4: string, bgpSource: string): string { + const meta: Record = {} + if (agentDomain.trim()) meta.agent_domain = agentDomain.trim() + if (nodeIpv4.trim()) meta.node_ipv4 = nodeIpv4.trim() + if (bgpSource.trim()) meta.bird_bgp_source_ipv4 = bgpSource.trim() + return JSON.stringify(meta) +} + +export function SpeakerFormDialog({ open, onOpenChange }: SpeakerFormDialogProps) { + const createMutation = useCreateSpeakerMutation() + + const [endpoint, setEndpoint] = useState('') + const [role, setRole] = useState('replica') + const [agentDomain, setAgentDomain] = useState('') + const [nodeIpv4, setNodeIpv4] = useState('') + const [bgpSourceIpv4, setBgpSourceIpv4] = useState('') + const [bgpSourceManual, setBgpSourceManual] = useState(false) + + useEffect(() => { + if (!open) return + setEndpoint('') + setRole('replica') + setAgentDomain('') + setNodeIpv4('') + setBgpSourceIpv4('') + setBgpSourceManual(false) + }, [open]) + + function handleEndpointChange(value: string) { + setEndpoint(value) + const ip = parseIpv4FromEndpoint(value) + if (ip && !nodeIpv4) { + handleNodeIpv4Change(ip) + } + } + + function handleNodeIpv4Change(value: string) { + setNodeIpv4(value) + if (!bgpSourceManual) { + setBgpSourceIpv4(value) + } + } + + async function save() { + const ep = + endpoint.trim() || (agentDomain.trim() ? `https://${agentDomain.trim()}` : '') + if (!ep) { + toast.error('Укажите endpoint или agent domain') + return + } + const body: BgpSpeakerCreate = { + endpoint: ep, + role: role.trim() || 'replica', + meta_json: buildMetaJson(agentDomain, nodeIpv4, bgpSourceIpv4), + } + try { + await createMutation.mutateAsync(body) + onOpenChange(false) + } catch { + // toast in mutation + } + } + + return ( + + + + Новый спикер + BIRD-агент на ноде реплики или control plane + +
+
+ + handleEndpointChange(e.target.value)} + /> +
+ setRole(v ?? 'replica')} + /> +
+ + setAgentDomain(e.target.value)} + /> +
+
+ + handleNodeIpv4Change(e.target.value)} + /> +
+
+ + { + setBgpSourceManual(true) + setBgpSourceIpv4(e.target.value) + }} + /> +
+
+ + + + Создать + + +
+
+ ) +} diff --git a/apps/web/src/components/operations/operations-jobs-grid.tsx b/apps/web/src/components/operations/operations-jobs-grid.tsx index ce9e391..4b4ab43 100644 --- a/apps/web/src/components/operations/operations-jobs-grid.tsx +++ b/apps/web/src/components/operations/operations-jobs-grid.tsx @@ -1,14 +1,14 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMutation } from '@tanstack/react-query' import { useMemo } from 'react' import { toast } from 'sonner' import { Button } from '@evobgp/ui/components/button' -import { DataGridShell } from '@/components/data-grid-shell' +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' import { apiMutate } from '@/lib/api-client' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' import type { JobRow } from '@/types/api' import type { QueryClient } from '@tanstack/react-query' @@ -113,19 +113,27 @@ export function OperationsJobsGrid({ [cancelMutation, nameById], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => { + const moduleName = row.meta?.module_id + ? (nameById.get(String(row.meta.module_id)) ?? String(row.meta.module_id)) + : '' + return `${row.kind} ${row.status} ${row.job_id} ${moduleName}` + }, getRowId: (row) => row.job_id, }) return ( - ) } diff --git a/apps/web/src/components/operations/operations-revisions-grid.tsx b/apps/web/src/components/operations/operations-revisions-grid.tsx index 7e1401e..a61113c 100644 --- a/apps/web/src/components/operations/operations-revisions-grid.tsx +++ b/apps/web/src/components/operations/operations-revisions-grid.tsx @@ -1,4 +1,4 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMutation } from '@tanstack/react-query' import { RefreshCw } from 'lucide-react' import { useMemo } from 'react' @@ -6,11 +6,11 @@ import { toast } from 'sonner' import { Button } from '@evobgp/ui/components/button' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { ConfirmDialog } from '@/components/confirm-dialog' 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 { createClientDataGridOptions } from '@/lib/data-grid-defaults' import type { RevisionRow } from '@/types/api' import type { QueryClient } from '@tanstack/react-query' @@ -87,19 +87,22 @@ export function OperationsRevisionsGrid({ [rollbackMutation], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => `${row.id} ${row.materialized_prefix_count}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/reui/badge.tsx b/apps/web/src/components/reui/badge.tsx index e94aaa0..3428eb4 100644 --- a/apps/web/src/components/reui/badge.tsx +++ b/apps/web/src/components/reui/badge.tsx @@ -5,7 +5,11 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@evobgp/ui/lib/utils" const badgeVariants = cva( - "relative inline-flex shrink-0 items-center justify-center w-fit border border-transparent font-medium whitespace-nowrap outline-none transition-shadow focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-3", + [ + "relative inline-flex shrink-0 items-center justify-center w-fit border border-transparent font-medium whitespace-nowrap outline-none transition-shadow", + "focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50", + "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=size-])]:size-3", + ], { variants: { variant: { @@ -19,19 +23,19 @@ const badgeVariants = cva( focus: "bg-focus text-focus-foreground", invert: "bg-invert text-invert-foreground", "primary-light": - "bg-primary/10 border-none text-primary dark:bg-primary/20", + "border-primary/10 bg-primary/10 text-primary dark:border-primary/25 dark:bg-primary/15 dark:text-primary", "warning-light": - "bg-warning/10 border-none text-warning-foreground dark:bg-warning/20", + "border-warning/15 bg-warning/10 text-warning-foreground dark:border-warning/25 dark:bg-warning/15 dark:text-warning", "success-light": - "bg-success/10 border-none text-success-foreground dark:bg-success/20", + "border-success/15 bg-success/10 text-success-foreground dark:border-success/25 dark:bg-success/15 dark:text-success", "info-light": - "bg-info/10 border-none text-info-foreground dark:bg-info/20", + "border-info/15 bg-info/10 text-info-foreground dark:border-info/25 dark:bg-info/15 dark:text-info", "destructive-light": - "bg-destructive/10 border-none text-destructive-foreground dark:bg-destructive/20", + "border-destructive/15 bg-destructive/10 text-destructive-foreground dark:border-destructive/25 dark:bg-destructive/15 dark:text-destructive", "invert-light": - "bg-invert/10 border-none text-foreground dark:bg-invert/20", + "border-invert/15 bg-invert/10 text-foreground dark:border-invert/45 dark:bg-invert/35 dark:text-invert-foreground", "focus-light": - "bg-focus/10 border-none text-focus-foreground dark:bg-focus/20", + "border-focus/15 bg-focus/10 text-focus-foreground dark:border-focus/25 dark:bg-focus/15 dark:text-focus", "primary-outline": "bg-background border-border text-primary dark:bg-input/30", "warning-outline": @@ -54,7 +58,7 @@ const badgeVariants = cva( lg: "px-1.5 py-0.5 text-xs h-5.5 min-w-5.5 gap-1", xl: "px-2 py-0.75 text-sm h-6 min-w-6 gap-1.5", }, - /** `default`: per-theme radius. `full`: max radius per theme (Lyra stays `rounded-none`). */ + /** `default`: active style radius. `full`: pill radius. */ radius: { default: "rounded-sm", diff --git a/apps/web/src/components/schedule/schedule-jobs-grid.tsx b/apps/web/src/components/schedule/schedule-jobs-grid.tsx index 11e1abe..3b69396 100644 --- a/apps/web/src/components/schedule/schedule-jobs-grid.tsx +++ b/apps/web/src/components/schedule/schedule-jobs-grid.tsx @@ -1,11 +1,11 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' import { Badge } from '@evobgp/ui/components/badge' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { JobRow } from '@/types/api' export function ScheduleJobsGrid({ @@ -82,19 +82,22 @@ export function ScheduleJobsGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => `${row.kind} ${row.status} ${row.error ?? ''}`, getRowId: (row) => row.job_id, }) return ( - ) } diff --git a/apps/web/src/components/schedule/schedule-modules-grid.tsx b/apps/web/src/components/schedule/schedule-modules-grid.tsx index 5f98fa0..c860b82 100644 --- a/apps/web/src/components/schedule/schedule-modules-grid.tsx +++ b/apps/web/src/components/schedule/schedule-modules-grid.tsx @@ -1,13 +1,13 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { RefreshCw } from 'lucide-react' import { useMemo } from 'react' import { Badge } from '@evobgp/ui/components/badge' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { LoadingButton } from '@/components/loading-button' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' import type { ModuleRow } from '@/types/api' export function ScheduleModulesGrid({ @@ -95,19 +95,22 @@ export function ScheduleModulesGrid({ [onRefresh, refreshing], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions(), + getSearchText: (row) => `${row.name} ${row.type} ${row.enabled ? 'вкл' : 'выкл'}`, getRowId: (row) => row.id, }) return ( - ) } diff --git a/apps/web/src/components/settings/settings-kv-grid.tsx b/apps/web/src/components/settings/settings-kv-grid.tsx index 47669af..0310314 100644 --- a/apps/web/src/components/settings/settings-kv-grid.tsx +++ b/apps/web/src/components/settings/settings-kv-grid.tsx @@ -1,9 +1,9 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef } from '@tanstack/react-table' import { useMemo } from 'react' -import { DataGridShell } from '@/components/data-grid-shell' +import { DataGridSection } from '@/components/data-grid-shell' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' -import { createClientDataGridOptions } from '@/lib/data-grid-defaults' +import { useClientDataGrid } from '@/hooks/use-client-data-grid' export interface SettingsKvRow { id: string | number @@ -36,22 +36,24 @@ export function SettingsKvGrid({ [], ) - const table = useReactTable({ + const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({ data: items, columns, - ...createClientDataGridOptions({ - initialState: { pagination: { pageSize: 25 } }, - }), + getSearchText: (row) => `${row.key} ${row.value}`, getRowId: (row) => String(row.id), + pageSize: 25, }) return ( - 10} + searchValue={globalFilter} + onSearchChange={setGlobalFilter} + searchPlaceholder="Поиск настроек…" /> ) } diff --git a/apps/web/src/hooks/use-client-data-grid.ts b/apps/web/src/hooks/use-client-data-grid.ts new file mode 100644 index 0000000..fd472b4 --- /dev/null +++ b/apps/web/src/hooks/use-client-data-grid.ts @@ -0,0 +1,61 @@ +import { useMemo, useState } from 'react' +import { + type ColumnDef, + type FilterFn, + type TableOptions, + useReactTable, +} from '@tanstack/react-table' + +import { + createClientDataGridOptions, + createTextGlobalFilter, +} from '@/lib/data-grid-defaults' + +interface UseClientDataGridOptions { + data: TData[] + columns: ColumnDef[] + getSearchText: (row: TData) => string + getRowId: (row: TData) => string + pageSize?: number + tableOptions?: Partial> + globalFilterFn?: FilterFn +} + +export function useClientDataGrid({ + data, + columns, + getSearchText, + getRowId, + pageSize = 10, + tableOptions, + globalFilterFn, +}: UseClientDataGridOptions) { + const [globalFilter, setGlobalFilter] = useState('') + + const filterFn = useMemo( + () => globalFilterFn ?? createTextGlobalFilter(getSearchText), + [getSearchText, globalFilterFn], + ) + + const table = useReactTable({ + data, + columns, + state: { globalFilter }, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: filterFn, + ...createClientDataGridOptions({ + initialState: { pagination: { pageSize } }, + ...tableOptions, + }), + getRowId, + }) + + const filteredCount = table.getFilteredRowModel().rows.length + + return { + table, + globalFilter, + setGlobalFilter, + filteredCount, + } +} diff --git a/apps/web/src/lib/data-grid-defaults.ts b/apps/web/src/lib/data-grid-defaults.ts index 57a9749..d4d1053 100644 --- a/apps/web/src/lib/data-grid-defaults.ts +++ b/apps/web/src/lib/data-grid-defaults.ts @@ -1,7 +1,9 @@ import { getCoreRowModel, + getFilteredRowModel, getPaginationRowModel, getSortedRowModel, + type FilterFn, type TableOptions, } from '@tanstack/react-table' @@ -28,14 +30,31 @@ export const DATA_GRID_DENSE_LAYOUT: NonNullable['tableLay dense: true, } +export function createTextGlobalFilter( + getSearchText: (row: TData) => string, +): FilterFn { + return (row, _columnId, filterValue) => { + const query = String(filterValue ?? '') + .toLowerCase() + .trim() + if (!query) return true + return getSearchText(row.original).toLowerCase().includes(query) + } +} + export function createClientDataGridOptions( overrides?: Partial>, ): Pick< TableOptions, - 'getCoreRowModel' | 'getSortedRowModel' | 'getPaginationRowModel' | 'initialState' + | 'getCoreRowModel' + | 'getFilteredRowModel' + | 'getSortedRowModel' + | 'getPaginationRowModel' + | 'initialState' > { return { getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 10 } }, diff --git a/apps/web/src/queries/network.ts b/apps/web/src/queries/network.ts index 19635f4..360b9d7 100644 --- a/apps/web/src/queries/network.ts +++ b/apps/web/src/queries/network.ts @@ -1,6 +1,18 @@ -import { queryOptions } from '@tanstack/react-query' -import { apiJSON } from '@/lib/api-client' -import type { BirdStatus, PeersResponse, SpeakersResponse } from '@/types/api' +import { queryOptions, useMutation, useQueryClient } from '@tanstack/react-query' +import { toast } from 'sonner' + +import { apiJSON, apiMutate } from '@/lib/api-client' +import type { + BgpPeerCreate, + BgpPeerPatch, + BgpSpeakerCreate, + BgpSpeakerPatch, + BirdStatus, + PeerRow, + PeersResponse, + SpeakerRow, + SpeakersResponse, +} from '@/types/api' export const NETWORK_AUTO_REFRESH_MS = 30_000 @@ -34,3 +46,86 @@ export function networkBirdQueryOptions() { staleTime: 15_000, }) } + +function invalidateNetwork(qc: ReturnType) { + void qc.invalidateQueries({ queryKey: networkKeys.all }) + void qc.invalidateQueries({ queryKey: ['overview'] }) +} + +export function useCreatePeerMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (body: BgpPeerCreate) => + apiMutate('/v1/peers', 'POST', body, { idempotent: false }), + onSuccess: () => { + toast.success('Пир создан') + invalidateNetwork(qc) + }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось создать пира'), + }) +} + +export function useUpdatePeerMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: ({ id, body }: { id: string; body: BgpPeerPatch }) => + apiMutate(`/v1/peers/${id}`, 'PATCH', body, { idempotent: false }), + onSuccess: () => { + toast.success('Пир обновлён') + invalidateNetwork(qc) + }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось обновить пира'), + }) +} + +export function useDeletePeerMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: string) => + apiMutate(`/v1/peers/${id}`, 'DELETE', undefined, { idempotent: false }), + onSuccess: () => { + toast.success('Пир удалён') + invalidateNetwork(qc) + }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось удалить пира'), + }) +} + +export function useCreateSpeakerMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (body: BgpSpeakerCreate) => + apiMutate('/v1/speakers', 'POST', body, { idempotent: false }), + onSuccess: () => { + toast.success('Спикер создан') + invalidateNetwork(qc) + }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось создать спикера'), + }) +} + +export function useUpdateSpeakerMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: ({ id, body }: { id: string; body: BgpSpeakerPatch }) => + apiMutate(`/v1/speakers/${id}`, 'PATCH', body, { idempotent: false }), + onSuccess: () => { + toast.success('Спикер обновлён') + invalidateNetwork(qc) + }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось обновить спикера'), + }) +} + +export function useDeleteSpeakerMutation() { + const qc = useQueryClient() + return useMutation({ + mutationFn: (id: string) => + apiMutate(`/v1/speakers/${id}`, 'DELETE', undefined, { idempotent: false }), + onSuccess: () => { + toast.success('Спикер удалён') + invalidateNetwork(qc) + }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось удалить спикера'), + }) +} diff --git a/apps/web/src/routes/_auth/directories.tsx b/apps/web/src/routes/_auth/directories.tsx index da324e5..70a9521 100644 --- a/apps/web/src/routes/_auth/directories.tsx +++ b/apps/web/src/routes/_auth/directories.tsx @@ -4,8 +4,7 @@ import { BookText, Globe, Info, RefreshCw, Tags } from 'lucide-react' import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert' import { Button } from '@evobgp/ui/components/button' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' - +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { DataGridCard } from '@/components/data-grid-shell' import { DirectoriesCommunitiesGrid } from '@/components/directories/directories-communities-grid' import { DirectoriesDohGrid } from '@/components/directories/directories-doh-grid' @@ -80,13 +79,14 @@ function DirectoriesComponent() { {loading ? : } - - - Сообщества BGP - DoH профили - - - + + - + - + ) } diff --git a/apps/web/src/routes/_auth/firewall.tsx b/apps/web/src/routes/_auth/firewall.tsx index 773ca54..646712c 100644 --- a/apps/web/src/routes/_auth/firewall.tsx +++ b/apps/web/src/routes/_auth/firewall.tsx @@ -9,8 +9,7 @@ import { Button } from '@evobgp/ui/components/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' - +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { FirewallClientsGrid } from '@/components/firewall/firewall-clients-grid' import { FirewallRulesGrid } from '@/components/firewall/firewall-rules-grid' import { PageHeader } from '@/components/page-header' @@ -185,14 +184,20 @@ function FirewallPage() { - - - Клиенты ({activeClients.length}) - Правила ({rules.length}) - Запросы ({pending.length}) - - - + 0 ? 'warning-light' : 'primary-light', + }, + ]} + > + - +
@@ -281,7 +286,7 @@ function FirewallPage() { - + - +
) } diff --git a/apps/web/src/routes/_auth/monitoring.tsx b/apps/web/src/routes/_auth/monitoring.tsx index e6265b8..cfafed0 100644 --- a/apps/web/src/routes/_auth/monitoring.tsx +++ b/apps/web/src/routes/_auth/monitoring.tsx @@ -7,8 +7,7 @@ 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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' - +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { DashboardOperationsFlowCard, MonitoringHealthCard, @@ -41,6 +40,7 @@ export const Route = createFileRoute('/_auth/monitoring')({ function MonitoringComponent() { const search = useSearch({ from: '/_auth/monitoring' }) + const navigate = Route.useNavigate() const healthQ = useQuery(monitoringHealthQueryOptions()) const readyQ = useQuery(monitoringReadyQueryOptions()) const versionQ = useQuery(monitoringVersionQueryOptions()) @@ -92,14 +92,18 @@ function MonitoringComponent() { } /> - - - Система - PostgreSQL - Файловые логи - - - + + navigate({ search: { tab: tab as 'system' | 'postgres' | 'runtime-logs' } }) + } + items={[ + { value: 'system', label: 'Система' }, + { value: 'postgres', label: 'PostgreSQL' }, + { value: 'runtime-logs', label: 'Файловые логи' }, + ]} + > + {analyticsLoading ? ( ) : ( @@ -259,7 +263,7 @@ function MonitoringComponent() {
- + PostgreSQL @@ -278,7 +282,7 @@ function MonitoringComponent() { - + Файловые логи @@ -296,7 +300,7 @@ function MonitoringComponent() { -
+ ) } diff --git a/apps/web/src/routes/_auth/network.tsx b/apps/web/src/routes/_auth/network.tsx index d809e30..9f287c5 100644 --- a/apps/web/src/routes/_auth/network.tsx +++ b/apps/web/src/routes/_auth/network.tsx @@ -4,15 +4,14 @@ import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert import { Button } from '@evobgp/ui/components/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Info, RefreshCw } from 'lucide-react' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' import { DashboardNetworkCapacityCard, NetworkOverviewAnalyticsCard, } from '@/components/analytics' -import { DataGridCard } from '@/components/data-grid-shell' -import { NetworkPeersGrid } from '@/components/network/network-peers-grid' -import { NetworkSpeakersGrid } from '@/components/network/network-speakers-grid' +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' +import { NetworkPeersCard } from '@/components/network/network-peers-card' +import { NetworkSpeakersCard } from '@/components/network/network-speakers-card' import { PageHeader } from '@/components/page-header' import { QueryState } from '@/components/query-state' import { TableSkeleton } from '@/components/skeletons' @@ -30,6 +29,7 @@ export const Route = createFileRoute('/_auth/network')({ function NetworkComponent() { const search = useSearch({ from: '/_auth/network' }) + const navigate = Route.useNavigate() const peersQ = useQuery({ ...networkPeersQueryOptions(), refetchInterval: 30_000 }) const speakersQ = useQuery({ ...networkSpeakersQueryOptions(), refetchInterval: 30_000 }) const birdQ = useQuery({ ...networkBirdQueryOptions(), refetchInterval: 30_000 }) @@ -68,15 +68,23 @@ function NetworkComponent() { - - - Обзор - Пиры ({peers.length}) - Спикеры ({speakers.length}) - Control plane - - - + + navigate({ + search: { + tab: tab as 'overview' | 'peers' | 'speakers' | 'control-plane', + }, + }) + } + items={[ + { value: 'overview', label: 'Обзор' }, + { value: 'peers', label: 'Пиры', count: peers.length }, + { value: 'speakers', label: 'Спикеры', count: speakers.length, badgeVariant: 'info-light' }, + { value: 'control-plane', label: 'Control plane' }, + ]} + > +
- - - } - onRetry={() => peersQ.refetch()} - > - {(items) => ( - - )} - - + + peersQ.refetch()} + /> - - - } - onRetry={() => speakersQ.refetch()} - > - {(items) => ( - - )} - - + + speakersQ.refetch()} + /> - + Настройки Control Plane (BIRD) @@ -165,7 +150,7 @@ function NetworkComponent() { - +
) } diff --git a/apps/web/src/routes/_auth/operations.tsx b/apps/web/src/routes/_auth/operations.tsx index 876a9e8..d4fa098 100644 --- a/apps/web/src/routes/_auth/operations.tsx +++ b/apps/web/src/routes/_auth/operations.tsx @@ -7,10 +7,9 @@ import { useState, useMemo } from 'react' import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert' import { Button } from '@evobgp/ui/components/button' import { Card, CardContent, CardHeader, CardTitle } from '@evobgp/ui/components/card' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' - -import { OperationsAnalyticsCard } from '@/components/analytics' +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { DataGridCard } from '@/components/data-grid-shell' +import { OperationsAnalyticsCard } from '@/components/analytics' import { SelectMenu } from '@/components/select-field' import { OperationsJobsGrid } from '@/components/operations/operations-jobs-grid' import { OperationsRevisionsGrid } from '@/components/operations/operations-revisions-grid' @@ -34,6 +33,7 @@ export const Route = createFileRoute('/_auth/operations')({ function OperationsComponent() { const search = useSearch({ from: '/_auth/operations' }) + const navigate = Route.useNavigate() const qc = useQueryClient() const revisionsQ = useQuery(operationsRevisionsQueryOptions()) @@ -134,14 +134,18 @@ function OperationsComponent() { )} - - - Ревизии ({revisions.length}) - Сравнение - Задачи ({jobs.length}) - - - + + navigate({ search: { tab: tab as 'revisions' | 'diff' | 'jobs' } }) + } + items={[ + { value: 'revisions', label: 'Ревизии', count: revisions.length }, + { value: 'diff', label: 'Сравнение' }, + { value: 'jobs', label: 'Задачи', count: jobs.length, badgeVariant: 'info-light' }, + ]} + > + - + - + - +
) } diff --git a/apps/web/src/routes/_auth/schedule.tsx b/apps/web/src/routes/_auth/schedule.tsx index c134158..0ad3908 100644 --- a/apps/web/src/routes/_auth/schedule.tsx +++ b/apps/web/src/routes/_auth/schedule.tsx @@ -6,8 +6,7 @@ import { useState } from 'react' import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert' import { Button } from '@evobgp/ui/components/button' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' - +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { DataGridCard } from '@/components/data-grid-shell' import { ScheduleJobsGrid } from '@/components/schedule/schedule-jobs-grid' import { ScheduleModulesGrid } from '@/components/schedule/schedule-modules-grid' @@ -138,12 +137,20 @@ function JobsTabs({ jobs, loading }: { jobs: JobRow[]; loading: boolean }) { ) return ( - - - Все ({jobs.length}) - Обновление ({refresh.length}) - С ошибкой ({failed.length}) - + 0 ? 'destructive-light' : 'primary-light', + }, + ]} + > @@ -153,6 +160,6 @@ function JobsTabs({ jobs, loading }: { jobs: JobRow[]; loading: boolean }) { - + ) } diff --git a/apps/web/src/routes/_auth/tenant-settings.tsx b/apps/web/src/routes/_auth/tenant-settings.tsx index c56bafa..dcfed00 100644 --- a/apps/web/src/routes/_auth/tenant-settings.tsx +++ b/apps/web/src/routes/_auth/tenant-settings.tsx @@ -8,11 +8,8 @@ import { Alert, AlertDescription, AlertTitle } from '@evobgp/ui/components/alert import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@evobgp/ui/components/tabs' - +import { BadgeTabs, TabsContent } from '@/components/badge-tabs' import { SelectField } from '@/components/select-field' - - import { SettingsKvGrid } from '@/components/settings/settings-kv-grid' import { PageHeader } from '@/components/page-header' import { QueryState } from '@/components/query-state' @@ -60,6 +57,7 @@ const BIRD_LABELS: Record = { function TenantSettingsComponent() { const search = useSearch({ from: '/_auth/tenant-settings' }) + const navigate = Route.useNavigate() const settingsQ = useQuery(settingsQueryOptions()) const qc = useQueryClient() @@ -114,15 +112,21 @@ function TenantSettingsComponent() { - - - BIRD - Ревизии - Файловые логи - Дополнительно - - - + + navigate({ + search: { tab: tab as 'bird' | 'revision' | 'runtime-logs' | 'additional' }, + }) + } + items={[ + { value: 'bird', label: 'BIRD' }, + { value: 'revision', label: 'Ревизии' }, + { value: 'runtime-logs', label: 'Файловые логи' }, + { value: 'additional', label: 'Дополнительно' }, + ]} + > + BIRD control plane @@ -175,7 +179,7 @@ function TenantSettingsComponent() { - + Ревизии @@ -222,7 +226,7 @@ function TenantSettingsComponent() { - + Файловые логи @@ -313,7 +317,7 @@ function TenantSettingsComponent() { - + Дополнительные параметры @@ -342,7 +346,7 @@ function TenantSettingsComponent() { - + ) } diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index 545de94..b0478ef 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/confirm-dialog.tsx","./src/components/data-grid-shell.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-select-4.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-grid.tsx","./src/components/network/network-speakers-grid.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/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/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 diff --git a/packages/ui/src/components/checkbox.tsx b/packages/ui/src/components/checkbox.tsx index ec11eef..559432b 100644 --- a/packages/ui/src/components/checkbox.tsx +++ b/packages/ui/src/components/checkbox.tsx @@ -1,3 +1,5 @@ +"use client" + import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox" import { cn } from "@evobgp/ui/lib/utils" diff --git a/packages/ui/src/components/tabs.tsx b/packages/ui/src/components/tabs.tsx index ffc96ef..32979e5 100644 --- a/packages/ui/src/components/tabs.tsx +++ b/packages/ui/src/components/tabs.tsx @@ -1,5 +1,3 @@ -"use client" - import { Tabs as TabsPrimitive } from "@base-ui/react/tabs" import { cva, type VariantProps } from "class-variance-authority" @@ -13,10 +11,9 @@ function Tabs({ return (