feat(analytics): remove OperationsAnalyticsCard and update imports
Deleted the OperationsAnalyticsCard component to streamline analytics functionality. Updated the analytics index file to reflect this change by removing the export for OperationsAnalyticsCard. Adjusted the QuickActionGrid component to enhance user interaction with new keyboard handling and badge functionality. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -10,4 +10,3 @@ export { DashboardOperationsFlowCard } from './dashboard-operations-flow-card'
|
||||
export { DashboardPlatformCard } from './dashboard-platform-card'
|
||||
export { MonitoringHealthCard } from './monitoring-health-card'
|
||||
export { NetworkOverviewAnalyticsCard } from './network-overview-analytics-card'
|
||||
export { OperationsAnalyticsCard } from './operations-analytics-card'
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { AnalyticsCardShell } from '@/components/analytics/analytics-card-shell'
|
||||
import { AnalyticsKpiRow } from '@/components/analytics/analytics-kpi-row'
|
||||
import { ChartDonutMetric } from '@/components/analytics/chart-donut-metric'
|
||||
import { jobStatusBreakdown } from '@/lib/metrics'
|
||||
import type { JobRow, RevisionRow } from '@/types/api'
|
||||
|
||||
export function OperationsAnalyticsCard({
|
||||
jobs,
|
||||
revisions,
|
||||
loading,
|
||||
}: {
|
||||
jobs: JobRow[]
|
||||
revisions: RevisionRow[]
|
||||
loading?: boolean
|
||||
}) {
|
||||
const running = jobs.filter((j) => ['running', 'queued'].includes(j.status.toLowerCase())).length
|
||||
const failed = jobs.filter((j) =>
|
||||
['failed', 'error', 'cancelled'].includes(j.status.toLowerCase()),
|
||||
).length
|
||||
const slices = jobStatusBreakdown(jobs)
|
||||
const total = slices.reduce((sum, slice) => sum + slice.count, 0)
|
||||
|
||||
return (
|
||||
<AnalyticsCardShell
|
||||
title="Операции и задачи"
|
||||
description="Статистика ревизий и фоновых jobs"
|
||||
info="Данные из GET /v1/jobs и /v1/revisions."
|
||||
>
|
||||
<AnalyticsKpiRow
|
||||
items={[
|
||||
{
|
||||
label: 'Ревизий',
|
||||
value: loading ? '—' : String(revisions.length),
|
||||
delta: { direction: 'neutral', label: 'в выборке', tone: 'muted' },
|
||||
},
|
||||
{
|
||||
label: 'Активных задач',
|
||||
value: loading ? '—' : String(running),
|
||||
delta: {
|
||||
direction: running > 0 ? 'up' : 'neutral',
|
||||
label: running > 0 ? 'выполняются' : 'очередь пуста',
|
||||
tone: (running > 0 ? 'warning' : 'muted') as 'warning' | 'muted',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'С ошибкой',
|
||||
value: loading ? '—' : String(failed),
|
||||
delta: {
|
||||
direction: failed > 0 ? 'down' : 'up',
|
||||
label: failed > 0 ? 'требуют внимания' : 'в норме',
|
||||
tone: failed > 0 ? 'destructive' : 'success',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{loading ? (
|
||||
<div className="flex h-48 items-center justify-center text-sm text-muted-foreground">
|
||||
Загрузка…
|
||||
</div>
|
||||
) : (
|
||||
<ChartDonutMetric slices={slices} centerLabel="Задачи" centerValue={total} />
|
||||
)}
|
||||
</AnalyticsCardShell>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { KeyboardEvent, ReactNode } from 'react'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
Frame,
|
||||
@@ -12,16 +12,23 @@ import { Item, ItemMedia } from '@evobgp/ui/components/item'
|
||||
import { cn } from '@evobgp/ui/lib/utils'
|
||||
import { kpiCols } from './kpi-cols'
|
||||
|
||||
export interface QuickActionItem {
|
||||
type QuickActionBase = {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
to: string
|
||||
search?: Record<string, unknown>
|
||||
icon?: ReactNode
|
||||
iconClassName?: string
|
||||
/** Override badge label (default: «Перейти» for links, «Выполнить» for onClick). */
|
||||
badge?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export type QuickActionItem = QuickActionBase &
|
||||
(
|
||||
| { to: string; search?: Record<string, unknown>; onClick?: never }
|
||||
| { onClick: () => void; to?: never; search?: never }
|
||||
)
|
||||
|
||||
interface QuickActionGridProps {
|
||||
actions: QuickActionItem[]
|
||||
title?: string
|
||||
@@ -31,6 +38,18 @@ interface QuickActionGridProps {
|
||||
|
||||
const DEFAULT_ICON_CLASS = 'text-muted-foreground [&_svg]:text-current'
|
||||
|
||||
function resolveBadge(action: QuickActionItem): string {
|
||||
if (action.badge) return action.badge
|
||||
return action.onClick ? 'Выполнить' : 'Перейти'
|
||||
}
|
||||
|
||||
function handleActionKeyDown(onActivate: () => void, event: KeyboardEvent<HTMLDivElement>) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
onActivate()
|
||||
}
|
||||
}
|
||||
|
||||
function QuickActionBody({ action }: { action: QuickActionItem }) {
|
||||
return (
|
||||
<div className="relative z-10 flex h-full items-start gap-3">
|
||||
@@ -51,7 +70,7 @@ function QuickActionBody({ action }: { action: QuickActionItem }) {
|
||||
<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">
|
||||
Перейти
|
||||
{resolveBadge(action)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
|
||||
@@ -62,6 +81,15 @@ function QuickActionBody({ action }: { action: QuickActionItem }) {
|
||||
)
|
||||
}
|
||||
|
||||
function panelClassName(disabled?: boolean) {
|
||||
return cn(
|
||||
'relative isolate flex h-full flex-col transition-colors',
|
||||
disabled
|
||||
? 'cursor-not-allowed opacity-60'
|
||||
: 'hover:bg-muted/40 focus-within:ring-ring cursor-pointer focus-within:ring-2',
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* KPI-like quick actions strip (horizontal Frame tiles).
|
||||
* Preview: https://reui.io/preview/base/stats-12
|
||||
@@ -83,21 +111,51 @@ export function QuickActionGrid({
|
||||
</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}`}
|
||||
{actions.map((action) => {
|
||||
const label = `${action.title}: ${action.description}`
|
||||
|
||||
if ('to' in action && action.to) {
|
||||
return (
|
||||
<FramePanel key={action.id} className={panelClassName(action.disabled)}>
|
||||
{action.disabled ? (
|
||||
<div aria-disabled aria-label={label}>
|
||||
<QuickActionBody action={action} />
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
to={action.to}
|
||||
search={action.search}
|
||||
className="focus-visible:outline-none"
|
||||
aria-label={label}
|
||||
>
|
||||
<QuickActionBody action={action} />
|
||||
</Link>
|
||||
)}
|
||||
</FramePanel>
|
||||
)
|
||||
}
|
||||
|
||||
const onClick = action.onClick
|
||||
const onActivate = () => {
|
||||
if (action.disabled || !onClick) return
|
||||
onClick()
|
||||
}
|
||||
|
||||
return (
|
||||
<FramePanel
|
||||
key={action.id}
|
||||
className={panelClassName(action.disabled)}
|
||||
role="button"
|
||||
tabIndex={action.disabled ? -1 : 0}
|
||||
aria-disabled={action.disabled || undefined}
|
||||
aria-label={label}
|
||||
onClick={onActivate}
|
||||
onKeyDown={(e) => handleActionKeyDown(onActivate, e)}
|
||||
>
|
||||
<QuickActionBody action={action} />
|
||||
</Link>
|
||||
</FramePanel>
|
||||
))}
|
||||
</FramePanel>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Frame>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user