From d434eb0d945b32707c9e803e2b6008be4603869a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 9 Jul 2026 13:45:53 +0700 Subject: [PATCH] feat: enhance UI components with DataGridCard integration and improved error handling Refactored multiple components to utilize the new DataGridCard for better organization and presentation of data. Updated the FirewallPage and Monitoring components to enhance loading states and error handling using QueryState. Added success and error notifications for firewall rule creation, improving user feedback. This update streamlines the user experience and ensures a more consistent interface across the application. --- apps/web/src/components/data-grid-shell.tsx | 6 +- apps/web/src/components/data-grid-toolbar.tsx | 2 +- .../firewall/firewall-rule-create-dialog.tsx | 107 ++++++++++ apps/web/src/queries/firewall.ts | 2 + apps/web/src/routes/_auth/firewall.tsx | 191 ++++++++---------- apps/web/src/routes/_auth/monitoring.tsx | 34 ++-- apps/web/src/routes/_auth/tenant-settings.tsx | 50 +++-- apps/web/tsconfig.tsbuildinfo | 2 +- 8 files changed, 237 insertions(+), 157 deletions(-) create mode 100644 apps/web/src/components/firewall/firewall-rule-create-dialog.tsx diff --git a/apps/web/src/components/data-grid-shell.tsx b/apps/web/src/components/data-grid-shell.tsx index 390d597..835efa6 100644 --- a/apps/web/src/components/data-grid-shell.tsx +++ b/apps/web/src/components/data-grid-shell.tsx @@ -46,7 +46,11 @@ export function DataGridShell({ - {showPagination ? : null} + {showPagination ? ( +
+ +
+ ) : null} ) } diff --git a/apps/web/src/components/data-grid-toolbar.tsx b/apps/web/src/components/data-grid-toolbar.tsx index e734b2f..2c4cab6 100644 --- a/apps/web/src/components/data-grid-toolbar.tsx +++ b/apps/web/src/components/data-grid-toolbar.tsx @@ -28,7 +28,7 @@ export function DataGridToolbar({ className, }: DataGridToolbarProps) { return ( -
+
diff --git a/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx b/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx new file mode 100644 index 0000000..2cd7afe --- /dev/null +++ b/apps/web/src/components/firewall/firewall-rule-create-dialog.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from 'react' + +import { Button } from '@evobgp/ui/components/button' +import { + Dialog, + DialogContent, + 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 { CommunitySelect } from '@/components/modules/community-select' +import { SelectField } from '@/components/select-field' +import { useCreateFirewallRule } from '@/queries/firewall' +import type { BgpCommunity } from '@/types/api' + +const FIREWALL_ACTION_ITEMS = [ + { value: 'block', label: 'block' }, + { value: 'accept', label: 'accept' }, +] as const + +interface FirewallRuleCreateDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + communities: BgpCommunity[] +} + +export function FirewallRuleCreateDialog({ + open, + onOpenChange, + communities, +}: FirewallRuleCreateDialogProps) { + const createMutation = useCreateFirewallRule() + const [action, setAction] = useState<'block' | 'accept'>('block') + const [communityId, setCommunityId] = useState(null) + const [comment, setComment] = useState('') + + useEffect(() => { + if (!open) return + setAction('block') + setCommunityId(null) + setComment('') + }, [open]) + + async function save() { + try { + await createMutation.mutateAsync({ + scope: 'tenant', + action, + community_id: communityId, + comment: comment.trim(), + }) + onOpenChange(false) + } catch { + // toast handled in mutation + } + } + + return ( + + + + Новое правило + +
+ v && setAction(v as 'block' | 'accept')} + /> + +
+ + setComment(e.target.value)} + /> +
+
+ + + + Добавить + + +
+
+ ) +} diff --git a/apps/web/src/queries/firewall.ts b/apps/web/src/queries/firewall.ts index 52e42f6..e442713 100644 --- a/apps/web/src/queries/firewall.ts +++ b/apps/web/src/queries/firewall.ts @@ -83,8 +83,10 @@ export function useCreateFirewallRule() { body: JSON.stringify(body), }), onSuccess: () => { + toast.success('Правило добавлено') void qc.invalidateQueries({ queryKey: firewallKeys.all }) }, + onError: (e) => toast.error(e instanceof Error ? e.message : 'Не удалось добавить правило'), }) } diff --git a/apps/web/src/routes/_auth/firewall.tsx b/apps/web/src/routes/_auth/firewall.tsx index 646712c..aa801a9 100644 --- a/apps/web/src/routes/_auth/firewall.tsx +++ b/apps/web/src/routes/_auth/firewall.tsx @@ -1,6 +1,6 @@ import { createFileRoute } from '@tanstack/react-router' import { useQuery } from '@tanstack/react-query' -import { Copy, Info, RefreshCw, Shield } from 'lucide-react' +import { Copy, Info, Plus, RefreshCw, Shield } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { toast } from 'sonner' @@ -10,10 +10,11 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evob import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' import { BadgeTabs, TabsContent } from '@/components/badge-tabs' +import { DataGridCard } from '@/components/data-grid-shell' import { FirewallClientsGrid } from '@/components/firewall/firewall-clients-grid' +import { FirewallRuleCreateDialog } from '@/components/firewall/firewall-rule-create-dialog' import { FirewallRulesGrid } from '@/components/firewall/firewall-rules-grid' import { PageHeader } from '@/components/page-header' -import { CommunitySelect } from '@/components/modules/community-select' import { QueryState } from '@/components/query-state' import { TableSkeleton } from '@/components/skeletons' import { directoriesCommunitiesQueryOptions } from '@/queries/directories' @@ -22,7 +23,6 @@ import { firewallInstallContextQueryOptions, firewallRulesQueryOptions, useApproveFirewallClient, - useCreateFirewallRule, useDeleteFirewallClient, useDeleteFirewallRule, } from '@/queries/firewall' @@ -48,7 +48,6 @@ function FirewallPage() { const rulesQ = useQuery(firewallRulesQueryOptions('tenant')) const approve = useApproveFirewallClient() const deleteClient = useDeleteFirewallClient() - const createRule = useCreateFirewallRule() const deleteRule = useDeleteFirewallRule() const installCtx = installCtxQ.data @@ -58,6 +57,7 @@ function FirewallPage() { typeof window !== 'undefined' ? httpsOrigin(window.location.origin) : 'https://api.example.com', ) const [seed, setSeed] = useState('') + const [createRuleOpen, setCreateRuleOpen] = useState(false) useEffect(() => { if (installCtx?.suggested_cp_url) { @@ -67,9 +67,6 @@ function FirewallPage() { setSeed(installCtx.bundle_seed) } }, [installCtx?.bundle_seed, installCtx?.suggested_cp_url]) - const [ruleAction, setRuleAction] = useState<'block' | 'accept'>('block') - const [ruleCommunityId, setRuleCommunityId] = useState(null) - const [ruleComment, setRuleComment] = useState('') const communities = communitiesQ.data?.items ?? [] @@ -198,115 +195,91 @@ function FirewallPage() { ]} > - void clientsQ.refetch()} - skeleton={} - > - {() => ( - approve.mutate(id)} - onReject={(id) => deleteClient.mutate(id)} - approvePending={approve.isPending} - rejectPending={deleteClient.isPending} - /> - )} - + + void clientsQ.refetch()} + skeleton={} + > + {() => ( + approve.mutate(id)} + onReject={(id) => deleteClient.mutate(id)} + approvePending={approve.isPending} + rejectPending={deleteClient.isPending} + /> + )} + + - -
-
- - -
- -
- - setRuleComment(e.target.value)} - /> -
- -
- void rulesQ.refetch()} - skeleton={} + + setCreateRuleOpen(true)}> + + Добавить правило + + } > - {() => ( - deleteRule.mutate(id)} - deletePending={deleteRule.isPending} - /> - )} - + void rulesQ.refetch()} + skeleton={} + > + {() => ( + deleteRule.mutate(id)} + deletePending={deleteRule.isPending} + /> + )} + + +
- void clientsQ.refetch()} - skeleton={} + - {() => ( - approve.mutate(id)} - onReject={(id) => deleteClient.mutate(id)} - approvePending={approve.isPending} - rejectPending={deleteClient.isPending} - emptyTitle="Нет pending-запросов" - /> - )} - + void clientsQ.refetch()} + skeleton={} + > + {() => ( + approve.mutate(id)} + onReject={(id) => deleteClient.mutate(id)} + approvePending={approve.isPending} + rejectPending={deleteClient.isPending} + emptyTitle="Нет pending-запросов" + /> + )} + +
diff --git a/apps/web/src/routes/_auth/monitoring.tsx b/apps/web/src/routes/_auth/monitoring.tsx index f2e0753..6ca1475 100644 --- a/apps/web/src/routes/_auth/monitoring.tsx +++ b/apps/web/src/routes/_auth/monitoring.tsx @@ -7,6 +7,7 @@ import { Button } from '@evobgp/ui/components/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evobgp/ui/components/card' import { Separator } from '@evobgp/ui/components/separator' import { BadgeTabs, TabsContent } from '@/components/badge-tabs' +import { DataGridCard } from '@/components/data-grid-shell' import { StatusBadge } from '@/components/status-badge' import { DashboardOperationsFlowCard, @@ -129,24 +130,21 @@ function MonitoringComponent() {
- - - Доступность и готовность - GET /v1/health · GET /v1/ready - - - } - onRetry={() => readyQ.refetch()} - > - {(ready) => } - - - + + } + onRetry={() => readyQ.refetch()} + > + {(ready) => } + + diff --git a/apps/web/src/routes/_auth/tenant-settings.tsx b/apps/web/src/routes/_auth/tenant-settings.tsx index dcfed00..a799714 100644 --- a/apps/web/src/routes/_auth/tenant-settings.tsx +++ b/apps/web/src/routes/_auth/tenant-settings.tsx @@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@evob import { Input } from '@evobgp/ui/components/input' import { Label } from '@evobgp/ui/components/label' import { BadgeTabs, TabsContent } from '@/components/badge-tabs' +import { DataGridCard } from '@/components/data-grid-shell' import { SelectField } from '@/components/select-field' import { SettingsKvGrid } from '@/components/settings/settings-kv-grid' import { PageHeader } from '@/components/page-header' @@ -318,33 +319,28 @@ function TenantSettingsComponent() { - - - Дополнительные параметры - - Параметры вне стандартных групп (readonly — изменяются только через API) - - - - } - onRetry={() => settingsQ.refetch()} - > - {(items) => ( - - )} - - - + + } + onRetry={() => settingsQ.refetch()} + > + {(items) => ( + + )} + +
diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index 5914b0e..17f5ace 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rules-grid.tsx","./src/components/layout/app-shell.tsx","./src/components/modules/community-select.tsx","./src/components/modules/module-as-entry-dialog.tsx","./src/components/modules/module-cdn-source-dialog.tsx","./src/components/modules/module-domain-entry-dialog.tsx","./src/components/modules/module-entries-grid.tsx","./src/components/modules/module-entries-section.tsx","./src/components/modules/module-ip-range-entry-dialog.tsx","./src/components/modules/module-kpi-cards.tsx","./src/components/modules/modules-list-grid.tsx","./src/components/monitoring/monitoring-ready-grid.tsx","./src/components/network/network-peers-card.tsx","./src/components/network/network-peers-grid.tsx","./src/components/network/network-speakers-card.tsx","./src/components/network/network-speakers-grid.tsx","./src/components/network/peer-form-dialog.tsx","./src/components/network/speaker-form-dialog.tsx","./src/components/operations/operations-jobs-grid.tsx","./src/components/operations/operations-revisions-grid.tsx","./src/components/reui/autocomplete.tsx","./src/components/reui/badge.tsx","./src/components/reui/date-selector.tsx","./src/components/reui/filters.tsx","./src/components/reui/frame.tsx","./src/components/reui/number-field.tsx","./src/components/reui/data-grid/data-grid-column-filter.tsx","./src/components/reui/data-grid/data-grid-column-header.tsx","./src/components/reui/data-grid/data-grid-column-visibility.tsx","./src/components/reui/data-grid/data-grid-pagination.tsx","./src/components/reui/data-grid/data-grid-scroll-area.tsx","./src/components/reui/data-grid/data-grid-table-dnd-rows.tsx","./src/components/reui/data-grid/data-grid-table-dnd.tsx","./src/components/reui/data-grid/data-grid-table-virtual.tsx","./src/components/reui/data-grid/data-grid-table.tsx","./src/components/reui/data-grid/data-grid.tsx","./src/components/schedule/schedule-jobs-grid.tsx","./src/components/schedule/schedule-modules-grid.tsx","./src/components/settings/settings-kv-grid.tsx","./src/hooks/use-client-data-grid.ts","./src/lib/api-client.ts","./src/lib/data-grid-defaults.ts","./src/lib/queryclient.ts","./src/lib/router.ts","./src/lib/ui-labels.ts","./src/lib/access/api-key-labels.ts","./src/lib/metrics/deployment-progress.ts","./src/lib/metrics/index.ts","./src/lib/metrics/job-status-breakdown.ts","./src/lib/metrics/module-type-breakdown.ts","./src/lib/metrics/peer-capacity-bars.ts","./src/lib/metrics/peer-session-breakdown.ts","./src/lib/metrics/readiness-breakdown.ts","./src/lib/metrics/recent-platform-activity.ts","./src/lib/metrics/types.ts","./src/lib/modules/display.ts","./src/lib/modules/helpers.ts","./src/queries/api-keys.ts","./src/queries/auth.ts","./src/queries/directories.ts","./src/queries/firewall.ts","./src/queries/modules.ts","./src/queries/monitoring.ts","./src/queries/network.ts","./src/queries/operations.ts","./src/queries/overview.ts","./src/queries/settings.ts","./src/routes/__root.tsx","./src/routes/_auth.tsx","./src/routes/index.tsx","./src/routes/_auth/access.tsx","./src/routes/_auth/dashboard.tsx","./src/routes/_auth/directories.tsx","./src/routes/_auth/firewall.tsx","./src/routes/_auth/monitoring.tsx","./src/routes/_auth/network.tsx","./src/routes/_auth/operations.tsx","./src/routes/_auth/schedule.tsx","./src/routes/_auth/settings.tsx","./src/routes/_auth/tenant-settings.tsx","./src/routes/_auth/modules/$moduleid.tsx","./src/routes/_auth/modules/index.tsx","./src/routes/_auth/modules/new.tsx","./src/types/api.ts","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/main.tsx","./src/routetree.gen.ts","./src/components/badge-tabs.tsx","./src/components/category-badge.tsx","./src/components/confirm-dialog.tsx","./src/components/data-grid-cell.tsx","./src/components/data-grid-shell.tsx","./src/components/data-grid-toolbar.tsx","./src/components/empty-state.tsx","./src/components/loading-button.tsx","./src/components/mode-toggle.tsx","./src/components/page-header.tsx","./src/components/page-shell.tsx","./src/components/query-state.tsx","./src/components/section-cards.tsx","./src/components/select-field.tsx","./src/components/skeletons.tsx","./src/components/status-badge.tsx","./src/components/theme-provider.tsx","./src/components/truncated-text.tsx","./src/components/access/access-api-keys-card.tsx","./src/components/access/access-api-keys-grid.tsx","./src/components/access/api-key-create-dialog.tsx","./src/components/access/api-key-token-dialog.tsx","./src/components/analytics/analytics-activity-list.tsx","./src/components/analytics/analytics-card-shell.tsx","./src/components/analytics/analytics-kpi-row.tsx","./src/components/analytics/analytics-progress.tsx","./src/components/analytics/analytics-segment-control.tsx","./src/components/analytics/chart-bar-strip.tsx","./src/components/analytics/chart-donut-metric.tsx","./src/components/analytics/dashboard-network-capacity-card.tsx","./src/components/analytics/dashboard-operations-flow-card.tsx","./src/components/analytics/dashboard-platform-card.tsx","./src/components/analytics/index.ts","./src/components/analytics/monitoring-health-card.tsx","./src/components/analytics/network-overview-analytics-card.tsx","./src/components/analytics/operations-analytics-card.tsx","./src/components/dashboard/dashboard-network-panel.tsx","./src/components/dashboard/dashboard-quick-actions.tsx","./src/components/dashboard/dashboard-recent-jobs-grid.tsx","./src/components/dashboard/dashboard-recent-revisions-grid.tsx","./src/components/directories/directories-communities-grid.tsx","./src/components/directories/directories-doh-grid.tsx","./src/components/examples/c-input-group-37.tsx","./src/components/examples/c-select-4.tsx","./src/components/examples/c-tabs-6.tsx","./src/components/examples/c-tabs-7.tsx","./src/components/firewall/firewall-clients-grid.tsx","./src/components/firewall/firewall-rule-create-dialog.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