refactor(web): streamline dashboard and tab components for improved layout
CI / changes (push) Successful in 8s
CI / commitlint (push) Skipped
CI / go (push) Skipped
CI / bird2 (push) Skipped
CI / openapi (push) Successful in 40s
CI / web (push) Successful in 57s
CI / release (push) Successful in 4m24s

Refactored various dashboard components and tab structures to enhance layout and responsiveness. Adjusted class names to include 'min-w-0' for better overflow handling and improved alignment. Updated the ChartDonutMetric to include a tooltip for better data representation. Simplified the DashboardRecentJobsGrid and DashboardRecentRevisionsGrid by replacing DataGridSection with DataGridShell for consistency. Enhanced the OpsDashboard layout for better clarity and usability.
This commit is contained in:
Denozordec
2026-08-18 16:03:52 +07:00
parent 44f70443c3
commit dfc33dcb99
8 changed files with 54 additions and 91 deletions
@@ -2,6 +2,8 @@ import { Cell, Label, Pie, PieChart } from 'recharts'
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from '@evobgp/ui/components/chart'
import { cn } from '@evobgp/ui/lib/utils'
@@ -43,6 +45,7 @@ export function ChartDonutMetric({
<div className={cn('flex flex-col items-center justify-start gap-4 sm:flex-row sm:gap-6', className)}>
<ChartContainer config={chartConfig} className="mx-0 aspect-square h-44 w-44 shrink-0">
<PieChart>
<ChartTooltip content={<ChartTooltipContent nameKey="label" hideLabel />} />
<Pie
data={data}
dataKey="count"
+1 -1
View File
@@ -46,7 +46,7 @@ export function BadgeTabs({
<TabsList
variant="line"
className={cn(
'mb-3.5 w-full min-w-0 justify-start gap-4 overflow-x-auto',
'mb-3.5 w-full min-w-0 justify-start gap-4',
listClassName,
)}
>
@@ -3,13 +3,16 @@ import { useMemo, useState } from 'react'
import { ChartDonutMetric } from '@/components/analytics/chart-donut-metric'
import { AnalyticsSegmentControl } from '@/components/analytics/analytics-segment-control'
import { PanelCard } from '@/components/panel-card'
import { Separator } from '@evobgp/ui/components/separator'
import { jobStatusBreakdown, moduleTypeBreakdown } from '@/lib/metrics'
import type { JobRow, ModuleRow } from '@/types/api'
type FlowMode = 'jobs' | 'modules'
/** chart-13 inspired donut breakdown (Card surface). */
/**
* Donut distribution — chart-27 DNA: Frame + one legend (inside ChartDonutMetric).
* @see https://reui.io/preview/base/chart-27
* @see https://reui.io/docs/components/base/frame
*/
export function DashboardOperationsBreakdown({
jobs,
modules,
@@ -32,7 +35,7 @@ export function DashboardOperationsBreakdown({
return (
<PanelCard
title="Поток операций"
description="Распределение задач и типов модулей"
description="Доли статусов задач или типов модулей"
actions={
<AnalyticsSegmentControl
value={mode}
@@ -43,45 +46,16 @@ export function DashboardOperationsBreakdown({
]}
/>
}
className="h-full"
className="min-w-0"
contentClassName="px-5 py-4"
>
<div className="p-4">
{loading ? (
<div className="text-muted-foreground flex h-48 items-center justify-center text-sm">
Загрузка
</div>
) : (
<>
<ChartDonutMetric slices={slices} centerLabel={centerLabel} centerValue={total} />
{slices.length > 0 ? (
<ul className="mt-4 flex min-w-0 flex-col">
{slices.map((slice, index) => {
const pct = total > 0 ? ((slice.count / total) * 100).toFixed(1) : '0'
return (
<li key={slice.key}>
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-3 py-2">
<div className="flex min-w-0 items-center gap-2.5">
<span
aria-hidden
className="border-background size-3 shrink-0 rounded-full border-2 shadow-sm"
style={{ backgroundColor: slice.color }}
/>
<span className="truncate text-sm font-medium">{slice.label}</span>
</div>
<span className="text-sm font-medium tabular-nums">{slice.count}</span>
<span className="text-muted-foreground/70 w-10 text-right text-xs tabular-nums">
{pct}%
</span>
</div>
{index < slices.length - 1 ? <Separator className="w-auto" /> : null}
</li>
)
})}
</ul>
) : null}
</>
)}
</div>
{loading ? (
<div className="text-muted-foreground flex h-44 items-center justify-center text-sm">
Загрузка
</div>
) : (
<ChartDonutMetric slices={slices} centerLabel={centerLabel} centerValue={total} />
)}
</PanelCard>
)
}
@@ -2,7 +2,7 @@ import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridShell } from '@/components/data-grid-shell'
import { StatusBadge } from '@/components/status-badge'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { DATA_GRID_DENSE_LAYOUT } from '@/lib/data-grid-defaults'
@@ -49,7 +49,7 @@ export function DashboardRecentJobsGrid({
[nameById],
)
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
const { table, filteredCount } = useClientDataGrid({
data,
columns,
getSearchText: (row) => {
@@ -63,16 +63,13 @@ export function DashboardRecentJobsGrid({
})
return (
<DataGridSection
<DataGridShell
table={table}
recordCount={filteredCount}
isLoading={isLoading}
emptyMessage="Нет задач"
showPagination={false}
tableLayout={DATA_GRID_DENSE_LAYOUT}
searchValue={globalFilter}
onSearchChange={setGlobalFilter}
searchPlaceholder="Поиск задач…"
/>
)
}
@@ -2,7 +2,7 @@ import { ColumnDef } from '@tanstack/react-table'
import { useMemo } from 'react'
import { DataGridMutedCell, DataGridPrimaryCell } from '@/components/data-grid-cell'
import { DataGridSection } from '@/components/data-grid-shell'
import { DataGridShell } 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'
@@ -42,7 +42,7 @@ export function DashboardRecentRevisionsGrid({
[],
)
const { table, globalFilter, setGlobalFilter, filteredCount } = useClientDataGrid({
const { table, filteredCount } = useClientDataGrid({
data,
columns,
getSearchText: (row) => row.id,
@@ -51,16 +51,13 @@ export function DashboardRecentRevisionsGrid({
})
return (
<DataGridSection
<DataGridShell
table={table}
recordCount={filteredCount}
isLoading={isLoading}
emptyMessage="Нет ревизий"
showPagination={false}
tableLayout={DATA_GRID_DENSE_LAYOUT}
searchValue={globalFilter}
onSearchChange={setGlobalFilter}
searchPlaceholder="Поиск ревизий…"
/>
)
}
@@ -1,11 +1,4 @@
import type { ReactNode } from 'react'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { Skeleton } from '@evobgp/ui/components/skeleton'
import { PageHeader } from '@/components/page-header'
import { KpiStatGrid, type KpiStatCardData } from './kpi-stat-grid'
@@ -47,9 +40,11 @@ function OpsDashboardSkeleton() {
}
/**
* Ops dashboard: KPI → optional Quick Actions → charts → attention queue.
* Ops dashboard: KPI → optional Quick Actions → charts → queue.
* Queue is a sibling Frame grid — never wrap FrameDataGrid inside another Frame.
* @see https://reui.io/preview/base/dashboard-1
* @see https://reui.io/preview/base/stats-12
* @see https://reui.io/docs/components/base/frame
*/
export function OpsDashboard({
title = 'Панель управления',
@@ -77,21 +72,18 @@ export function OpsDashboard({
{afterKpi ? <section aria-label="Быстрые действия">{afterKpi}</section> : null}
<section
aria-label="Аналитика"
className="flex min-w-0 flex-col gap-4"
>
<section aria-label="Аналитика" className="flex min-w-0 flex-col gap-4">
{charts}
</section>
<section aria-label={queueTitle}>
<Frame dense spacing="sm" className="w-full min-w-0">
<FrameHeader>
<FrameTitle>{queueTitle}</FrameTitle>
<FrameDescription>{queueDescription}</FrameDescription>
</FrameHeader>
<FramePanel>{queue}</FramePanel>
</Frame>
<section aria-label={queueTitle} className="flex min-w-0 flex-col gap-4">
<div className="flex min-w-0 flex-col gap-1">
<h2 className="text-sm font-semibold tracking-tight">{queueTitle}</h2>
{queueDescription ? (
<p className="text-muted-foreground max-w-prose text-sm">{queueDescription}</p>
) : null}
</div>
{queue}
</section>
</div>
)
+7 -10
View File
@@ -7,7 +7,6 @@ import { Button } from '@evobgp/ui/components/button'
import { Skeleton } from '@evobgp/ui/components/skeleton'
import { DashboardActivityTimeline } from '@/components/dashboard/dashboard-activity-timeline'
import { DashboardFramePanel } from '@/components/dashboard/dashboard-frame-panel'
import { buildDashboardKpiCards } from '@/components/dashboard/dashboard-kpi-grid'
import { DashboardModulesGrid } from '@/components/dashboard/dashboard-modules-grid'
import { DashboardNetworkHealth } from '@/components/dashboard/dashboard-network-health'
@@ -15,7 +14,7 @@ import { DashboardOperationsBreakdown } from '@/components/dashboard/dashboard-o
import { DashboardQuickLinks } from '@/components/dashboard/dashboard-quick-links'
import { DashboardRecentJobsGrid } from '@/components/dashboard/dashboard-recent-jobs-grid'
import { DashboardRecentRevisionsGrid } from '@/components/dashboard/dashboard-recent-revisions-grid'
import { OpsDashboard } from '@/components/reui-kit'
import { FrameDataGrid, OpsDashboard } from '@/components/reui-kit'
import { chartPanelGridClassName, dashboardMainSidebarClassName } from '@/lib/ui-surface'
import {
moduleNameById,
@@ -111,7 +110,7 @@ function DashboardComponent() {
peers={peers}
speakers={speakers}
/>
<div className="grid min-w-0 gap-4">
<div className="grid min-w-0 items-start gap-4">
<DashboardNetworkHealth peers={peers} speakers={speakers} jobs={jobs} />
<DashboardOperationsBreakdown jobs={jobs} modules={modules} />
</div>
@@ -122,28 +121,26 @@ function DashboardComponent() {
queueDescription="Последние фоновые операции и история конфигураций"
queue={
<div className={chartPanelGridClassName}>
<DashboardFramePanel
<FrameDataGrid
title="Недавние задачи"
description="Последние фоновые операции"
className="h-full min-w-0"
>
{activityLoading ? (
<Skeleton className="m-4 h-24 w-auto" />
) : (
<DashboardRecentJobsGrid jobs={jobs.slice(0, 10)} nameById={nameById} isLoading={refreshing} />
<DashboardRecentJobsGrid jobs={jobs.slice(0, 8)} nameById={nameById} isLoading={refreshing} />
)}
</DashboardFramePanel>
<DashboardFramePanel
</FrameDataGrid>
<FrameDataGrid
title="Последние ревизии"
description="История конфигураций"
className="h-full min-w-0"
>
{activityLoading ? (
<Skeleton className="m-4 h-24 w-auto" />
) : (
<DashboardRecentRevisionsGrid revisions={revisions} isLoading={refreshing} />
)}
</DashboardFramePanel>
</FrameDataGrid>
</div>
}
/>
+10 -7
View File
@@ -24,12 +24,13 @@ function Tabs({
}
const tabsListVariants = cva(
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
"group/tabs-list inline-flex w-fit items-center justify-center rounded-lg text-muted-foreground group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 border-b border-border bg-transparent",
default:
"bg-muted p-[3px] group-data-[orientation=horizontal]/tabs:h-9",
line: "h-auto gap-1 overflow-visible border-b border-border bg-transparent p-0",
},
},
defaultVariants: {
@@ -58,10 +59,12 @@ function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) {
<TabsPrimitive.Tab
data-slot="tabs-trigger"
className={cn(
"relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground group-data-[variant=default]/tabs-list:data-active:shadow-sm group-data-[variant=line]/tabs-list:data-active:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 dark:data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
"relative inline-flex items-center justify-center gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 has-data-[icon=inline-end]:pr-1 has-data-[icon=inline-start]:pl-1 aria-disabled:pointer-events-none aria-disabled:opacity-50 dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=default]/tabs-list:h-[calc(100%-1px)] group-data-[variant=default]/tabs-list:flex-1 group-data-[variant=default]/tabs-list:data-active:shadow-sm",
"group-data-[variant=line]/tabs-list:h-auto group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:rounded-none group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:py-1.5 group-data-[variant=line]/tabs-list:shadow-none group-data-[variant=line]/tabs-list:data-active:bg-transparent group-data-[variant=line]/tabs-list:data-active:shadow-none dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"group-data-[variant=default]/tabs-list:data-active:bg-background group-data-[variant=default]/tabs-list:data-active:text-foreground dark:group-data-[variant=default]/tabs-list:data-active:border-input dark:group-data-[variant=default]/tabs-list:data-active:bg-input/30 dark:group-data-[variant=default]/tabs-list:data-active:text-foreground",
"data-active:text-foreground",
"after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-0 group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
className
)}
{...props}