refactor(web): improve layout and interactivity in dashboard components
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m1s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m44s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m1s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m44s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 6s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
- Removed unnecessary height classes from CertStatusChart and GroupDomainsChart for better responsiveness. - Enhanced KpiStatCardItem to improve interactivity with clearer button and link structures. - Added metrics logging in OpsDashboard for better tracking of layout performance. - Updated ChartContainer to ensure full width and height for responsive design. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -80,7 +80,7 @@ export function CertStatusChart({ data }: CertStatusChartProps) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame dense spacing="sm" className="h-full w-full">
|
<Frame dense spacing="sm" className="w-full">
|
||||||
<FrameHeader>
|
<FrameHeader>
|
||||||
<FrameTitle>Статусы сертификатов</FrameTitle>
|
<FrameTitle>Статусы сертификатов</FrameTitle>
|
||||||
<FrameDescription>Распределение по последней проверке</FrameDescription>
|
<FrameDescription>Распределение по последней проверке</FrameDescription>
|
||||||
@@ -162,7 +162,7 @@ export function GroupDomainsChart({ data }: GroupDomainsChartProps) {
|
|||||||
const chartRef = useChartSizeLog('group-domains', data.length)
|
const chartRef = useChartSizeLog('group-domains', data.length)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Frame dense spacing="sm" className="h-full w-full">
|
<Frame dense spacing="sm" className="w-full">
|
||||||
<FrameHeader>
|
<FrameHeader>
|
||||||
<FrameTitle>Домены по группам</FrameTitle>
|
<FrameTitle>Домены по группам</FrameTitle>
|
||||||
<FrameDescription>Топ-6 групп по количеству зон</FrameDescription>
|
<FrameDescription>Топ-6 групп по количеству зон</FrameDescription>
|
||||||
|
|||||||
@@ -42,36 +42,40 @@ function KpiStatCardBody({ card }: { card: KpiStatCard }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
function KpiStatCardItem({ card }: { card: KpiStatCard }) {
|
||||||
|
const interactive = Boolean(card.to || card.onSelect)
|
||||||
const panelClass = cn(
|
const panelClass = cn(
|
||||||
'relative isolate flex h-full flex-col gap-3',
|
'relative isolate flex flex-col gap-3',
|
||||||
card.selected && 'ring-primary/30 bg-muted/30 ring-1',
|
card.selected && 'ring-primary/30 bg-muted/30 ring-1',
|
||||||
|
interactive &&
|
||||||
|
'hover:bg-muted/40 focus-within:ring-ring transition-colors focus-within:ring-2',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// card-35: FramePanel — прямой child грида; ссылка/кнопка внутри панели
|
||||||
if (card.to) {
|
if (card.to) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<FramePanel className={panelClass}>
|
||||||
to={card.to}
|
<Link
|
||||||
search={card.search}
|
to={card.to}
|
||||||
className="hover:bg-muted/40 focus-visible:ring-ring rounded-[calc(var(--frame-radius)-2px)] transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
search={card.search}
|
||||||
>
|
className="focus-visible:outline-none"
|
||||||
<FramePanel className={panelClass}>
|
>
|
||||||
<KpiStatCardBody card={card} />
|
<KpiStatCardBody card={card} />
|
||||||
</FramePanel>
|
</Link>
|
||||||
</Link>
|
</FramePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.onSelect) {
|
if (card.onSelect) {
|
||||||
return (
|
return (
|
||||||
<button
|
<FramePanel className={panelClass}>
|
||||||
type="button"
|
<button
|
||||||
onClick={card.onSelect}
|
type="button"
|
||||||
className="hover:bg-muted/40 focus-visible:ring-ring rounded-[calc(var(--frame-radius)-2px)] text-start transition-colors focus-visible:ring-2 focus-visible:outline-none"
|
onClick={card.onSelect}
|
||||||
>
|
className="w-full text-start focus-visible:outline-none"
|
||||||
<FramePanel className={panelClass}>
|
>
|
||||||
<KpiStatCardBody card={card} />
|
<KpiStatCardBody card={card} />
|
||||||
</FramePanel>
|
</button>
|
||||||
</button>
|
</FramePanel>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,57 @@ export function OpsDashboard({
|
|||||||
},
|
},
|
||||||
'A',
|
'A',
|
||||||
)
|
)
|
||||||
|
// #region agent log
|
||||||
|
const kpi = el.querySelector('[aria-label="Ключевые метрики"]')
|
||||||
|
const analytics = el.querySelector('[aria-label="Аналитика"]')
|
||||||
|
const kpiFrame = kpi?.querySelector('[data-slot=frame]')
|
||||||
|
const kpiGrid = kpiFrame?.firstElementChild
|
||||||
|
const charts = analytics
|
||||||
|
? [...analytics.querySelectorAll('[data-slot=frame]')].map((f) => {
|
||||||
|
const panel = f.querySelector('[data-slot=frame-panel]')
|
||||||
|
const chart = f.querySelector('[data-slot=chart]')
|
||||||
|
const inner = chart?.querySelector(
|
||||||
|
'.recharts-responsive-container > div',
|
||||||
|
) as HTMLElement | null
|
||||||
|
return {
|
||||||
|
frameH: (f as HTMLElement).clientHeight,
|
||||||
|
panelH: (panel as HTMLElement | null)?.clientHeight ?? 0,
|
||||||
|
chartW: (chart as HTMLElement | null)?.clientWidth ?? 0,
|
||||||
|
chartH: (chart as HTMLElement | null)?.clientHeight ?? 0,
|
||||||
|
innerW: inner?.clientWidth ?? 0,
|
||||||
|
innerH: inner?.clientHeight ?? 0,
|
||||||
|
hasSvg: Boolean(f.querySelector('.recharts-surface')),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: []
|
||||||
|
fetch('http://127.0.0.1:7580/ingest/5c1b60ca-3f59-41ce-8435-d25bcc12c3cf', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Debug-Session-Id': '0f5088',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sessionId: '0f5088',
|
||||||
|
runId: 'post-fix',
|
||||||
|
location: 'ops-dashboard.tsx:layout',
|
||||||
|
message: 'dashboard layout metrics',
|
||||||
|
hypothesisId: 'A-C',
|
||||||
|
data: {
|
||||||
|
kpiPanelDirect:
|
||||||
|
kpiGrid != null &&
|
||||||
|
[...kpiGrid.children].every(
|
||||||
|
(c) => c.getAttribute('data-slot') === 'frame-panel',
|
||||||
|
),
|
||||||
|
kpiFirstChildTag: kpiGrid?.firstElementChild?.tagName ?? null,
|
||||||
|
analyticsAutoRows: analytics
|
||||||
|
? getComputedStyle(analytics).gridAutoRows
|
||||||
|
: null,
|
||||||
|
charts,
|
||||||
|
},
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}),
|
||||||
|
}).catch(() => {})
|
||||||
|
// #endregion
|
||||||
}, [isLoading])
|
}, [isLoading])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -88,7 +139,7 @@ export function OpsDashboard({
|
|||||||
|
|
||||||
<section
|
<section
|
||||||
aria-label="Аналитика"
|
aria-label="Аналитика"
|
||||||
className="grid min-w-0 auto-rows-fr items-start gap-2 @3xl:grid-cols-2"
|
className="grid min-w-0 items-start gap-2 @3xl:grid-cols-2"
|
||||||
>
|
>
|
||||||
{charts}
|
{charts}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -65,13 +65,17 @@ function ChartContainer({
|
|||||||
data-slot="chart"
|
data-slot="chart"
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
// relative + block: flex на контейнере даёт Recharts ResponsiveContainer 0×0
|
||||||
|
"relative block w-full text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChartStyle id={chartId} config={config} />
|
<ChartStyle id={chartId} config={config} />
|
||||||
<RechartsPrimitive.ResponsiveContainer
|
<RechartsPrimitive.ResponsiveContainer
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
minWidth={0}
|
||||||
initialDimension={initialDimension}
|
initialDimension={initialDimension}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user