Compare commits

...
3 Commits
Author SHA1 Message Date
Denozordec 1639ba40f3 refactor: update dashboard components to integrate QuickActionGrid and enhance settings
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 33s
CI / web (push) Successful in 1m0s
CI / go (push) Successful in 56s
CI / bird2 (push) Successful in 18s
CI / release (push) Successful in 4m14s
Removed the deprecated DashboardQuickLinkCard and replaced it with QuickActionGrid in the DashboardQuickLinks component for improved organization and user experience. Updated the AppearanceSettingsTab to include a toggle for displaying quick actions on the dashboard, enhancing user customization options. Adjusted the SystemMonitorPopover layout for better alignment and spacing. Updated documentation to reflect the new UI preferences for quick actions.
2026-07-17 20:34:25 +07:00
Denozordec 26a96bc824 refactor: update KPI components for improved layout and clarity
CI / changes (push) Successful in 7s
CI / commitlint (push) Skipped
CI / openapi (push) Skipped
CI / web (push) Successful in 47s
CI / go (push) Successful in 52s
CI / bird2 (push) Successful in 15s
CI / release (push) Successful in 4m21s
Revised the KpiStatGrid and KpiStatCardBody components to enhance the layout and presentation of KPI tiles. Updated the documentation to reflect the new horizontal compact hybrid design, ensuring better visual organization with the icon on the left and improved label and value alignment. Adjusted the skeleton loading state for a more cohesive user experience.
2026-07-17 18:37:00 +07:00
Denozordec 4fc5c96e63 refactor: enhance KPI components with variant support and improved layout
CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Skipped
CI / web (push) Successful in 49s
CI / go (push) Successful in 51s
CI / bird2 (push) Successful in 16s
CI / release (push) Successful in 4m12s
Updated the KPI components to include a new KpiStatVariant type for better visual differentiation. Refactored the KpiStatCardBody and KpiStatGrid to utilize this variant, allowing for dynamic styling based on the KPI state. Improved the layout of the KpiStatGrid and its associated components for a more cohesive user experience, including adjustments to the Badge footer presentation.
2026-07-17 17:15:14 +07:00
14 changed files with 357 additions and 170 deletions
@@ -122,6 +122,7 @@ function buildKpis({
iconClassName: 'text-destructive',
value: loading ? '—' : String(riskCount),
label: 'Риски',
variant: riskCount > 0 ? 'destructive' : 'default',
footer: (
<Badge variant={riskCount > 0 ? 'destructive-light' : 'success-light'} size="sm">
{loading
@@ -1,69 +0,0 @@
import { Link } from '@tanstack/react-router'
import { ChevronRight } from 'lucide-react'
import type { ReactNode } from 'react'
import { CardDotField } from '@/components/dashboard/card-dot-field'
import { Card, CardContent } from '@evobgp/ui/components/card'
import { cn } from '@evobgp/ui/lib/utils'
import { Item, ItemMedia } from '@evobgp/ui/components/item'
export type QuickLinkCardProps = {
icon: ReactNode
label: string
description: string
to: string
search?: Record<string, string>
iconClass: string
}
export function DashboardQuickLinkCard({
icon,
label,
description,
to,
search,
iconClass,
}: QuickLinkCardProps) {
return (
<Link
to={to}
search={search}
className="group block h-full rounded-[inherit] focus-visible:outline-none"
aria-label={`${label}: ${description}`}
>
<Card
size="sm"
className={cn(
'relative isolate h-full overflow-hidden transition-colors',
'hover:border-foreground/20',
'group-focus-visible:ring-2 group-focus-visible:ring-ring group-focus-visible:ring-offset-2 group-focus-visible:ring-offset-background',
)}
>
<CardDotField className="text-muted-foreground [mask-image:linear-gradient(to_bottom_left,black,transparent_60%)]" />
<CardContent className="relative z-10 flex h-full flex-col gap-7.5 p-5">
<Item
className={cn(
'border-background flex size-11 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-5',
iconClass,
)}
>
<ItemMedia variant="icon" className="size-auto">
{icon}
</ItemMedia>
</Item>
<div className="mt-auto flex flex-col gap-3">
<span className="text-foreground block text-sm leading-tight font-medium">{label}</span>
<p className="text-muted-foreground text-xs leading-relaxed">{description}</p>
<span className="text-primary inline-flex items-center gap-1 text-xs font-medium underline-offset-2 group-hover:underline">
Перейти
<ChevronRight
aria-hidden
className="size-2.5 shrink-0 transition-transform group-hover:translate-x-0.5"
/>
</span>
</div>
</CardContent>
</Card>
</Link>
)
}
@@ -1,78 +1,67 @@
import { Gauge, Network, Play, Plus, Share2, Tags } from 'lucide-react'
import type { ReactNode } from 'react'
import { DashboardQuickLinkCard } from '@/components/dashboard/dashboard-quick-link-card'
import { PanelCard } from '@/components/panel-card'
import { QuickActionGrid, type QuickActionItem } from '@/components/reui-kit'
type QuickLink = {
icon: ReactNode
label: string
description: string
to: string
search?: Record<string, string>
iconClass: string
}
const LINKS: QuickLink[] = [
const ACTIONS: QuickActionItem[] = [
{
icon: <Plus aria-hidden />,
label: 'Создать модуль',
id: 'new-module',
title: 'Создать модуль',
description: 'Новый модуль маршрутизации и источники префиксов.',
to: '/modules/new',
iconClass: 'bg-primary text-primary-foreground [&_svg]:text-primary-foreground',
icon: <Plus aria-hidden />,
iconClassName: 'bg-primary text-primary-foreground [&_svg]:text-primary-foreground',
},
{
icon: <Tags aria-hidden />,
label: 'BGP-сообщества',
id: 'communities',
title: 'BGP-сообщества',
description: 'Справочник communities для политик экспорта.',
to: '/directories',
iconClass: 'bg-info text-info-foreground [&_svg]:text-info-foreground',
icon: <Tags aria-hidden />,
iconClassName: 'bg-info text-info-foreground [&_svg]:text-info-foreground',
},
{
icon: <Network aria-hidden />,
label: 'Сеть',
id: 'network',
title: 'Сеть',
description: 'Обзор пиров, спикеров и live-сессий BGP.',
to: '/network',
search: { tab: 'overview' },
iconClass: 'bg-success text-success-foreground [&_svg]:text-success-foreground',
icon: <Network aria-hidden />,
iconClassName: 'bg-success text-success-foreground [&_svg]:text-success-foreground',
},
{
icon: <Share2 aria-hidden />,
label: 'Добавить пира',
id: 'add-peer',
title: 'Добавить пира',
description: 'Настройка BGP-соседа и шаблонов сессии.',
to: '/network',
search: { tab: 'peers' },
iconClass: 'bg-warning text-warning-foreground [&_svg]:text-warning-foreground',
icon: <Share2 aria-hidden />,
iconClassName: 'bg-warning text-warning-foreground [&_svg]:text-warning-foreground',
},
{
icon: <Play aria-hidden />,
label: 'Деплой',
id: 'deploy',
title: 'Деплой',
description: 'Ревизии конфигурации и применение на нодах.',
to: '/operations',
search: { tab: 'revisions' },
iconClass: 'bg-focus text-focus-foreground [&_svg]:text-focus-foreground',
icon: <Play aria-hidden />,
iconClassName: 'bg-focus text-focus-foreground [&_svg]:text-focus-foreground',
},
{
icon: <Gauge aria-hidden />,
label: 'Мониторинг',
id: 'monitoring',
title: 'Мониторинг',
description: 'Состояние системы, BIRD и PostgreSQL.',
to: '/monitoring',
search: { tab: 'system' },
iconClass: 'bg-destructive text-destructive-foreground [&_svg]:text-destructive-foreground',
icon: <Gauge aria-hidden />,
iconClassName: 'bg-destructive text-destructive-foreground [&_svg]:text-destructive-foreground',
},
]
export function DashboardQuickLinks() {
return (
<PanelCard
title="Быстрые действия"
<QuickActionGrid
actions={ACTIONS}
description="Частые переходы к настройке и деплою"
className="@container w-full"
contentClassName="grid gap-3 p-4 sm:grid-cols-2 xl:grid-cols-3"
>
{LINKS.map((link) => (
<DashboardQuickLinkCard key={link.label} {...link} />
))}
</PanelCard>
/>
)
}
@@ -9,5 +9,6 @@ export {
kpiStatItemKey,
type KpiStatItem,
type KpiStatCardData,
type KpiStatVariant,
type OpsKpiCard,
} from '@/components/reui-kit/kpi-stat-grid'
@@ -181,7 +181,7 @@ export function SystemMonitorPopover() {
</Badge>
</PopoverTrigger>
<PopoverContent align="end" sideOffset={8} className="w-80 gap-0! space-y-0! p-0!">
<PopoverContent align="end" sideOffset={8} className="flex w-80 flex-col gap-0! p-0!">
<div className="border-border flex items-center justify-between border-b px-3 py-2.5">
<span className="text-foreground text-xs font-medium">Монитор EvoBGP</span>
<span className="text-muted-foreground text-[11px] tabular-nums">
@@ -17,8 +17,10 @@ export {
type KpiStatItem,
type KpiStatCardData,
type KpiStatCard as KpiStatCardType,
type KpiStatVariant,
type OpsKpiCard,
} from './kpi-stat-grid'
export { QuickActionGrid, type QuickActionItem } from './quick-action-grid'
export { OpsDashboard } from './ops-dashboard'
export { DetailPanel, type DetailMetricCard } from './detail-panel'
export { SettingsShell, type SettingsTabConfig } from './settings-shell'
@@ -2,15 +2,15 @@ import type { KeyboardEvent, ReactNode } from 'react'
import { Link } from '@tanstack/react-router'
import { Frame, FramePanel } from '@/components/reui/frame'
import { Badge } from '@/components/reui/badge'
import { cn } from '@evobgp/ui/lib/utils'
import { Item, ItemMedia } from '@evobgp/ui/components/item'
import { Skeleton } from '@evobgp/ui/components/skeleton'
export type KpiStatVariant = 'default' | 'warning' | 'destructive'
/**
* KPI tile data — stats-12 visual (icon tile + value + label + footer).
* CFDM-compatible: id, label, value, hint?, to?, search?, onSelect?, selected?
* EvoBGP: icon?, iconClassName?, footer?, active?, onClick?
*
* KPI tile data — horizontal compact hybrid (icon left + label/Badge + value).
* @see https://reui.io/preview/base/stats-12
*/
export type KpiStatItem = {
@@ -26,6 +26,7 @@ export type KpiStatItem = {
active?: boolean
icon?: ReactNode
iconClassName?: string
variant?: KpiStatVariant
footer?: ReactNode
}
@@ -40,7 +41,13 @@ export type KpiStatCard = KpiStatCardData
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
function kpiStatGridClassName(count: number): string {
const VALUE_VARIANT_CLASS: Record<KpiStatVariant, string> = {
default: 'text-foreground',
warning: 'text-warning',
destructive: 'text-destructive',
}
function kpiCols(count: number): string {
if (count <= 1) return 'grid-cols-1'
if (count === 2) return 'grid-cols-1 @xl:grid-cols-2'
if (count === 3) return 'grid-cols-1 @3xl:grid-cols-3'
@@ -65,19 +72,29 @@ function isSelected(item: KpiStatItem): boolean {
return Boolean(item.selected ?? item.active)
}
function resolveFooter(item: KpiStatItem): ReactNode {
if (item.footer) return item.footer
if (typeof item.hint === 'string') {
return (
<Badge variant="outline" size="sm">
{item.hint}
</Badge>
)
}
if (item.hint) return item.hint
return null
}
function KpiStatCardBody({ item }: { item: KpiStatItem }) {
const footer =
item.footer ??
(item.hint ? (
<span className="text-muted-foreground text-xs leading-snug">{item.hint}</span>
) : null)
const footer = resolveFooter(item)
const valueVariant = item.variant ?? 'default'
return (
<>
<div className="relative z-10 flex h-full items-start gap-3">
{item.icon ? (
<Item
className={cn(
'border-background bg-muted flex size-10.5 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
item.iconClassName ?? DEFAULT_ICON_CLASS,
)}
>
@@ -87,19 +104,39 @@ function KpiStatCardBody({ item }: { item: KpiStatItem }) {
</Item>
) : null}
<div className="space-y-0.5">
<div className="text-foreground text-2xl leading-none font-bold tabular-nums">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex items-start justify-between gap-2">
<div className="text-muted-foreground text-sm font-medium">{item.label}</div>
{footer ? <div className="shrink-0">{footer}</div> : null}
</div>
<div
className={cn(
'text-2xl leading-none font-bold tabular-nums',
VALUE_VARIANT_CLASS[valueVariant],
)}
>
{item.value}
</div>
<div className="text-muted-foreground text-sm font-medium">{item.label}</div>
</div>
{footer ? <div className="mt-auto w-full">{footer}</div> : null}
</>
</div>
)
}
/** Single KPI tile (ReUI stats-12 / dashboard PRO pattern). */
function panelClassName(item: KpiStatItem, className?: string) {
const onActivate = resolveActivate(item)
const clickable = Boolean(item.to || onActivate)
const selected = isSelected(item)
return cn(
'relative isolate flex h-full flex-col',
clickable &&
'hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2',
selected && 'ring-primary/30 bg-muted/30 ring-1',
className,
)
}
/** Single KPI tile — used for embedded / standalone contexts. */
export function KpiStatCardTile({
item,
embedded = false,
@@ -110,26 +147,14 @@ export function KpiStatCardTile({
className?: string
}) {
const onActivate = resolveActivate(item)
const clickable = Boolean(item.to || onActivate)
const selected = isSelected(item)
const panelClass = cn(
'flex h-full flex-col items-start gap-6',
clickable && 'cursor-pointer transition-colors hover:bg-muted/30',
selected && 'ring-1 ring-primary/30',
className,
)
const panelClass = panelClassName(item, className)
let panel: ReactNode
if (item.to) {
panel = (
<FramePanel className={panelClass}>
<Link
to={item.to}
search={item.search}
className="flex h-full w-full flex-col items-start gap-6 focus-visible:outline-none"
>
<Link to={item.to} search={item.search} className="focus-visible:outline-none">
<KpiStatCardBody item={item} />
</Link>
</FramePanel>
@@ -176,22 +201,22 @@ export function KpiStatCard({
function KpiStatGridSkeleton({ count }: { count: number }) {
return (
<section className="@container w-full" aria-label="Загрузка показателей">
<div className={cn('grid gap-5', kpiStatGridClassName(count))}>
<Frame className="@container w-full">
<div className={cn('grid gap-2', kpiCols(count))}>
{Array.from({ length: count }).map((_, index) => (
<Frame key={index} className="h-full">
<FramePanel className="flex h-full flex-col items-start gap-6">
<Skeleton className="size-10.5 rounded-md" />
<div className="flex flex-col gap-1">
<Skeleton className="h-8 w-16" />
<Skeleton className="h-4 w-28" />
<FramePanel key={index} className="flex items-start gap-3">
<Skeleton className="size-10.5 shrink-0 rounded-lg" />
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
<div className="flex items-center justify-between gap-2">
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4.5 w-14 rounded-full" />
</div>
<Skeleton className="mt-auto h-5 w-32 rounded-full" />
</FramePanel>
</Frame>
<Skeleton className="h-7 w-16" />
</div>
</FramePanel>
))}
</div>
</section>
</Frame>
)
}
@@ -205,10 +230,50 @@ interface KpiStatGridProps {
emptyIcon?: ReactNode
className?: string
skeletonCount?: number
/** Wrap each tile in its own Frame (analytics panels). */
embedded?: boolean
'aria-label'?: string
}
function KpiStatCardItem({ item }: { item: KpiStatItem }) {
const onActivate = resolveActivate(item)
const panelClass = panelClassName(item)
if (item.to) {
return (
<FramePanel className={panelClass}>
<Link to={item.to} search={item.search} className="focus-visible:outline-none">
<KpiStatCardBody item={item} />
</Link>
</FramePanel>
)
}
if (onActivate) {
return (
<FramePanel
className={panelClass}
onClick={onActivate}
role="button"
tabIndex={0}
onKeyDown={(e) => handleCardKeyDown(onActivate, e)}
>
<KpiStatCardBody item={item} />
</FramePanel>
)
}
return (
<FramePanel className={panelClass}>
<KpiStatCardBody item={item} />
</FramePanel>
)
}
/**
* Hybrid KPI — EvoBGP visual + horizontal compact layout (icon left).
* Preview: https://reui.io/preview/base/stats-12
*/
export function KpiStatGrid({
items,
cards,
@@ -239,18 +304,26 @@ export function KpiStatGrid({
)
}
if (embedded) {
return (
<section aria-label={ariaLabel} className={cn('@container w-full', className)}>
<div className={cn('grid gap-2', kpiCols(list.length || 1))}>
{list.map((item, index) => (
<KpiStatCardTile key={kpiStatItemKey(item, index)} item={item} embedded />
))}
</div>
</section>
)
}
return (
<section aria-label={ariaLabel} className={cn('@container w-full', className)}>
<div className={cn('grid gap-5', kpiStatGridClassName(list.length || 1))}>
<Frame className={cn('@container w-full', className)} aria-label={ariaLabel}>
<div className={cn('grid gap-2', kpiCols(list.length || 1))}>
{list.map((item, index) => (
<KpiStatCardTile
key={kpiStatItemKey(item, index)}
item={item}
embedded={embedded}
/>
<KpiStatCardItem key={kpiStatItemKey(item, index)} item={item} />
))}
</div>
</section>
</Frame>
)
}
@@ -0,0 +1,113 @@
import type { ReactNode } from 'react'
import { Link } from '@tanstack/react-router'
import {
Frame,
FrameDescription,
FrameHeader,
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { Badge } from '@/components/reui/badge'
import { Item, ItemMedia } from '@evobgp/ui/components/item'
import { cn } from '@evobgp/ui/lib/utils'
export interface QuickActionItem {
id: string
title: string
description: string
to: string
search?: Record<string, unknown>
icon?: ReactNode
iconClassName?: string
}
interface QuickActionGridProps {
actions: QuickActionItem[]
title?: string
description?: string
className?: string
}
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
function kpiCols(count: number): string {
if (count <= 1) return 'grid-cols-1'
if (count === 2) return 'grid-cols-1 @xl:grid-cols-2'
if (count === 3) return 'grid-cols-1 @3xl:grid-cols-3'
if (count === 4) return 'grid-cols-1 @3xl:grid-cols-2 @6xl:grid-cols-4'
if (count === 5) return 'grid-cols-2 @3xl:grid-cols-3 xl:grid-cols-5'
if (count === 6) return 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-6'
return 'grid-cols-2 sm:grid-cols-3 lg:grid-cols-4'
}
function QuickActionBody({ action }: { action: QuickActionItem }) {
return (
<div className="relative z-10 flex h-full items-start gap-3">
{action.icon ? (
<Item
className={cn(
'border-background bg-muted flex size-10.5 shrink-0 items-center justify-center border-2 p-0 shadow-[0_1px_3px_0_rgba(0,0,0,0.14)] dark:border [&_svg]:size-4',
action.iconClassName ?? DEFAULT_ICON_CLASS,
)}
>
<ItemMedia variant="icon" className="size-auto">
{action.icon}
</ItemMedia>
</Item>
) : null}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<div className="flex items-start justify-between gap-2">
<span className="text-foreground text-sm font-medium">{action.title}</span>
<Badge variant="outline" size="sm" className="shrink-0">
Перейти
</Badge>
</div>
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
{action.description}
</p>
</div>
</div>
)
}
/**
* KPI-like quick actions strip (horizontal Frame tiles).
* Preview: https://reui.io/preview/base/stats-12
*/
export function QuickActionGrid({
actions,
title = 'Быстрые действия',
description,
className,
}: QuickActionGridProps) {
if (actions.length === 0) return null
return (
<Frame dense spacing="sm" className={cn('@container w-full', className)}>
{(title || description) && (
<FrameHeader>
{title ? <FrameTitle>{title}</FrameTitle> : null}
{description ? <FrameDescription>{description}</FrameDescription> : null}
</FrameHeader>
)}
<div className={cn('grid gap-2', kpiCols(actions.length))}>
{actions.map((action) => (
<FramePanel
key={action.id}
className="relative isolate flex h-full flex-col hover:bg-muted/40 focus-within:ring-ring cursor-pointer transition-colors focus-within:ring-2"
>
<Link
to={action.to}
search={action.search}
className="focus-visible:outline-none"
aria-label={`${action.title}: ${action.description}`}
>
<QuickActionBody action={action} />
</Link>
</FramePanel>
))}
</div>
</Frame>
)
}
+3 -1
View File
@@ -32,15 +32,17 @@ function hintToFooter(hint: ReactNode) {
}
function toKpiStatItem(item: SectionCardItem, index: number): KpiStatItem {
const variant = item.variant ?? 'default'
const footer =
item.badge ?? (item.hint ? hintToFooter(item.hint) : undefined)
return {
id: typeof item.label === 'string' ? item.label : `section-${index}`,
icon: item.icon,
iconClassName: VARIANT_ICON_CLASS[item.variant ?? 'default'],
iconClassName: VARIANT_ICON_CLASS[variant],
value: item.value,
label: item.label,
variant,
footer,
active: item.active,
onClick: item.onClick,
@@ -1,5 +1,7 @@
import { Moon, Sun, SunMoon } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { SettingRow } from '@/components/blocks/settings-7/components/setting-row'
import { SettingsCard } from '@/components/blocks/settings-7/components/settings-card'
@@ -9,6 +11,9 @@ import {
ToggleGroup,
ToggleGroupItem,
} from '@evobgp/ui/components/toggle-group'
import { Switch } from '@evobgp/ui/components/switch'
import { apiMutate } from '@/lib/api-client'
import { settingsKeys, settingsQueryOptions } from '@/queries/settings'
const THEME_OPTIONS = [
{ value: 'light', label: 'Светлая', icon: Sun },
@@ -16,8 +21,27 @@ const THEME_OPTIONS = [
{ value: 'system', label: 'Система', icon: SunMoon },
] as const
function parseShowQuickActions(value: unknown): boolean {
if (value === false || value === 0 || value === 'false' || value === '0') return false
return true
}
export function AppearanceSettingsTab() {
const { theme, setTheme } = useTheme()
const qc = useQueryClient()
const settingsQ = useQuery(settingsQueryOptions())
const showQuickActions = parseShowQuickActions(settingsQ.data?.ui_show_quick_actions)
const patchMut = useMutation({
mutationFn: (payload: Record<string, boolean>) =>
apiMutate('/v1/settings', 'PATCH', payload),
onSuccess: () => {
toast.success('Настройки интерфейса сохранены')
void qc.invalidateQueries({ queryKey: settingsKeys.all })
},
onError: (e) =>
toast.error(e instanceof Error ? e.message : 'Не удалось сохранить'),
})
return (
<div className="space-y-6">
@@ -63,6 +87,31 @@ export function AppearanceSettingsTab() {
</SettingRow>
</SettingsFieldGroup>
</SettingsCard>
<SettingsCard
title="Дашборд"
description="Блоки на экране «Обзор»"
>
<SettingsFieldGroup
legend="Быстрые действия"
description="Показывать KPI-like плитки быстрых переходов под метриками."
>
<SettingRow
title="Быстрые действия"
description="Блок с частыми переходами (модули, сеть, деплой) на дашборде."
last
>
<Switch
checked={showQuickActions}
disabled={settingsQ.isLoading || patchMut.isPending}
onCheckedChange={(checked) =>
patchMut.mutate({ ui_show_quick_actions: checked })
}
aria-label="Показывать быстрые действия"
/>
</SettingRow>
</SettingsFieldGroup>
</SettingsCard>
</div>
)
}
+11 -3
View File
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { useQueries } from '@tanstack/react-query'
import { useQueries, useQuery } from '@tanstack/react-query'
import { RefreshCw } from 'lucide-react'
import { useState } from 'react'
@@ -27,13 +27,21 @@ import {
overviewRevisionsQueryOptions,
overviewSpeakersQueryOptions,
} from '@/queries/overview'
import { settingsQueryOptions } from '@/queries/settings'
export const Route = createFileRoute('/_auth/dashboard')({
component: DashboardComponent,
})
function parseShowQuickActions(value: unknown): boolean {
if (value === false || value === 0 || value === 'false' || value === '0') return false
return true
}
function DashboardComponent() {
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const settingsQ = useQuery(settingsQueryOptions())
const showQuickActions = parseShowQuickActions(settingsQ.data?.ui_show_quick_actions)
const results = useQueries({
queries: [
@@ -86,8 +94,6 @@ function DashboardComponent() {
}
/>
<DashboardQuickLinks />
{initialLoading ? (
<AnalyticsDashboardSkeleton />
) : (
@@ -99,6 +105,8 @@ function DashboardComponent() {
jobs={jobs}
/>
{showQuickActions ? <DashboardQuickLinks /> : null}
<div className={dashboardMainSidebarClassName}>
<div className="xl:col-span-8">
<DashboardModulesGrid modules={modules} isLoading={refreshing} />
File diff suppressed because one or more lines are too long
+4
View File
@@ -1455,6 +1455,10 @@ components:
description: UTC cron для автоочистки (по умолчанию `0 */6 * * *`).
runtime_logs_auto_mode:
$ref: "#/components/schemas/RuntimeLogCleanupMode"
ui_show_quick_actions:
type: boolean
description: Показывать блок «Быстрые действия» на дашборде (UI preference).
default: true
additionalProperties: true
RevisionDiff:
+16 -2
View File
@@ -18,7 +18,7 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad
| Зона | Block | Preview |
|------|-------|---------|
| Shell | `app-shell-12` (+ cmdk/monitor где нужно) | https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7 |
| KPI | `stats-12` (primary); `card-35` compact strip | https://reui.io/preview/base/stats-12 · https://reui.io/preview/base/card-35 |
| KPI | horizontal compact hybrid (icon left + label/Badge + value ± variant; EvoBGP visual) | https://reui.io/preview/base/stats-12 |
| Dashboard | `dashboard-1` | https://reui.io/preview/base/dashboard-1 |
| Lists | `data-grid-filtering-2` | https://reui.io/preview/base/data-grid-filtering-2 |
| Settings | `settings-16` + SettingRow (`settings-7`) | https://reui.io/preview/base/settings-16 · https://reui.io/preview/base/settings-7 |
@@ -31,12 +31,26 @@ Ops / list / dashboard / detail / settings — только **Frame**, не shad
| Component | Role |
|-----------|------|
| `ResourcePage` | Frame + line tabs + Filters + DataGrid |
| `KpiStatGrid` | stats-12 KPI tiles |
| `KpiStatGrid` | horizontal compact hybrid KPI tiles (`variant`, Badge) |
| `QuickActionGrid` | KPI-like quick action tiles under KPI (gated by `ui_show_quick_actions`) |
| `OpsDashboard` | KPI + charts + attention queue |
| `SettingsShell` | settings nav + Outlet |
| `DetailPanel` | detail Frame sections |
| `filter-utils` | apply/clear ReUI Filters |
## Dashboard layout
| App | Section order |
|-----|---------------|
| EvoBGP / CFDM | KPI → **QuickActionGrid** → charts / rest |
| vps-tracker | banner → KPI → charts → attention → **QuickActionGrid** → CSV |
Gating: KV `ui_show_quick_actions` in `global_settings` via `PATCH /v1/settings` (default `true`).
## System monitor
`SystemMonitorPopover` in app-shell header next to `ModeToggle`. Preview: https://reui.io/preview/base/app-shell-12 · https://reui.io/preview/base/app-shell-7
## MCP workflow
1. MCP `user-reui``search` / `get_block` / `get_component` with `surface: "frame"`