refactor(web): enhance OpsDashboard and ChartContainer for improved metrics logging and responsiveness
Build, Test, and Push CFDM Docker Image / test (push) Successful in 3m46s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m56s
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 3m46s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 1m56s
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
- Refactored OpsDashboard to encapsulate layout metrics logging in a dedicated function, improving code clarity and maintainability. - Updated metrics logging to include SVG dimensions for better analytics. - Enhanced ChartContainer to utilize ResizeObserver for dynamic sizing, ensuring charts render correctly within their containers. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -63,57 +63,68 @@ export function OpsDashboard({
|
|||||||
'A',
|
'A',
|
||||||
)
|
)
|
||||||
// #region agent log
|
// #region agent log
|
||||||
const kpi = el.querySelector('[aria-label="Ключевые метрики"]')
|
const logLayout = (runId: string) => {
|
||||||
const analytics = el.querySelector('[aria-label="Аналитика"]')
|
const kpi = el.querySelector('[aria-label="Ключевые метрики"]')
|
||||||
const kpiFrame = kpi?.querySelector('[data-slot=frame]')
|
const analytics = el.querySelector('[aria-label="Аналитика"]')
|
||||||
const kpiGrid = kpiFrame?.firstElementChild
|
const kpiFrame = kpi?.querySelector('[data-slot=frame]')
|
||||||
const charts = analytics
|
const kpiGrid = kpiFrame?.firstElementChild
|
||||||
? [...analytics.querySelectorAll('[data-slot=frame]')].map((f) => {
|
const charts = analytics
|
||||||
const panel = f.querySelector('[data-slot=frame-panel]')
|
? [...analytics.querySelectorAll('[data-slot=frame]')].map((f) => {
|
||||||
const chart = f.querySelector('[data-slot=chart]')
|
const panel = f.querySelector('[data-slot=frame-panel]')
|
||||||
const inner = chart?.querySelector(
|
const chart = f.querySelector('[data-slot=chart]')
|
||||||
'.recharts-responsive-container > div',
|
const inner = chart?.querySelector(
|
||||||
) as HTMLElement | null
|
'.recharts-responsive-container > div',
|
||||||
return {
|
) as HTMLElement | null
|
||||||
frameH: (f as HTMLElement).clientHeight,
|
const surface = f.querySelector(
|
||||||
panelH: (panel as HTMLElement | null)?.clientHeight ?? 0,
|
'.recharts-surface',
|
||||||
chartW: (chart as HTMLElement | null)?.clientWidth ?? 0,
|
) as SVGElement | null
|
||||||
chartH: (chart as HTMLElement | null)?.clientHeight ?? 0,
|
return {
|
||||||
innerW: inner?.clientWidth ?? 0,
|
frameH: (f as HTMLElement).clientHeight,
|
||||||
innerH: inner?.clientHeight ?? 0,
|
panelH: (panel as HTMLElement | null)?.clientHeight ?? 0,
|
||||||
hasSvg: Boolean(f.querySelector('.recharts-surface')),
|
chartW: (chart as HTMLElement | null)?.clientWidth ?? 0,
|
||||||
}
|
chartH: (chart as HTMLElement | null)?.clientHeight ?? 0,
|
||||||
})
|
innerW: inner?.clientWidth ?? 0,
|
||||||
: []
|
innerH: inner?.clientHeight ?? 0,
|
||||||
fetch('http://127.0.0.1:7580/ingest/5c1b60ca-3f59-41ce-8435-d25bcc12c3cf', {
|
hasSvg: Boolean(surface),
|
||||||
method: 'POST',
|
svgW: surface?.clientWidth ?? 0,
|
||||||
headers: {
|
svgH: surface?.clientHeight ?? 0,
|
||||||
'Content-Type': 'application/json',
|
}
|
||||||
'X-Debug-Session-Id': '0f5088',
|
})
|
||||||
},
|
: []
|
||||||
body: JSON.stringify({
|
fetch('http://127.0.0.1:7580/ingest/5c1b60ca-3f59-41ce-8435-d25bcc12c3cf', {
|
||||||
sessionId: '0f5088',
|
method: 'POST',
|
||||||
runId: 'post-fix',
|
headers: {
|
||||||
location: 'ops-dashboard.tsx:layout',
|
'Content-Type': 'application/json',
|
||||||
message: 'dashboard layout metrics',
|
'X-Debug-Session-Id': '0f5088',
|
||||||
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(),
|
body: JSON.stringify({
|
||||||
}),
|
sessionId: '0f5088',
|
||||||
}).catch(() => {})
|
runId,
|
||||||
|
location: 'ops-dashboard.tsx:layout',
|
||||||
|
message: 'dashboard layout metrics',
|
||||||
|
hypothesisId: '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(() => {})
|
||||||
|
}
|
||||||
|
logLayout('post-fix-2')
|
||||||
|
requestAnimationFrame(() =>
|
||||||
|
requestAnimationFrame(() => logLayout('post-fix-2-raf')),
|
||||||
|
)
|
||||||
// #endregion
|
// #endregion
|
||||||
}, [isLoading])
|
}, [isLoading, kpiCards])
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <OpsDashboardSkeleton />
|
return <OpsDashboardSkeleton />
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ function ChartContainer({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<"div"> & {
|
||||||
config: ChartConfig
|
config: ChartConfig
|
||||||
children: React.ComponentProps<
|
children: React.ReactNode
|
||||||
typeof RechartsPrimitive.ResponsiveContainer
|
|
||||||
>["children"]
|
|
||||||
initialDimension?: {
|
initialDimension?: {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
@@ -58,28 +56,53 @@ function ChartContainer({
|
|||||||
}) {
|
}) {
|
||||||
const uniqueId = React.useId()
|
const uniqueId = React.useId()
|
||||||
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`
|
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`
|
||||||
|
const containerRef = React.useRef<HTMLDivElement>(null)
|
||||||
|
const [size, setSize] = React.useState(initialDimension)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const el = containerRef.current
|
||||||
|
if (!el || typeof ResizeObserver === "undefined") return
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
const width = Math.max(1, Math.floor(el.clientWidth))
|
||||||
|
const height = Math.max(1, Math.floor(el.clientHeight))
|
||||||
|
setSize((prev) =>
|
||||||
|
prev.width === width && prev.height === height
|
||||||
|
? prev
|
||||||
|
: { width, height },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
const ro = new ResizeObserver(update)
|
||||||
|
ro.observe(el)
|
||||||
|
return () => ro.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Recharts 3 ResponsiveContainer часто оставляет inner 0×0 (см. debug dashboard).
|
||||||
|
// Рендерим chart с явными width/height по размеру контейнера.
|
||||||
|
const sizedChildren = React.Children.map(children, (child) => {
|
||||||
|
if (!React.isValidElement(child)) return child
|
||||||
|
return React.cloneElement(
|
||||||
|
child as React.ReactElement<{ width?: number; height?: number }>,
|
||||||
|
{ width: size.width, height: size.height },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChartContext.Provider value={{ config }}>
|
<ChartContext.Provider value={{ config }}>
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
data-slot="chart"
|
data-slot="chart"
|
||||||
data-chart={chartId}
|
data-chart={chartId}
|
||||||
className={cn(
|
className={cn(
|
||||||
// relative + block: flex на контейнере даёт Recharts ResponsiveContainer 0×0
|
"relative block w-full min-h-0 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 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
|
{sizedChildren}
|
||||||
width="100%"
|
|
||||||
height="100%"
|
|
||||||
minWidth={0}
|
|
||||||
initialDimension={initialDimension}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</ChartContext.Provider>
|
</ChartContext.Provider>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user