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 (