@flanksource/clicky-ui 0.3.38 → 0.3.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/data/TimeseriesCoreBars.cjs +10 -36
- package/dist/data/TimeseriesCoreBars.cjs.map +1 -1
- package/dist/data/TimeseriesCoreBars.d.ts +1 -1
- package/dist/data/TimeseriesCoreBars.d.ts.map +1 -1
- package/dist/data/TimeseriesCoreBars.js +8 -34
- package/dist/data/TimeseriesCoreBars.js.map +1 -1
- package/dist/data/TimeseriesGauge.cjs +12 -36
- package/dist/data/TimeseriesGauge.cjs.map +1 -1
- package/dist/data/TimeseriesGauge.d.ts +14 -4
- package/dist/data/TimeseriesGauge.d.ts.map +1 -1
- package/dist/data/TimeseriesGauge.js +10 -34
- package/dist/data/TimeseriesGauge.js.map +1 -1
- package/dist/data/TimeseriesPanel.cjs +82 -39
- package/dist/data/TimeseriesPanel.cjs.map +1 -1
- package/dist/data/TimeseriesPanel.d.ts +1 -1
- package/dist/data/TimeseriesPanel.d.ts.map +1 -1
- package/dist/data/TimeseriesPanel.js +82 -39
- package/dist/data/TimeseriesPanel.js.map +1 -1
- package/dist/data/TimeseriesPanel.model.cjs +1 -0
- package/dist/data/TimeseriesPanel.model.cjs.map +1 -1
- package/dist/data/TimeseriesPanel.model.d.ts +38 -2
- package/dist/data/TimeseriesPanel.model.d.ts.map +1 -1
- package/dist/data/TimeseriesPanel.model.js +1 -0
- package/dist/data/TimeseriesPanel.model.js.map +1 -1
- package/dist/data/WorkloadCard.cjs +65 -31
- package/dist/data/WorkloadCard.cjs.map +1 -1
- package/dist/data/WorkloadCard.d.ts +15 -5
- package/dist/data/WorkloadCard.d.ts.map +1 -1
- package/dist/data/WorkloadCard.js +67 -33
- package/dist/data/WorkloadCard.js.map +1 -1
- package/dist/data/WorkloadCard.model.cjs +16 -10
- package/dist/data/WorkloadCard.model.cjs.map +1 -1
- package/dist/data/WorkloadCard.model.d.ts +29 -1
- package/dist/data/WorkloadCard.model.d.ts.map +1 -1
- package/dist/data/WorkloadCard.model.js +17 -11
- package/dist/data/WorkloadCard.model.js.map +1 -1
- package/dist/data/timeseries-query.cjs +51 -0
- package/dist/data/timeseries-query.cjs.map +1 -0
- package/dist/data/timeseries-query.d.ts +31 -0
- package/dist/data/timeseries-query.d.ts.map +1 -0
- package/dist/data/timeseries-query.js +51 -0
- package/dist/data/timeseries-query.js.map +1 -0
- package/dist/data/version-info.cjs +3 -3
- package/dist/data/version-info.js +3 -3
- package/dist/data.cjs +1 -0
- package/dist/data.cjs.map +1 -1
- package/dist/data.d.ts +2 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +3 -2
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeseriesGauge.js","sources":["../../src/data/TimeseriesGauge.tsx"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { formatUnit } from \"../lib/format\";\nimport { Modal } from \"../overlay/Modal\";\nimport { UiFullscreen } from \"../icons\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport {\n TimeseriesPanel,\n type TimeseriesResponse,\n type TimeseriesSeries,\n} from \"./TimeseriesPanel\";\nimport { GaugeHoverCard, type GaugeHoverRow } from \"./GaugeHoverCard\";\nimport { seriesStats } from \"./gauge-stats\";\n\n/** A metric series whose latest value drives the gauge fill or its maximum. */\nexport interface GaugeSeries {\n /** Metric id appended to the gauge's baseUrl, e.g. \"k8s.statefulset.cycle.cpu.usage\". */\n id: string;\n /** Maps the latest value before display (e.g. unit scaling). Identity when omitted. */\n transform?: (value: number) => number;\n}\n\nexport type TimeseriesGaugeVariant = \"default\" | \"cell\";\n/** Gauge fill geometry: a half-circle arc (default) or a horizontal progress bar. */\nexport type TimeseriesGaugeShape = \"radial\" | \"linear\";\n\nexport interface TimeseriesGaugeProps {\n /** Common prefix; the value/max requests are `baseUrl + id`. */\n baseUrl?: string;\n /** The metric whose latest value fills the gauge. */\n value: GaugeSeries;\n /**\n * The gauge maximum. Either a metric series (its latest value, e.g. a `.limit`\n * metric) or a fixed number. When omitted, or when the resolved max is 0, the\n * gauge shows the raw value with no fill (an unbounded reading).\n */\n max?: GaugeSeries | number;\n title: string;\n /** Iconify name or static icon component shown beside the label. */\n icon?: string | StaticIconComponent;\n /** Grafana-style unit key for the readout (e.g. \"bytes\", \"percent\", \"short\", \"ms\"). */\n unit?: string;\n /** Look-back window for the expand chart, passed as ?since=; defaults to \"1h\". */\n range?: string;\n /** Poll interval in ms; defaults to 5000. Pass 0 to disable polling. */\n refreshMs?: number;\n /** Show an expand button that opens the value/max time-series chart in a modal. Default true. */\n expandable?: boolean;\n /**\n * Utilisation thresholds (percent of max) at which the arc turns amber then\n * red. Defaults to [75, 90].\n */\n thresholds?: [warning: number, danger: number];\n /**\n * What to print in the centre: the formatted value (default) or the\n * utilisation percentage of max (e.g. CPU usage out of its millicore limit).\n */\n centerDisplay?: \"value\" | \"percent\";\n /** Visual density/layout. `cell` is a compact inline form for table/grid cells. */\n variant?: TimeseriesGaugeVariant;\n /** Fill geometry: a radial half-gauge (default) or a horizontal progress bar. */\n shape?: TimeseriesGaugeShape;\n /** Show the title text. Icons and values remain visible when false. */\n showLabel?: boolean;\n /**\n * Wrap the gauge in a hover card summarising the metric (label + current / min /\n * max / avg / capacity over the window). Defaults to true.\n */\n hoverCard?: boolean;\n /** Override the default fetch (e.g. to route through an app's API client). */\n fetcher?: (url: string) => Promise<TimeseriesResponse>;\n className?: string;\n}\n\nconst defaultFetcher = async (url: string): Promise<TimeseriesResponse> => {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);\n return res.json();\n};\n\n// Half-gauge SVG geometry: a 100×50 viewBox semicircle (radius 40, centre 50,50)\n// swept 180°. The arc length is π·r; stroke-dashoffset reveals value/max of it.\nconst GAUGE_RADIUS = 40;\nconst GAUGE_ARC = Math.PI * GAUGE_RADIUS;\nconst GAUGE_PATH = `M 10 50 A ${GAUGE_RADIUS} ${GAUGE_RADIUS} 0 0 1 90 50`;\n\nfunction toneClass(pct: number, [warn, danger]: [number, number]): string {\n if (pct >= danger) return \"text-red-500\";\n if (pct >= warn) return \"text-amber-500\";\n return \"text-emerald-500\";\n}\n\n// toneBgClass mirrors toneClass for the linear bar's fill, which needs a\n// background colour rather than the arc's stroke (currentColor) tone.\nfunction toneBgClass(pct: number, [warn, danger]: [number, number]): string {\n if (pct >= danger) return \"bg-red-500\";\n if (pct >= warn) return \"bg-amber-500\";\n return \"bg-emerald-500\";\n}\n\n// GaugeArc is the half-circle SVG shared by the cell and default radial gauges;\n// only the stroke width differs between the two densities.\nfunction GaugeArc({ pct, tone, strokeWidth }: { pct: number; tone: string; strokeWidth: number }) {\n return (\n <svg viewBox=\"0 0 100 50\" className=\"h-full w-full overflow-visible\">\n <path d={GAUGE_PATH} fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" className=\"text-muted\" />\n <path\n d={GAUGE_PATH}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n className={tone}\n strokeDasharray={GAUGE_ARC}\n strokeDashoffset={GAUGE_ARC * (1 - pct / 100)}\n />\n </svg>\n );\n}\n\n// GaugeBar is the horizontal progress track shared by the cell and default linear\n// gauges; the caller sizes it via className (e.g. \"h-1.5 w-12\" or \"h-2 w-full\").\nfunction GaugeBar({ pct, toneBg, className }: { pct: number; toneBg: string; className?: string }) {\n return (\n <span className={cn(\"relative block overflow-hidden rounded-full bg-muted\", className)} aria-hidden=\"true\">\n <span\n className={cn(\"absolute inset-y-0 left-0 rounded-full transition-all duration-300\", toneBg)}\n style={{ width: `${pct}%` }}\n />\n </span>\n );\n}\n\nfunction latestValue(resp: TimeseriesResponse | undefined): number | undefined {\n const points = resp?.points;\n if (!points || points.length === 0) return undefined;\n return points[points.length - 1]?.value;\n}\n\nfunction GaugeIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n}\n\n/**\n * TimeseriesGauge renders a half (180°) radial gauge whose fill is the latest\n * value of a metric over its maximum (a `.limit`-style metric or a fixed number),\n * both read live from the timeseries store. The centre shows the utilisation\n * percentage, with the value/max caption below; the arc colour crosses warning\n * and danger thresholds. An expand button opens the full value/max time-series\n * chart in a modal (a `TimeseriesPanel`), so the gauge gives the at-a-glance\n * reading and the chart the trend.\n */\nexport function TimeseriesGauge({\n baseUrl = \"\",\n value,\n max,\n title,\n icon,\n unit,\n range = \"1h\",\n refreshMs = 5000,\n expandable = true,\n thresholds = [75, 90],\n centerDisplay = \"value\",\n variant = \"default\",\n shape = \"radial\",\n showLabel = true,\n hoverCard = true,\n fetcher = defaultFetcher,\n className,\n}: TimeseriesGaugeProps) {\n const [expanded, setExpanded] = useState(false);\n\n const maxIsSeries = typeof max === \"object\";\n const maxSeries = maxIsSeries ? max : undefined;\n const ids = useMemo(() => {\n const list = [value.id];\n if (maxSeries) list.push(maxSeries.id);\n return list;\n }, [value.id, maxSeries]);\n\n const results = useQueries({\n queries: ids.map((id) => {\n const u = new URL(baseUrl + id, window.location.origin);\n if (range) u.searchParams.set(\"since\", range);\n const requestUrl = u.pathname + u.search;\n return {\n queryKey: [\"timeseries\", requestUrl],\n queryFn: () => fetcher(requestUrl),\n refetchInterval: refreshMs > 0 ? refreshMs : false,\n staleTime: 0,\n retry: 0,\n };\n }),\n });\n\n const rawValue = latestValue(results[0]?.data);\n const hasValue = rawValue !== undefined;\n const usage = value.transform ? value.transform(rawValue ?? 0) : (rawValue ?? 0);\n\n let limit: number | undefined;\n if (typeof max === \"number\") {\n limit = max;\n } else if (maxSeries) {\n const rawMax = latestValue(results[1]?.data);\n limit = rawMax === undefined ? undefined : maxSeries.transform ? maxSeries.transform(rawMax) : rawMax;\n }\n\n const bounded = hasValue && limit !== undefined && limit > 0;\n const pct = bounded ? Math.min(100, Math.round((usage / (limit as number)) * 100)) : 0;\n const tone = toneClass(pct, thresholds);\n const toneBg = toneBgClass(pct, thresholds);\n const readout =\n centerDisplay === \"percent\"\n ? bounded\n ? `${pct}%`\n : hasValue\n ? \"n/a\"\n : \"—\"\n : hasValue\n ? formatUnit(usage, unit)\n : \"—\";\n const titleText = `${title}: ${readout}`;\n const canExpand = expandable && variant === \"default\";\n const readoutTone = bounded ? tone : \"text-foreground\";\n\n const chartSeries: TimeseriesSeries[] = useMemo(() => {\n const s: TimeseriesSeries[] = [{ id: value.id, label: \"value\", ...(value.transform ? { transform: value.transform } : {}) }];\n if (maxSeries) s.push({ id: maxSeries.id, label: \"max\", ...(maxSeries.transform ? { transform: maxSeries.transform } : {}) });\n return s;\n }, [value.id, value.transform, maxSeries]);\n\n // Hover card: reduce the value's points to current/min/max/avg (transformed to\n // the display unit), plus the resolved capacity. The current row is tinted by\n // its threshold tone when the reading is bounded.\n const stats = seriesStats(results[0]?.data?.points, value.transform);\n const fmt = (n: number) => formatUnit(n, unit);\n const hoverRows: GaugeHoverRow[] = hasValue\n ? [\n { label: \"Current\", value: fmt(usage), ...(bounded ? { tone } : {}) },\n ...(stats\n ? [\n { label: \"Min\", value: fmt(stats.min) },\n { label: \"Max\", value: fmt(stats.max) },\n { label: \"Avg\", value: fmt(stats.avg) },\n ]\n : []),\n ...(limit !== undefined\n ? [{ label: \"Capacity\", value: fmt(limit) }]\n : []),\n ]\n : [];\n const useHover = hoverCard && hoverRows.length > 0;\n const footer = `over last ${range}`;\n\n if (variant === \"cell\") {\n const cell = (\n <span\n className={cn(\n \"inline-flex min-w-0 items-center gap-1.5 whitespace-nowrap align-middle text-xs\",\n className,\n )}\n {...(useHover ? {} : { title: titleText })}\n aria-label={useHover || !showLabel ? titleText : undefined}\n data-shape={shape}\n >\n {icon ? <GaugeIcon icon={icon} /> : null}\n {showLabel ? <span className=\"min-w-0 truncate text-muted-foreground\">{title}</span> : null}\n {shape === \"linear\" ? (\n <GaugeBar pct={pct} toneBg={toneBg} className=\"h-1.5 w-12 shrink-0\" />\n ) : (\n <span className=\"relative h-4 w-8 shrink-0\" aria-hidden=\"true\">\n <GaugeArc pct={pct} tone={tone} strokeWidth={10} />\n </span>\n )}\n <span className={cn(\"shrink-0 font-semibold tabular-nums\", readoutTone)}>\n {readout}\n </span>\n </span>\n );\n return useHover ? (\n <GaugeHoverCard title={title} rows={hoverRows} footer={footer} trigger={cell} />\n ) : (\n cell\n );\n }\n\n const body = (\n <div\n className={cn(\"flex flex-col items-center gap-1\", className)}\n {...(useHover ? {} : { title: !showLabel ? titleText : undefined })}\n aria-label={useHover || !showLabel ? titleText : undefined}\n data-shape={shape}\n >\n {shape === \"linear\" ? (\n <div className=\"flex w-full items-center gap-2\">\n <GaugeBar pct={pct} toneBg={toneBg} className=\"h-2 w-full\" />\n <span className=\"shrink-0 text-sm font-semibold text-foreground\">{readout}</span>\n {canExpand && (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"shrink-0 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={12} height={12} />\n </button>\n )}\n </div>\n ) : (\n <div className=\"relative h-10 w-20\">\n <GaugeArc pct={pct} tone={tone} strokeWidth={9} />\n <span className=\"absolute inset-x-0 bottom-0 text-center text-sm font-semibold text-foreground\">\n {readout}\n </span>\n {canExpand && (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"absolute right-0 top-0 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={12} height={12} />\n </button>\n )}\n </div>\n )}\n {icon || showLabel ? (\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n {icon ? <GaugeIcon icon={icon} /> : null}\n {showLabel ? <span>{title}</span> : null}\n </div>\n ) : null}\n </div>\n );\n\n return (\n <>\n {useHover ? (\n <GaugeHoverCard\n title={title}\n rows={hoverRows}\n footer={footer}\n trigger={body}\n className=\"w-full justify-center\"\n />\n ) : (\n body\n )}\n {canExpand && (\n <Modal open={expanded} onClose={() => setExpanded(false)} title={title} size=\"xl\">\n <TimeseriesPanel\n title={title}\n {...(icon ? { icon } : {})}\n {...(unit ? { unit } : {})}\n baseUrl={baseUrl}\n series={chartSeries}\n range={range}\n refreshMs={refreshMs}\n expandable={false}\n fetcher={fetcher}\n />\n </Modal>\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TimeseriesGauge.js","sources":["../../src/data/TimeseriesGauge.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { formatUnit } from \"../lib/format\";\nimport { Modal } from \"../overlay/Modal\";\nimport { UiFullscreen } from \"../icons\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport {\n TimeseriesPanel,\n type SeriesLoader,\n type TimeseriesResponse,\n type TimeseriesSeries,\n} from \"./TimeseriesPanel\";\nimport { defaultTimeseriesFetcher, latestValue, useTimeseriesQueries } from \"./timeseries-query\";\nimport { GaugeHoverCard, type GaugeHoverRow } from \"./GaugeHoverCard\";\nimport { seriesStats } from \"./gauge-stats\";\n\n/**\n * A metric series whose latest value drives the gauge fill or its maximum. It\n * is requested as `baseUrl + id` through the widget's `fetcher`, or, when\n * `load` is set, loaded by calling `load` directly.\n */\nexport interface GaugeSeries {\n /**\n * Metric id appended to the gauge's baseUrl, e.g. \"k8s.statefulset.cycle.cpu.usage\".\n * With `load`, the id is the cache identity: loaded series are cached under\n * `[\"timeseries\", \"load\", id, range]`, so keep it unique per data source.\n */\n id: string;\n /** Maps the latest value before display (e.g. unit scaling). Identity when omitted. */\n transform?: (value: number) => number;\n /** Loads the series' points; when set, `baseUrl` and `fetcher` are not used. */\n load?: SeriesLoader;\n}\n\nexport type TimeseriesGaugeVariant = \"default\" | \"cell\";\n/** Gauge fill geometry: a half-circle arc (default) or a horizontal progress bar. */\nexport type TimeseriesGaugeShape = \"radial\" | \"linear\";\n\nexport interface TimeseriesGaugeProps {\n /** Common prefix; the value/max requests are `baseUrl + id` (unless a series has `load`). */\n baseUrl?: string;\n /** The metric whose latest value fills the gauge. */\n value: GaugeSeries;\n /**\n * The gauge maximum. Either a metric series (its latest value, e.g. a `.limit`\n * metric) or a fixed number. When omitted, or when the resolved max is 0, the\n * gauge shows the raw value with no fill (an unbounded reading).\n */\n max?: GaugeSeries | number;\n title: string;\n /** Iconify name or static icon component shown beside the label. */\n icon?: string | StaticIconComponent;\n /** Grafana-style unit key for the readout (e.g. \"bytes\", \"percent\", \"short\", \"ms\"). */\n unit?: string;\n /** Look-back window for the expand chart, passed as ?since=; defaults to \"1h\". */\n range?: string;\n /** Poll interval in ms; defaults to 5000. Pass 0 to disable polling. */\n refreshMs?: number;\n /** Show an expand button that opens the value/max time-series chart in a modal. Default true. */\n expandable?: boolean;\n /**\n * Utilisation thresholds (percent of max) at which the arc turns amber then\n * red. Defaults to [75, 90].\n */\n thresholds?: [warning: number, danger: number];\n /**\n * What to print in the centre: the formatted value (default) or the\n * utilisation percentage of max (e.g. CPU usage out of its millicore limit).\n */\n centerDisplay?: \"value\" | \"percent\";\n /** Visual density/layout. `cell` is a compact inline form for table/grid cells. */\n variant?: TimeseriesGaugeVariant;\n /** Fill geometry: a radial half-gauge (default) or a horizontal progress bar. */\n shape?: TimeseriesGaugeShape;\n /** Show the title text. Icons and values remain visible when false. */\n showLabel?: boolean;\n /**\n * Wrap the gauge in a hover card summarising the metric (label + current / min /\n * max / avg / capacity over the window). Defaults to true.\n */\n hoverCard?: boolean;\n /** Override the default fetch (e.g. to route through an app's API client). */\n fetcher?: (url: string) => Promise<TimeseriesResponse>;\n className?: string;\n}\n\n// Half-gauge SVG geometry: a 100×50 viewBox semicircle (radius 40, centre 50,50)\n// swept 180°. The arc length is π·r; stroke-dashoffset reveals value/max of it.\nconst GAUGE_RADIUS = 40;\nconst GAUGE_ARC = Math.PI * GAUGE_RADIUS;\nconst GAUGE_PATH = `M 10 50 A ${GAUGE_RADIUS} ${GAUGE_RADIUS} 0 0 1 90 50`;\n\nfunction toneClass(pct: number, [warn, danger]: [number, number]): string {\n if (pct >= danger) return \"text-red-500\";\n if (pct >= warn) return \"text-amber-500\";\n return \"text-emerald-500\";\n}\n\n// toneBgClass mirrors toneClass for the linear bar's fill, which needs a\n// background colour rather than the arc's stroke (currentColor) tone.\nfunction toneBgClass(pct: number, [warn, danger]: [number, number]): string {\n if (pct >= danger) return \"bg-red-500\";\n if (pct >= warn) return \"bg-amber-500\";\n return \"bg-emerald-500\";\n}\n\n// GaugeArc is the half-circle SVG shared by the cell and default radial gauges;\n// only the stroke width differs between the two densities.\nfunction GaugeArc({ pct, tone, strokeWidth }: { pct: number; tone: string; strokeWidth: number }) {\n return (\n <svg viewBox=\"0 0 100 50\" className=\"h-full w-full overflow-visible\">\n <path d={GAUGE_PATH} fill=\"none\" stroke=\"currentColor\" strokeWidth={strokeWidth} strokeLinecap=\"round\" className=\"text-muted\" />\n <path\n d={GAUGE_PATH}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={strokeWidth}\n strokeLinecap=\"round\"\n className={tone}\n strokeDasharray={GAUGE_ARC}\n strokeDashoffset={GAUGE_ARC * (1 - pct / 100)}\n />\n </svg>\n );\n}\n\n// GaugeBar is the horizontal progress track shared by the cell and default linear\n// gauges; the caller sizes it via className (e.g. \"h-1.5 w-12\" or \"h-2 w-full\").\nfunction GaugeBar({ pct, toneBg, className }: { pct: number; toneBg: string; className?: string }) {\n return (\n <span className={cn(\"relative block overflow-hidden rounded-full bg-muted\", className)} aria-hidden=\"true\">\n <span\n className={cn(\"absolute inset-y-0 left-0 rounded-full transition-all duration-300\", toneBg)}\n style={{ width: `${pct}%` }}\n />\n </span>\n );\n}\n\nfunction GaugeIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={14} height={14} className=\"text-muted-foreground\" />;\n}\n\n/**\n * TimeseriesGauge renders a half (180°) radial gauge whose fill is the latest\n * value of a metric over its maximum (a `.limit`-style metric or a fixed number),\n * both read live from the timeseries store. The centre shows the utilisation\n * percentage, with the value/max caption below; the arc colour crosses warning\n * and danger thresholds. An expand button opens the full value/max time-series\n * chart in a modal (a `TimeseriesPanel`), so the gauge gives the at-a-glance\n * reading and the chart the trend.\n */\nexport function TimeseriesGauge({\n baseUrl = \"\",\n value,\n max,\n title,\n icon,\n unit,\n range = \"1h\",\n refreshMs = 5000,\n expandable = true,\n thresholds = [75, 90],\n centerDisplay = \"value\",\n variant = \"default\",\n shape = \"radial\",\n showLabel = true,\n hoverCard = true,\n fetcher = defaultTimeseriesFetcher,\n className,\n}: TimeseriesGaugeProps) {\n const [expanded, setExpanded] = useState(false);\n\n const maxIsSeries = typeof max === \"object\";\n const maxSeries = maxIsSeries ? max : undefined;\n const results = useTimeseriesQueries(maxSeries ? [value, maxSeries] : [value], {\n baseUrl,\n range,\n refreshMs,\n fetcher,\n });\n\n const rawValue = latestValue(results[0]?.data);\n const hasValue = rawValue !== undefined;\n const usage = value.transform ? value.transform(rawValue ?? 0) : (rawValue ?? 0);\n\n let limit: number | undefined;\n if (typeof max === \"number\") {\n limit = max;\n } else if (maxSeries) {\n const rawMax = latestValue(results[1]?.data);\n limit = rawMax === undefined ? undefined : maxSeries.transform ? maxSeries.transform(rawMax) : rawMax;\n }\n\n const bounded = hasValue && limit !== undefined && limit > 0;\n const pct = bounded ? Math.min(100, Math.round((usage / (limit as number)) * 100)) : 0;\n const tone = toneClass(pct, thresholds);\n const toneBg = toneBgClass(pct, thresholds);\n const readout =\n centerDisplay === \"percent\"\n ? bounded\n ? `${pct}%`\n : hasValue\n ? \"n/a\"\n : \"—\"\n : hasValue\n ? formatUnit(usage, unit)\n : \"—\";\n const titleText = `${title}: ${readout}`;\n const canExpand = expandable && variant === \"default\";\n const readoutTone = bounded ? tone : \"text-foreground\";\n\n const chartSeries: TimeseriesSeries[] = useMemo(() => {\n const s: TimeseriesSeries[] = [{ ...value, label: \"value\" }];\n if (maxSeries) s.push({ ...maxSeries, label: \"max\" });\n return s;\n }, [value, maxSeries]);\n\n // Hover card: reduce the value's points to current/min/max/avg (transformed to\n // the display unit), plus the resolved capacity. The current row is tinted by\n // its threshold tone when the reading is bounded.\n const stats = seriesStats(results[0]?.data?.points, value.transform);\n const fmt = (n: number) => formatUnit(n, unit);\n const hoverRows: GaugeHoverRow[] = hasValue\n ? [\n { label: \"Current\", value: fmt(usage), ...(bounded ? { tone } : {}) },\n ...(stats\n ? [\n { label: \"Min\", value: fmt(stats.min) },\n { label: \"Max\", value: fmt(stats.max) },\n { label: \"Avg\", value: fmt(stats.avg) },\n ]\n : []),\n ...(limit !== undefined\n ? [{ label: \"Capacity\", value: fmt(limit) }]\n : []),\n ]\n : [];\n const useHover = hoverCard && hoverRows.length > 0;\n const footer = `over last ${range}`;\n\n if (variant === \"cell\") {\n const cell = (\n <span\n className={cn(\n \"inline-flex min-w-0 items-center gap-1.5 whitespace-nowrap align-middle text-xs\",\n className,\n )}\n {...(useHover ? {} : { title: titleText })}\n aria-label={useHover || !showLabel ? titleText : undefined}\n data-shape={shape}\n >\n {icon ? <GaugeIcon icon={icon} /> : null}\n {showLabel ? <span className=\"min-w-0 truncate text-muted-foreground\">{title}</span> : null}\n {shape === \"linear\" ? (\n <GaugeBar pct={pct} toneBg={toneBg} className=\"h-1.5 w-12 shrink-0\" />\n ) : (\n <span className=\"relative h-4 w-8 shrink-0\" aria-hidden=\"true\">\n <GaugeArc pct={pct} tone={tone} strokeWidth={10} />\n </span>\n )}\n <span className={cn(\"shrink-0 font-semibold tabular-nums\", readoutTone)}>\n {readout}\n </span>\n </span>\n );\n return useHover ? (\n <GaugeHoverCard title={title} rows={hoverRows} footer={footer} trigger={cell} />\n ) : (\n cell\n );\n }\n\n const body = (\n <div\n className={cn(\"flex flex-col items-center gap-1\", className)}\n {...(useHover ? {} : { title: !showLabel ? titleText : undefined })}\n aria-label={useHover || !showLabel ? titleText : undefined}\n data-shape={shape}\n >\n {shape === \"linear\" ? (\n <div className=\"flex w-full items-center gap-2\">\n <GaugeBar pct={pct} toneBg={toneBg} className=\"h-2 w-full\" />\n <span className=\"shrink-0 text-sm font-semibold text-foreground\">{readout}</span>\n {canExpand && (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"shrink-0 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={12} height={12} />\n </button>\n )}\n </div>\n ) : (\n <div className=\"relative h-10 w-20\">\n <GaugeArc pct={pct} tone={tone} strokeWidth={9} />\n <span className=\"absolute inset-x-0 bottom-0 text-center text-sm font-semibold text-foreground\">\n {readout}\n </span>\n {canExpand && (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"absolute right-0 top-0 text-muted-foreground hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={12} height={12} />\n </button>\n )}\n </div>\n )}\n {icon || showLabel ? (\n <div className=\"flex items-center gap-1 text-xs text-muted-foreground\">\n {icon ? <GaugeIcon icon={icon} /> : null}\n {showLabel ? <span>{title}</span> : null}\n </div>\n ) : null}\n </div>\n );\n\n return (\n <>\n {useHover ? (\n <GaugeHoverCard\n title={title}\n rows={hoverRows}\n footer={footer}\n trigger={body}\n className=\"w-full justify-center\"\n />\n ) : (\n body\n )}\n {canExpand && (\n <Modal open={expanded} onClose={() => setExpanded(false)} title={title} size=\"xl\">\n <TimeseriesPanel\n title={title}\n {...(icon ? { icon } : {})}\n {...(unit ? { unit } : {})}\n baseUrl={baseUrl}\n series={chartSeries}\n range={range}\n refreshMs={refreshMs}\n expandable={false}\n fetcher={fetcher}\n />\n </Modal>\n )}\n </>\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const jsxRuntime = require("react/jsx-runtime");
|
|
4
|
-
const reactQuery = require("@tanstack/react-query");
|
|
5
4
|
const react = require("react");
|
|
6
5
|
const recharts = require("recharts");
|
|
7
6
|
const utils = require("../lib/utils.cjs");
|
|
@@ -12,12 +11,8 @@ const ProgressBar = require("./ProgressBar.cjs");
|
|
|
12
11
|
const Icon = require("./Icon.cjs");
|
|
13
12
|
const TimeseriesPanel_axes = require("./TimeseriesPanel.axes.cjs");
|
|
14
13
|
const TimeseriesPanel_model = require("./TimeseriesPanel.model.cjs");
|
|
14
|
+
const timeseriesQuery = require("./timeseries-query.cjs");
|
|
15
15
|
const UiFullscreen = require("../icons/components/UiFullscreen.cjs");
|
|
16
|
-
const defaultFetcher = async (url) => {
|
|
17
|
-
const res = await fetch(url);
|
|
18
|
-
if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);
|
|
19
|
-
return res.json();
|
|
20
|
-
};
|
|
21
16
|
function formatClock(at) {
|
|
22
17
|
const date = new Date(at);
|
|
23
18
|
if (Number.isNaN(date.getTime())) return "";
|
|
@@ -35,7 +30,8 @@ function TimeseriesPanel(props) {
|
|
|
35
30
|
variant = "area",
|
|
36
31
|
expandable = true,
|
|
37
32
|
total,
|
|
38
|
-
|
|
33
|
+
referenceLines,
|
|
34
|
+
fetcher = timeseriesQuery.defaultTimeseriesFetcher,
|
|
39
35
|
className
|
|
40
36
|
} = props;
|
|
41
37
|
const isBreakdown = variant === "breakdown";
|
|
@@ -43,23 +39,11 @@ function TimeseriesPanel(props) {
|
|
|
43
39
|
const expandVariant = props.expandVariant ?? chartVariant;
|
|
44
40
|
const [expanded, setExpanded] = react.useState(false);
|
|
45
41
|
const series = react.useMemo(() => TimeseriesPanel_model.resolveSeries(props), [props]);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (range) u.searchParams.set("since", range);
|
|
50
|
-
return u.pathname + u.search;
|
|
51
|
-
}),
|
|
52
|
-
[series, range]
|
|
42
|
+
const results = timeseriesQuery.useTimeseriesQueries(
|
|
43
|
+
series.map((s) => ({ id: s.key, load: s.load })),
|
|
44
|
+
{ baseUrl: props.baseUrl ?? "", range, refreshMs, fetcher }
|
|
53
45
|
);
|
|
54
|
-
const
|
|
55
|
-
queries: requestUrls.map((requestUrl) => ({
|
|
56
|
-
queryKey: ["timeseries", requestUrl],
|
|
57
|
-
queryFn: () => fetcher(requestUrl),
|
|
58
|
-
refetchInterval: refreshMs > 0 ? refreshMs : false,
|
|
59
|
-
staleTime: 0,
|
|
60
|
-
retry: 0
|
|
61
|
-
}))
|
|
62
|
-
});
|
|
46
|
+
const refs = referenceLines ?? [];
|
|
63
47
|
const isError = results.some((r) => r.isError);
|
|
64
48
|
const error = (_a = results.find((r) => r.isError)) == null ? void 0 : _a.error;
|
|
65
49
|
const isLoading = results.some((r) => r.isLoading) && results.every((r) => !r.data);
|
|
@@ -94,11 +78,31 @@ function TimeseriesPanel(props) {
|
|
|
94
78
|
) : null
|
|
95
79
|
] })
|
|
96
80
|
] }),
|
|
97
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", style: breakdown ? void 0 : { height }, children: breakdown ? /* @__PURE__ */ jsxRuntime.jsx(BreakdownView, { total: breakdown.total, unit, items: breakdown.items }) : isError ? /* @__PURE__ */ jsxRuntime.jsx(PanelMessage, { children: (error == null ? void 0 : error.message) ?? "Failed to load metric" }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx(PanelMessage, { children: "Loading…" }) : rows.length < 2 ? /* @__PURE__ */ jsxRuntime.jsx(PanelMessage, { children: "Collecting data…" }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
98
|
-
|
|
81
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-2", style: breakdown ? void 0 : { height }, children: breakdown ? /* @__PURE__ */ jsxRuntime.jsx(BreakdownView, { total: breakdown.total, unit, items: breakdown.items }) : isError ? /* @__PURE__ */ jsxRuntime.jsx(PanelMessage, { children: (error == null ? void 0 : error.message) ?? "Failed to load metric" }) : isLoading ? /* @__PURE__ */ jsxRuntime.jsx(PanelMessage, { children: "Loading…" }) : rows.length < 2 ? /* @__PURE__ */ jsxRuntime.jsx(PanelMessage, { children: "Collecting data…" }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
82
|
+
Chart,
|
|
83
|
+
{
|
|
84
|
+
rows,
|
|
85
|
+
series,
|
|
86
|
+
variant: chartVariant,
|
|
87
|
+
unit,
|
|
88
|
+
showZeroLine: hasMirrored,
|
|
89
|
+
referenceLines: refs
|
|
90
|
+
}
|
|
91
|
+
) }),
|
|
92
|
+
!breakdown && !isError && !isLoading && rows.length >= 2 && series.length + refs.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { series, rows, referenceLines: refs, unit }) : null,
|
|
99
93
|
canExpand ? /* @__PURE__ */ jsxRuntime.jsxs(Modal.Modal, { open: expanded, onClose: () => setExpanded(false), title, size: "xl", children: [
|
|
100
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 360 }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
101
|
-
|
|
94
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { height: 360 }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
95
|
+
Chart,
|
|
96
|
+
{
|
|
97
|
+
rows,
|
|
98
|
+
series,
|
|
99
|
+
variant: expandVariant,
|
|
100
|
+
unit,
|
|
101
|
+
showZeroLine: hasMirrored,
|
|
102
|
+
referenceLines: refs
|
|
103
|
+
}
|
|
104
|
+
) }),
|
|
105
|
+
series.length + refs.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(ChartLegend, { series, rows, referenceLines: refs, unit }) : null
|
|
102
106
|
] }) : null
|
|
103
107
|
] });
|
|
104
108
|
}
|
|
@@ -109,22 +113,45 @@ function LatestReadout({ series, rows }) {
|
|
|
109
113
|
if (v === void 0) return null;
|
|
110
114
|
return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: format.formatUnit(Math.abs(v), only.unit) });
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
const REFERENCE_LINE_COLOR = "var(--muted-foreground, #64748b)";
|
|
117
|
+
function referenceColor(ref) {
|
|
118
|
+
return color.resolveCssColor(ref.color ?? REFERENCE_LINE_COLOR);
|
|
119
|
+
}
|
|
120
|
+
function ChartLegend({
|
|
121
|
+
series,
|
|
122
|
+
rows,
|
|
123
|
+
referenceLines,
|
|
124
|
+
unit
|
|
125
|
+
}) {
|
|
126
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
|
|
127
|
+
series.map((s) => {
|
|
128
|
+
const v = TimeseriesPanel_axes.latestOf(rows, s.key);
|
|
129
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "inline-flex min-w-0 items-center gap-1.5", children: [
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
131
|
+
"span",
|
|
132
|
+
{
|
|
133
|
+
className: "h-2 w-2 shrink-0 rounded-full",
|
|
134
|
+
style: { backgroundColor: color.resolveCssColor(s.color) },
|
|
135
|
+
"aria-hidden": true
|
|
136
|
+
}
|
|
137
|
+
),
|
|
138
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: s.label }),
|
|
139
|
+
v !== void 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: format.formatUnit(Math.abs(v), s.unit) }) : null
|
|
140
|
+
] }, s.key);
|
|
141
|
+
}),
|
|
142
|
+
referenceLines.map((ref, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "inline-flex min-w-0 items-center gap-1.5", children: [
|
|
116
143
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
117
144
|
"span",
|
|
118
145
|
{
|
|
119
|
-
className: "h-
|
|
120
|
-
style: { backgroundColor:
|
|
146
|
+
className: "h-0.5 w-2.5 shrink-0",
|
|
147
|
+
style: { backgroundColor: referenceColor(ref) },
|
|
121
148
|
"aria-hidden": true
|
|
122
149
|
}
|
|
123
150
|
),
|
|
124
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children:
|
|
125
|
-
|
|
126
|
-
] },
|
|
127
|
-
|
|
151
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-foreground", children: ref.label }),
|
|
152
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: format.formatUnit(ref.value, ref.unit ?? unit) })
|
|
153
|
+
] }, `ref-${i}`))
|
|
154
|
+
] });
|
|
128
155
|
}
|
|
129
156
|
function BreakdownReadout({
|
|
130
157
|
total,
|
|
@@ -177,13 +204,18 @@ function Chart({
|
|
|
177
204
|
series,
|
|
178
205
|
variant,
|
|
179
206
|
unit,
|
|
180
|
-
showZeroLine
|
|
207
|
+
showZeroLine,
|
|
208
|
+
referenceLines
|
|
181
209
|
}) {
|
|
182
210
|
const stacked = variant === "stacked";
|
|
183
211
|
const seriesByKey = new Map(series.map((s) => [s.key, s]));
|
|
184
212
|
const strokeOf = (s) => color.resolveCssColor(s.color);
|
|
185
213
|
const { axes: yAxes, axisOfKey } = react.useMemo(() => TimeseriesPanel_axes.assignAxes(series), [series]);
|
|
186
214
|
const axisOf = (key) => axisOfKey.get(key) ?? "left";
|
|
215
|
+
const axisOfUnit = (u) => {
|
|
216
|
+
var _a;
|
|
217
|
+
return ((_a = yAxes.find((ax) => ax.unit === u)) == null ? void 0 : _a.id) ?? "left";
|
|
218
|
+
};
|
|
187
219
|
const axes = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
188
220
|
/* @__PURE__ */ jsxRuntime.jsx(recharts.CartesianGrid, { strokeOpacity: 0.12, vertical: false }),
|
|
189
221
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -221,7 +253,18 @@ function Chart({
|
|
|
221
253
|
contentStyle: { fontSize: 12 }
|
|
222
254
|
}
|
|
223
255
|
),
|
|
224
|
-
showZeroLine ? /* @__PURE__ */ jsxRuntime.jsx(recharts.ReferenceLine, { yAxisId: "left", y: 0, stroke: "currentColor", strokeOpacity: 0.35 }) : null
|
|
256
|
+
showZeroLine ? /* @__PURE__ */ jsxRuntime.jsx(recharts.ReferenceLine, { yAxisId: "left", y: 0, stroke: "currentColor", strokeOpacity: 0.35 }) : null,
|
|
257
|
+
referenceLines.map((ref, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
258
|
+
recharts.ReferenceLine,
|
|
259
|
+
{
|
|
260
|
+
yAxisId: axisOfUnit(ref.unit ?? unit),
|
|
261
|
+
y: ref.value,
|
|
262
|
+
stroke: referenceColor(ref),
|
|
263
|
+
strokeDasharray: "4 4",
|
|
264
|
+
ifOverflow: "extendDomain"
|
|
265
|
+
},
|
|
266
|
+
`ref-${i}`
|
|
267
|
+
))
|
|
225
268
|
] });
|
|
226
269
|
return /* @__PURE__ */ jsxRuntime.jsx(recharts.ResponsiveContainer, { width: "100%", height: "100%", children: variant === "line" ? /* @__PURE__ */ jsxRuntime.jsxs(recharts.LineChart, { data: rows, margin: { top: 4, right: 8, bottom: 0, left: 0 }, children: [
|
|
227
270
|
axes,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeseriesPanel.cjs","sources":["../../src/data/TimeseriesPanel.tsx"],"sourcesContent":["import { useQueries } from \"@tanstack/react-query\";\nimport { useMemo, useState } from \"react\";\nimport {\n Area,\n AreaChart,\n CartesianGrid,\n Line,\n LineChart,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"../lib/utils\";\nimport { resolveCssColor } from \"../lib/color\";\nimport { formatUnit } from \"../lib/format\";\nimport { Modal } from \"../overlay/Modal\";\nimport { ProgressBar, type ProgressSegment } from \"./ProgressBar\";\nimport { UiFullscreen } from \"../icons\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport { assignAxes, latestOf } from \"./TimeseriesPanel.axes\";\nimport {\n mergeSeries,\n resolveBreakdown,\n resolveSeries,\n type BreakdownItem,\n type MergedRow,\n type ResolvedSeries,\n type TimeseriesPanelProps,\n type TimeseriesResponse,\n} from \"./TimeseriesPanel.model\";\n\nexport type { AxisAssignment, AxisOrientation, AxisSpec } from \"./TimeseriesPanel.axes\";\nexport type {\n BreakdownItem,\n MergedRow,\n ResolvedSeries,\n TimeseriesPanelProps,\n TimeseriesPoint,\n TimeseriesResponse,\n TimeseriesSeries,\n} from \"./TimeseriesPanel.model\";\n\nconst defaultFetcher = async (url: string): Promise<TimeseriesResponse> => {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);\n return res.json();\n};\n\nfunction formatClock(at: string): string {\n const date = new Date(at);\n if (Number.isNaN(date.getTime())) return \"\";\n return date.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\n}\n\nexport function TimeseriesPanel(props: TimeseriesPanelProps) {\n const {\n title,\n icon,\n unit,\n range = \"1h\",\n refreshMs = 5000,\n height = 160,\n variant = \"area\",\n expandable = true,\n total,\n fetcher = defaultFetcher,\n className,\n } = props;\n const isBreakdown = variant === \"breakdown\";\n // The inline chart (non-breakdown only) and the expand modal both need a\n // time-series variant; breakdown has no inline chart and expands to \"area\".\n const chartVariant: ChartVariant = isBreakdown ? \"area\" : variant;\n const expandVariant = props.expandVariant ?? chartVariant;\n\n const [expanded, setExpanded] = useState(false);\n\n const series = useMemo(() => resolveSeries(props), [props]);\n const requestUrls = useMemo(\n () =>\n series.map((s) => {\n const u = new URL(s.url, window.location.origin);\n if (range) u.searchParams.set(\"since\", range);\n return u.pathname + u.search;\n }),\n [series, range],\n );\n\n const results = useQueries({\n queries: requestUrls.map((requestUrl) => ({\n queryKey: [\"timeseries\", requestUrl],\n queryFn: () => fetcher(requestUrl),\n refetchInterval: refreshMs > 0 ? refreshMs : false,\n staleTime: 0,\n retry: 0,\n })),\n });\n\n const isError = results.some((r) => r.isError);\n const error = results.find((r) => r.isError)?.error;\n const isLoading = results.some((r) => r.isLoading) && results.every((r) => !r.data);\n const pointsBySeries = results.map((r) => r.data?.points);\n const rows = useMemo(() => mergeSeries(series, pointsBySeries), [series, pointsBySeries]);\n const hasMirrored = series.some((s) => s.transform);\n const breakdown = useMemo(\n () => (isBreakdown ? resolveBreakdown(series, rows, total) : null),\n [isBreakdown, series, rows, total],\n );\n\n // Breakdown renders instantly from `current`; the modal still needs polled\n // history. Other variants gate on having ≥2 points to draw a line.\n const canExpand = expandable && rows.length >= 2;\n\n return (\n <div className={cn(\"flex flex-col rounded-lg border border-border bg-card p-3\", className)}>\n <div className=\"flex items-center justify-between gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex min-w-0 items-center gap-1.5\">\n {icon ? <PanelIcon icon={icon} /> : null}\n <span className=\"truncate font-medium text-foreground\">{title}</span>\n </span>\n <span className=\"inline-flex shrink-0 items-center gap-1.5\">\n {breakdown ? (\n <BreakdownReadout total={breakdown.total} unit={unit} items={breakdown.items} />\n ) : (\n <LatestReadout series={series} rows={rows} />\n )}\n {canExpand ? (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"text-muted-foreground transition-colors hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={14} height={14} />\n </button>\n ) : null}\n </span>\n </div>\n <div className=\"mt-2\" style={breakdown ? undefined : { height }}>\n {breakdown ? (\n <BreakdownView total={breakdown.total} unit={unit} items={breakdown.items} />\n ) : isError ? (\n <PanelMessage>{(error as Error)?.message ?? \"Failed to load metric\"}</PanelMessage>\n ) : isLoading ? (\n <PanelMessage>Loading…</PanelMessage>\n ) : rows.length < 2 ? (\n <PanelMessage>Collecting data…</PanelMessage>\n ) : (\n <Chart rows={rows} series={series} variant={chartVariant} unit={unit} showZeroLine={hasMirrored} />\n )}\n </div>\n {!breakdown && !isError && !isLoading && rows.length >= 2 && series.length > 1 ? (\n <ChartLegend series={series} rows={rows} />\n ) : null}\n {canExpand ? (\n <Modal open={expanded} onClose={() => setExpanded(false)} title={title} size=\"xl\">\n <div style={{ height: 360 }}>\n <Chart rows={rows} series={series} variant={expandVariant} unit={unit} showZeroLine={hasMirrored} />\n </div>\n {series.length > 1 ? <ChartLegend series={series} rows={rows} /> : null}\n </Modal>\n ) : null}\n </div>\n );\n}\n\n/**\n * Header readout for a single-series panel: the latest value, formatted with the\n * series' unit. Multi-series panels render the per-series values in {@link ChartLegend}\n * below the chart instead, so this returns null for them to avoid overlapping the title.\n */\nfunction LatestReadout({ series, rows }: { series: ResolvedSeries[]; rows: MergedRow[] }) {\n const only = series.length === 1 ? series[0] : undefined;\n if (!only) return null;\n const v = latestOf(rows, only.key);\n if (v === undefined) return null;\n return <span className=\"tabular-nums\">{formatUnit(Math.abs(v), only.unit)}</span>;\n}\n\n/** Wrapping color-keyed legend for the time-series variants: dot + label + latest value. */\nfunction ChartLegend({ series, rows }: { series: ResolvedSeries[]; rows: MergedRow[] }) {\n return (\n <ul className=\"mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground\">\n {series.map((s) => {\n const v = latestOf(rows, s.key);\n return (\n <li key={s.key} className=\"inline-flex min-w-0 items-center gap-1.5\">\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: resolveCssColor(s.color) }}\n aria-hidden\n />\n <span className=\"truncate text-foreground\">{s.label}</span>\n {v !== undefined ? (\n <span className=\"tabular-nums\">{formatUnit(Math.abs(v), s.unit)}</span>\n ) : null}\n </li>\n );\n })}\n </ul>\n );\n}\n\n/** Breakdown header readout: `used / total · pct%`, where used is the sum of segments. */\nfunction BreakdownReadout({\n total,\n unit,\n items,\n}: {\n total: number;\n unit: string | undefined;\n items: BreakdownItem[];\n}) {\n const used = items.reduce((acc, it) => acc + it.value, 0);\n const pct = total > 0 ? Math.min(100, (used / total) * 100) : 0;\n return (\n <span className=\"font-mono tabular-nums\">\n {formatUnit(used, unit)} / {formatUnit(total, unit)} · {pct.toFixed(0)}%\n </span>\n );\n}\n\n/** Instantaneous stacked bar + legend list. The expand button (in the panel header) opens the graph. */\nfunction BreakdownView({\n total,\n unit,\n items,\n}: {\n total: number;\n unit: string | undefined;\n items: BreakdownItem[];\n}) {\n const segments: ProgressSegment[] = items.map((it) => ({\n count: it.value,\n color: it.color,\n label: it.label,\n }));\n return (\n <div className=\"flex flex-col gap-2\">\n <ProgressBar segments={segments} total={total > 0 ? total : 1} height=\"h-3\" />\n {items.length > 0 ? (\n <ul className=\"grid grid-cols-1 gap-y-1 text-xs\">\n {items.map((it) => (\n <li key={it.key} className=\"flex items-center gap-2\">\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: resolveCssColor(it.color) }}\n aria-hidden\n />\n <span className=\"min-w-0 flex-1 truncate font-mono text-muted-foreground\">\n {it.label}\n </span>\n <span className=\"font-mono tabular-nums text-foreground\">\n {formatUnit(it.value, it.unit ?? unit)}\n </span>\n <span className=\"w-10 text-right font-mono tabular-nums text-muted-foreground\">\n {it.percent.toFixed(0)}%\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\ntype ChartVariant = \"area\" | \"line\" | \"stacked\";\n\nfunction Chart({\n rows,\n series,\n variant,\n unit,\n showZeroLine,\n}: {\n rows: MergedRow[];\n series: ResolvedSeries[];\n variant: ChartVariant;\n unit: string | undefined;\n showZeroLine: boolean;\n}) {\n const stacked = variant === \"stacked\";\n // Mirrored series carry negative values; show magnitudes on axis and tooltip.\n const seriesByKey = new Map(series.map((s) => [s.key, s]));\n // recharts needs a CSS color value; a series may carry a Tailwind class.\n const strokeOf = (s: ResolvedSeries) => resolveCssColor(s.color);\n // Distinct units map to a left and (when present) right axis; each series draws\n // against the axis for its unit so mixed-unit panels keep independent scales.\n const { axes: yAxes, axisOfKey } = useMemo(() => assignAxes(series), [series]);\n const axisOf = (key: string): \"left\" | \"right\" => axisOfKey.get(key) ?? \"left\";\n const axes = (\n <>\n <CartesianGrid strokeOpacity={0.12} vertical={false} />\n <XAxis\n dataKey=\"at\"\n tickFormatter={formatClock}\n tick={{ fontSize: 10 }}\n stroke=\"currentColor\"\n strokeOpacity={0.35}\n minTickGap={32}\n />\n {yAxes.map((ax) => (\n <YAxis\n key={ax.id}\n yAxisId={ax.id}\n orientation={ax.orientation}\n tick={{ fontSize: 10 }}\n stroke=\"currentColor\"\n strokeOpacity={0.35}\n width={40}\n tickFormatter={(v: number) => formatUnit(Math.abs(v), ax.unit)}\n />\n ))}\n <Tooltip\n labelFormatter={(label) => formatClock(String(label))}\n formatter={(value, name) => {\n const s = seriesByKey.get(String(name));\n return [formatUnit(Math.abs(Number(value) || 0), s?.unit ?? unit), s?.label ?? String(name)];\n }}\n contentStyle={{ fontSize: 12 }}\n />\n {showZeroLine ? (\n <ReferenceLine yAxisId=\"left\" y={0} stroke=\"currentColor\" strokeOpacity={0.35} />\n ) : null}\n </>\n );\n\n return (\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {variant === \"line\" ? (\n <LineChart data={rows} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>\n {axes}\n {series.map((s) => (\n <Line\n key={s.key}\n yAxisId={axisOf(s.key)}\n type=\"monotone\"\n dataKey={s.key}\n name={s.key}\n stroke={strokeOf(s)}\n strokeWidth={2}\n dot={false}\n connectNulls\n isAnimationActive={false}\n />\n ))}\n </LineChart>\n ) : (\n <AreaChart data={rows} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>\n {axes}\n {series.map((s) => (\n <Area\n key={s.key}\n yAxisId={axisOf(s.key)}\n type=\"monotone\"\n dataKey={s.key}\n name={s.key}\n stroke={strokeOf(s)}\n fill={strokeOf(s)}\n {...(stacked ? { stackId: `stack-${axisOf(s.key)}` } : {})}\n fillOpacity={stacked ? 0.7 : 0.12}\n strokeWidth={stacked ? 0 : 2}\n connectNulls\n isAnimationActive={false}\n />\n ))}\n </AreaChart>\n )}\n </ResponsiveContainer>\n );\n}\n\nfunction PanelMessage({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex h-full items-center justify-center text-xs text-muted-foreground\">\n {children}\n </div>\n );\n}\n\nfunction PanelIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={16} height={16} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={16} height={16} className=\"text-muted-foreground\" />;\n}\n"],"names":["useState","useMemo","resolveSeries","useQueries","_a","mergeSeries","resolveBreakdown","cn","jsxs","jsx","Icon","UiFullscreen","Modal","latestOf","formatUnit","resolveCssColor","ProgressBar","assignAxes","Fragment","CartesianGrid","XAxis","YAxis","Tooltip","ReferenceLine","ResponsiveContainer","LineChart","Line","AreaChart","Area"],"mappings":";;;;;;;;;;;;;;;AA4CA,MAAM,iBAAiB,OAAO,QAA6C;AACzE,QAAM,MAAM,MAAM,MAAM,GAAG;AAC3B,MAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,2BAA2B,IAAI,MAAM,EAAE;AACpE,SAAO,IAAI,KAAA;AACb;AAEA,SAAS,YAAY,IAAoB;AACvC,QAAM,OAAO,IAAI,KAAK,EAAE;AACxB,MAAI,OAAO,MAAM,KAAK,QAAA,CAAS,EAAG,QAAO;AACzC,SAAO,KAAK,mBAAmB,IAAI,EAAE,MAAM,WAAW,QAAQ,WAAW;AAC3E;AAEO,SAAS,gBAAgB,OAA6B;;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb;AAAA,IACA,UAAU;AAAA,IACV;AAAA,EAAA,IACE;AACJ,QAAM,cAAc,YAAY;AAGhC,QAAM,eAA6B,cAAc,SAAS;AAC1D,QAAM,gBAAgB,MAAM,iBAAiB;AAE7C,QAAM,CAAC,UAAU,WAAW,IAAIA,MAAAA,SAAS,KAAK;AAE9C,QAAM,SAASC,MAAAA,QAAQ,MAAMC,sBAAAA,cAAc,KAAK,GAAG,CAAC,KAAK,CAAC;AAC1D,QAAM,cAAcD,MAAAA;AAAAA,IAClB,MACE,OAAO,IAAI,CAAC,MAAM;AAChB,YAAM,IAAI,IAAI,IAAI,EAAE,KAAK,OAAO,SAAS,MAAM;AAC/C,UAAI,MAAO,GAAE,aAAa,IAAI,SAAS,KAAK;AAC5C,aAAO,EAAE,WAAW,EAAE;AAAA,IACxB,CAAC;AAAA,IACH,CAAC,QAAQ,KAAK;AAAA,EAAA;AAGhB,QAAM,UAAUE,WAAAA,WAAW;AAAA,IACzB,SAAS,YAAY,IAAI,CAAC,gBAAgB;AAAA,MACxC,UAAU,CAAC,cAAc,UAAU;AAAA,MACnC,SAAS,MAAM,QAAQ,UAAU;AAAA,MACjC,iBAAiB,YAAY,IAAI,YAAY;AAAA,MAC7C,WAAW;AAAA,MACX,OAAO;AAAA,IAAA,EACP;AAAA,EAAA,CACH;AAED,QAAM,UAAU,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO;AAC7C,QAAM,SAAQ,aAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,MAA7B,mBAAgC;AAC9C,QAAM,YAAY,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,QAAQ,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI;AAClF,QAAM,iBAAiB,QAAQ,IAAI,CAAC;;AAAM,YAAAC,MAAA,EAAE,SAAF,gBAAAA,IAAQ;AAAA,GAAM;AACxD,QAAM,OAAOH,cAAQ,MAAMI,kCAAY,QAAQ,cAAc,GAAG,CAAC,QAAQ,cAAc,CAAC;AACxF,QAAM,cAAc,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS;AAClD,QAAM,YAAYJ,MAAAA;AAAAA,IAChB,MAAO,cAAcK,sBAAAA,iBAAiB,QAAQ,MAAM,KAAK,IAAI;AAAA,IAC7D,CAAC,aAAa,QAAQ,MAAM,KAAK;AAAA,EAAA;AAKnC,QAAM,YAAY,cAAc,KAAK,UAAU;AAE/C,yCACG,OAAA,EAAI,WAAWC,MAAAA,GAAG,6DAA6D,SAAS,GACvF,UAAA;AAAA,IAAAC,2BAAAA,KAAC,OAAA,EAAI,WAAU,yEACb,UAAA;AAAA,MAAAA,2BAAAA,KAAC,QAAA,EAAK,WAAU,4CACb,UAAA;AAAA,QAAA,OAAOC,2BAAAA,IAAC,WAAA,EAAU,KAAA,CAAY,IAAK;AAAA,QACpCA,2BAAAA,IAAC,QAAA,EAAK,WAAU,wCAAwC,UAAA,MAAA,CAAM;AAAA,MAAA,GAChE;AAAA,MACAD,2BAAAA,KAAC,QAAA,EAAK,WAAU,6CACb,UAAA;AAAA,QAAA,YACCC,2BAAAA,IAAC,kBAAA,EAAiB,OAAO,UAAU,OAAO,MAAY,OAAO,UAAU,MAAA,CAAO,IAE9EA,2BAAAA,IAAC,eAAA,EAAc,QAAgB,MAAY;AAAA,QAE5C,YACCA,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAW;AAAA,YACX,SAAS,MAAM,YAAY,IAAI;AAAA,YAC/B,WAAU;AAAA,YAEV,yCAACC,KAAAA,MAAA,EAAK,MAAMC,aAAAA,cAAc,OAAO,IAAI,QAAQ,GAAA,CAAI;AAAA,UAAA;AAAA,QAAA,IAEjD;AAAA,MAAA,EAAA,CACN;AAAA,IAAA,GACF;AAAA,IACAF,2BAAAA,IAAC,SAAI,WAAU,QAAO,OAAO,YAAY,SAAY,EAAE,OAAA,GACpD,UAAA,2CACE,eAAA,EAAc,OAAO,UAAU,OAAO,MAAY,OAAO,UAAU,OAAO,IACzE,UACFA,2BAAAA,IAAC,cAAA,EAAe,0CAAiB,YAAW,wBAAA,CAAwB,IAClE,YACFA,+BAAC,gBAAa,UAAA,WAAA,CAAQ,IACpB,KAAK,SAAS,IAChBA,2BAAAA,IAAC,gBAAa,UAAA,mBAAA,CAAgB,IAE9BA,2BAAAA,IAAC,OAAA,EAAM,MAAY,QAAgB,SAAS,cAAc,MAAY,cAAc,YAAA,CAAa,EAAA,CAErG;AAAA,IACC,CAAC,aAAa,CAAC,WAAW,CAAC,aAAa,KAAK,UAAU,KAAK,OAAO,SAAS,IAC3EA,2BAAAA,IAAC,aAAA,EAAY,QAAgB,MAAY,IACvC;AAAA,IACH,YACCD,2BAAAA,KAACI,MAAAA,OAAA,EAAM,MAAM,UAAU,SAAS,MAAM,YAAY,KAAK,GAAG,OAAc,MAAK,MAC3E,UAAA;AAAA,MAAAH,+BAAC,OAAA,EAAI,OAAO,EAAE,QAAQ,OACpB,UAAAA,+BAAC,OAAA,EAAM,MAAY,QAAgB,SAAS,eAAe,MAAY,cAAc,aAAa,GACpG;AAAA,MACC,OAAO,SAAS,mCAAK,aAAA,EAAY,QAAgB,MAAY,IAAK;AAAA,IAAA,EAAA,CACrE,IACE;AAAA,EAAA,GACN;AAEJ;AAOA,SAAS,cAAc,EAAE,QAAQ,QAAyD;AACxF,QAAM,OAAO,OAAO,WAAW,IAAI,OAAO,CAAC,IAAI;AAC/C,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,IAAII,qBAAAA,SAAS,MAAM,KAAK,GAAG;AACjC,MAAI,MAAM,OAAW,QAAO;AAC5B,SAAOJ,2BAAAA,IAAC,QAAA,EAAK,WAAU,gBAAgB,UAAAK,OAAAA,WAAW,KAAK,IAAI,CAAC,GAAG,KAAK,IAAI,EAAA,CAAE;AAC5E;AAGA,SAAS,YAAY,EAAE,QAAQ,QAAyD;AACtF,wCACG,MAAA,EAAG,WAAU,uEACX,UAAA,OAAO,IAAI,CAAC,MAAM;AACjB,UAAM,IAAID,qBAAAA,SAAS,MAAM,EAAE,GAAG;AAC9B,WACEL,2BAAAA,KAAC,MAAA,EAAe,WAAU,4CACxB,UAAA;AAAA,MAAAC,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,iBAAiBM,MAAAA,gBAAgB,EAAE,KAAK,EAAA;AAAA,UACjD,eAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAEbN,2BAAAA,IAAC,QAAA,EAAK,WAAU,4BAA4B,YAAE,OAAM;AAAA,MACnD,MAAM,SACLA,2BAAAA,IAAC,QAAA,EAAK,WAAU,gBAAgB,UAAAK,OAAAA,WAAW,KAAK,IAAI,CAAC,GAAG,EAAE,IAAI,GAAE,IAC9D;AAAA,IAAA,EAAA,GATG,EAAE,GAUX;AAAA,EAEJ,CAAC,EAAA,CACH;AAEJ;AAGA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,OAAO,MAAM,OAAO,CAAC,KAAK,OAAO,MAAM,GAAG,OAAO,CAAC;AACxD,QAAM,MAAM,QAAQ,IAAI,KAAK,IAAI,KAAM,OAAO,QAAS,GAAG,IAAI;AAC9D,SACEN,2BAAAA,KAAC,QAAA,EAAK,WAAU,0BACb,UAAA;AAAA,IAAAM,OAAAA,WAAW,MAAM,IAAI;AAAA,IAAE;AAAA,IAAIA,OAAAA,WAAW,OAAO,IAAI;AAAA,IAAE;AAAA,IAAI,IAAI,QAAQ,CAAC;AAAA,IAAE;AAAA,EAAA,GACzE;AAEJ;AAGA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,WAA8B,MAAM,IAAI,CAAC,QAAQ;AAAA,IACrD,OAAO,GAAG;AAAA,IACV,OAAO,GAAG;AAAA,IACV,OAAO,GAAG;AAAA,EAAA,EACV;AACF,SACEN,2BAAAA,KAAC,OAAA,EAAI,WAAU,uBACb,UAAA;AAAA,IAAAC,2BAAAA,IAACO,YAAAA,aAAA,EAAY,UAAoB,OAAO,QAAQ,IAAI,QAAQ,GAAG,QAAO,MAAA,CAAM;AAAA,IAC3E,MAAM,SAAS,IACdP,2BAAAA,IAAC,QAAG,WAAU,oCACX,UAAA,MAAM,IAAI,CAAC,OACVD,2BAAAA,KAAC,MAAA,EAAgB,WAAU,2BACzB,UAAA;AAAA,MAAAC,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,iBAAiBM,MAAAA,gBAAgB,GAAG,KAAK,EAAA;AAAA,UAClD,eAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAEbN,2BAAAA,IAAC,QAAA,EAAK,WAAU,2DACb,aAAG,OACN;AAAA,MACAA,2BAAAA,IAAC,QAAA,EAAK,WAAU,0CACb,UAAAK,OAAAA,WAAW,GAAG,OAAO,GAAG,QAAQ,IAAI,EAAA,CACvC;AAAA,MACAN,2BAAAA,KAAC,QAAA,EAAK,WAAU,gEACb,UAAA;AAAA,QAAA,GAAG,QAAQ,QAAQ,CAAC;AAAA,QAAE;AAAA,MAAA,EAAA,CACzB;AAAA,IAAA,EAAA,GAdO,GAAG,GAeZ,CACD,EAAA,CACH,IACE;AAAA,EAAA,GACN;AAEJ;AAIA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,UAAU,YAAY;AAE5B,QAAM,cAAc,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAEzD,QAAM,WAAW,CAAC,MAAsBO,MAAAA,gBAAgB,EAAE,KAAK;AAG/D,QAAM,EAAE,MAAM,OAAO,cAAcd,MAAAA,QAAQ,MAAMgB,qBAAAA,WAAW,MAAM,GAAG,CAAC,MAAM,CAAC;AAC7E,QAAM,SAAS,CAAC,QAAkC,UAAU,IAAI,GAAG,KAAK;AACxE,QAAM,OACJT,2BAAAA,KAAAU,WAAAA,UAAA,EACE,UAAA;AAAA,IAAAT,2BAAAA,IAACU,SAAAA,eAAA,EAAc,eAAe,MAAM,UAAU,OAAO;AAAA,IACrDV,2BAAAA;AAAAA,MAACW,SAAAA;AAAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,eAAe;AAAA,QACf,MAAM,EAAE,UAAU,GAAA;AAAA,QAClB,QAAO;AAAA,QACP,eAAe;AAAA,QACf,YAAY;AAAA,MAAA;AAAA,IAAA;AAAA,IAEb,MAAM,IAAI,CAAC,OACVX,2BAAAA;AAAAA,MAACY,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,GAAG;AAAA,QACZ,aAAa,GAAG;AAAA,QAChB,MAAM,EAAE,UAAU,GAAA;AAAA,QAClB,QAAO;AAAA,QACP,eAAe;AAAA,QACf,OAAO;AAAA,QACP,eAAe,CAAC,MAAcP,kBAAW,KAAK,IAAI,CAAC,GAAG,GAAG,IAAI;AAAA,MAAA;AAAA,MAPxD,GAAG;AAAA,IAAA,CASX;AAAA,IACDL,2BAAAA;AAAAA,MAACa,SAAAA;AAAAA,MAAA;AAAA,QACC,gBAAgB,CAAC,UAAU,YAAY,OAAO,KAAK,CAAC;AAAA,QACpD,WAAW,CAAC,OAAO,SAAS;AAC1B,gBAAM,IAAI,YAAY,IAAI,OAAO,IAAI,CAAC;AACtC,iBAAO,CAACR,OAAAA,WAAW,KAAK,IAAI,OAAO,KAAK,KAAK,CAAC,IAAG,uBAAG,SAAQ,IAAI,IAAG,uBAAG,UAAS,OAAO,IAAI,CAAC;AAAA,QAC7F;AAAA,QACA,cAAc,EAAE,UAAU,GAAA;AAAA,MAAG;AAAA,IAAA;AAAA,IAE9B,eACCL,2BAAAA,IAACc,SAAAA,eAAA,EAAc,SAAQ,QAAO,GAAG,GAAG,QAAO,gBAAe,eAAe,KAAA,CAAM,IAC7E;AAAA,EAAA,GACN;AAGF,SACEd,+BAACe,SAAAA,uBAAoB,OAAM,QAAO,QAAO,QACtC,UAAA,YAAY,SACXhB,2BAAAA,KAACiB,SAAAA,WAAA,EAAU,MAAM,MAAM,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,EAAA,GACjE,UAAA;AAAA,IAAA;AAAA,IACA,OAAO,IAAI,CAAC,MACXhB,2BAAAA;AAAAA,MAACiB,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,OAAO,EAAE,GAAG;AAAA,QACrB,MAAK;AAAA,QACL,SAAS,EAAE;AAAA,QACX,MAAM,EAAE;AAAA,QACR,QAAQ,SAAS,CAAC;AAAA,QAClB,aAAa;AAAA,QACb,KAAK;AAAA,QACL,cAAY;AAAA,QACZ,mBAAmB;AAAA,MAAA;AAAA,MATd,EAAE;AAAA,IAAA,CAWV;AAAA,EAAA,EAAA,CACH,IAEAlB,2BAAAA,KAACmB,SAAAA,WAAA,EAAU,MAAM,MAAM,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,KACjE,UAAA;AAAA,IAAA;AAAA,IACA,OAAO,IAAI,CAAC,MACXlB,2BAAAA;AAAAA,MAACmB,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,OAAO,EAAE,GAAG;AAAA,QACrB,MAAK;AAAA,QACL,SAAS,EAAE;AAAA,QACX,MAAM,EAAE;AAAA,QACR,QAAQ,SAAS,CAAC;AAAA,QAClB,MAAM,SAAS,CAAC;AAAA,QACf,GAAI,UAAU,EAAE,SAAS,SAAS,OAAO,EAAE,GAAG,CAAC,GAAA,IAAO,CAAA;AAAA,QACvD,aAAa,UAAU,MAAM;AAAA,QAC7B,aAAa,UAAU,IAAI;AAAA,QAC3B,cAAY;AAAA,QACZ,mBAAmB;AAAA,MAAA;AAAA,MAXd,EAAE;AAAA,IAAA,CAaV;AAAA,EAAA,EAAA,CACH,EAAA,CAEJ;AAEJ;AAEA,SAAS,aAAa,EAAE,YAA2C;AACjE,SACEnB,2BAAAA,IAAC,OAAA,EAAI,WAAU,yEACZ,SAAA,CACH;AAEJ;AAEA,SAAS,UAAU,EAAE,QAAgD;AACnE,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAOA,+BAACC,KAAAA,QAAK,MAAM,MAAM,OAAO,IAAI,QAAQ,IAAI,WAAU,wBAAA,CAAwB;AAAA,EACpF;AACA,SAAOD,+BAACC,KAAAA,QAAK,MAAY,OAAO,IAAI,QAAQ,IAAI,WAAU,yBAAwB;AACpF;;"}
|
|
1
|
+
{"version":3,"file":"TimeseriesPanel.cjs","sources":["../../src/data/TimeseriesPanel.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Area,\n AreaChart,\n CartesianGrid,\n Line,\n LineChart,\n ReferenceLine,\n ResponsiveContainer,\n Tooltip,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport { cn } from \"../lib/utils\";\nimport { resolveCssColor } from \"../lib/color\";\nimport { formatUnit } from \"../lib/format\";\nimport { Modal } from \"../overlay/Modal\";\nimport { ProgressBar, type ProgressSegment } from \"./ProgressBar\";\nimport { UiFullscreen } from \"../icons\";\nimport { Icon, type StaticIconComponent } from \"./Icon\";\nimport { assignAxes, latestOf } from \"./TimeseriesPanel.axes\";\nimport {\n mergeSeries,\n resolveBreakdown,\n resolveSeries,\n type BreakdownItem,\n type MergedRow,\n type ResolvedSeries,\n type TimeseriesPanelProps,\n type TimeseriesReferenceLine,\n} from \"./TimeseriesPanel.model\";\nimport { defaultTimeseriesFetcher, useTimeseriesQueries } from \"./timeseries-query\";\n\nexport type { AxisAssignment, AxisOrientation, AxisSpec } from \"./TimeseriesPanel.axes\";\nexport type {\n BreakdownItem,\n MergedRow,\n ResolvedSeries,\n SeriesLoadContext,\n SeriesLoader,\n TimeseriesPanelProps,\n TimeseriesPoint,\n TimeseriesReferenceLine,\n TimeseriesResponse,\n TimeseriesSeries,\n} from \"./TimeseriesPanel.model\";\n\nfunction formatClock(at: string): string {\n const date = new Date(at);\n if (Number.isNaN(date.getTime())) return \"\";\n return date.toLocaleTimeString([], { hour: \"2-digit\", minute: \"2-digit\" });\n}\n\nexport function TimeseriesPanel(props: TimeseriesPanelProps) {\n const {\n title,\n icon,\n unit,\n range = \"1h\",\n refreshMs = 5000,\n height = 160,\n variant = \"area\",\n expandable = true,\n total,\n referenceLines,\n fetcher = defaultTimeseriesFetcher,\n className,\n } = props;\n const isBreakdown = variant === \"breakdown\";\n // The inline chart (non-breakdown only) and the expand modal both need a\n // time-series variant; breakdown has no inline chart and expands to \"area\".\n const chartVariant: ChartVariant = isBreakdown ? \"area\" : variant;\n const expandVariant = props.expandVariant ?? chartVariant;\n\n const [expanded, setExpanded] = useState(false);\n\n const series = useMemo(() => resolveSeries(props), [props]);\n const results = useTimeseriesQueries(\n series.map((s) => ({ id: s.key, load: s.load })),\n { baseUrl: props.baseUrl ?? \"\", range, refreshMs, fetcher },\n );\n const refs = referenceLines ?? [];\n\n const isError = results.some((r) => r.isError);\n const error = results.find((r) => r.isError)?.error;\n const isLoading = results.some((r) => r.isLoading) && results.every((r) => !r.data);\n const pointsBySeries = results.map((r) => r.data?.points);\n const rows = useMemo(() => mergeSeries(series, pointsBySeries), [series, pointsBySeries]);\n const hasMirrored = series.some((s) => s.transform);\n const breakdown = useMemo(\n () => (isBreakdown ? resolveBreakdown(series, rows, total) : null),\n [isBreakdown, series, rows, total],\n );\n\n // Breakdown renders instantly from `current`; the modal still needs polled\n // history. Other variants gate on having ≥2 points to draw a line.\n const canExpand = expandable && rows.length >= 2;\n\n return (\n <div className={cn(\"flex flex-col rounded-lg border border-border bg-card p-3\", className)}>\n <div className=\"flex items-center justify-between gap-2 text-xs text-muted-foreground\">\n <span className=\"inline-flex min-w-0 items-center gap-1.5\">\n {icon ? <PanelIcon icon={icon} /> : null}\n <span className=\"truncate font-medium text-foreground\">{title}</span>\n </span>\n <span className=\"inline-flex shrink-0 items-center gap-1.5\">\n {breakdown ? (\n <BreakdownReadout total={breakdown.total} unit={unit} items={breakdown.items} />\n ) : (\n <LatestReadout series={series} rows={rows} />\n )}\n {canExpand ? (\n <button\n type=\"button\"\n aria-label=\"Expand chart\"\n onClick={() => setExpanded(true)}\n className=\"text-muted-foreground transition-colors hover:text-foreground\"\n >\n <Icon icon={UiFullscreen} width={14} height={14} />\n </button>\n ) : null}\n </span>\n </div>\n <div className=\"mt-2\" style={breakdown ? undefined : { height }}>\n {breakdown ? (\n <BreakdownView total={breakdown.total} unit={unit} items={breakdown.items} />\n ) : isError ? (\n <PanelMessage>{(error as Error)?.message ?? \"Failed to load metric\"}</PanelMessage>\n ) : isLoading ? (\n <PanelMessage>Loading…</PanelMessage>\n ) : rows.length < 2 ? (\n <PanelMessage>Collecting data…</PanelMessage>\n ) : (\n <Chart\n rows={rows}\n series={series}\n variant={chartVariant}\n unit={unit}\n showZeroLine={hasMirrored}\n referenceLines={refs}\n />\n )}\n </div>\n {!breakdown && !isError && !isLoading && rows.length >= 2 && series.length + refs.length > 1 ? (\n <ChartLegend series={series} rows={rows} referenceLines={refs} unit={unit} />\n ) : null}\n {canExpand ? (\n <Modal open={expanded} onClose={() => setExpanded(false)} title={title} size=\"xl\">\n <div style={{ height: 360 }}>\n <Chart\n rows={rows}\n series={series}\n variant={expandVariant}\n unit={unit}\n showZeroLine={hasMirrored}\n referenceLines={refs}\n />\n </div>\n {series.length + refs.length > 1 ? (\n <ChartLegend series={series} rows={rows} referenceLines={refs} unit={unit} />\n ) : null}\n </Modal>\n ) : null}\n </div>\n );\n}\n\n/**\n * Header readout for a single-series panel: the latest value, formatted with the\n * series' unit. Multi-series panels render the per-series values in {@link ChartLegend}\n * below the chart instead, so this returns null for them to avoid overlapping the title.\n */\nfunction LatestReadout({ series, rows }: { series: ResolvedSeries[]; rows: MergedRow[] }) {\n const only = series.length === 1 ? series[0] : undefined;\n if (!only) return null;\n const v = latestOf(rows, only.key);\n if (v === undefined) return null;\n return <span className=\"tabular-nums\">{formatUnit(Math.abs(v), only.unit)}</span>;\n}\n\nconst REFERENCE_LINE_COLOR = \"var(--muted-foreground, #64748b)\";\n\nfunction referenceColor(ref: TimeseriesReferenceLine): string {\n return resolveCssColor(ref.color ?? REFERENCE_LINE_COLOR);\n}\n\n/**\n * Wrapping color-keyed legend for the time-series variants: dot + label + latest\n * value, then each reference line with its fixed value.\n */\nfunction ChartLegend({\n series,\n rows,\n referenceLines,\n unit,\n}: {\n series: ResolvedSeries[];\n rows: MergedRow[];\n referenceLines: TimeseriesReferenceLine[];\n unit: string | undefined;\n}) {\n return (\n <ul className=\"mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground\">\n {series.map((s) => {\n const v = latestOf(rows, s.key);\n return (\n <li key={s.key} className=\"inline-flex min-w-0 items-center gap-1.5\">\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: resolveCssColor(s.color) }}\n aria-hidden\n />\n <span className=\"truncate text-foreground\">{s.label}</span>\n {v !== undefined ? (\n <span className=\"tabular-nums\">{formatUnit(Math.abs(v), s.unit)}</span>\n ) : null}\n </li>\n );\n })}\n {referenceLines.map((ref, i) => (\n <li key={`ref-${i}`} className=\"inline-flex min-w-0 items-center gap-1.5\">\n <span\n className=\"h-0.5 w-2.5 shrink-0\"\n style={{ backgroundColor: referenceColor(ref) }}\n aria-hidden\n />\n <span className=\"truncate text-foreground\">{ref.label}</span>\n <span className=\"tabular-nums\">{formatUnit(ref.value, ref.unit ?? unit)}</span>\n </li>\n ))}\n </ul>\n );\n}\n\n/** Breakdown header readout: `used / total · pct%`, where used is the sum of segments. */\nfunction BreakdownReadout({\n total,\n unit,\n items,\n}: {\n total: number;\n unit: string | undefined;\n items: BreakdownItem[];\n}) {\n const used = items.reduce((acc, it) => acc + it.value, 0);\n const pct = total > 0 ? Math.min(100, (used / total) * 100) : 0;\n return (\n <span className=\"font-mono tabular-nums\">\n {formatUnit(used, unit)} / {formatUnit(total, unit)} · {pct.toFixed(0)}%\n </span>\n );\n}\n\n/** Instantaneous stacked bar + legend list. The expand button (in the panel header) opens the graph. */\nfunction BreakdownView({\n total,\n unit,\n items,\n}: {\n total: number;\n unit: string | undefined;\n items: BreakdownItem[];\n}) {\n const segments: ProgressSegment[] = items.map((it) => ({\n count: it.value,\n color: it.color,\n label: it.label,\n }));\n return (\n <div className=\"flex flex-col gap-2\">\n <ProgressBar segments={segments} total={total > 0 ? total : 1} height=\"h-3\" />\n {items.length > 0 ? (\n <ul className=\"grid grid-cols-1 gap-y-1 text-xs\">\n {items.map((it) => (\n <li key={it.key} className=\"flex items-center gap-2\">\n <span\n className=\"h-2 w-2 shrink-0 rounded-full\"\n style={{ backgroundColor: resolveCssColor(it.color) }}\n aria-hidden\n />\n <span className=\"min-w-0 flex-1 truncate font-mono text-muted-foreground\">\n {it.label}\n </span>\n <span className=\"font-mono tabular-nums text-foreground\">\n {formatUnit(it.value, it.unit ?? unit)}\n </span>\n <span className=\"w-10 text-right font-mono tabular-nums text-muted-foreground\">\n {it.percent.toFixed(0)}%\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n\ntype ChartVariant = \"area\" | \"line\" | \"stacked\";\n\nfunction Chart({\n rows,\n series,\n variant,\n unit,\n showZeroLine,\n referenceLines,\n}: {\n rows: MergedRow[];\n series: ResolvedSeries[];\n variant: ChartVariant;\n unit: string | undefined;\n showZeroLine: boolean;\n referenceLines: TimeseriesReferenceLine[];\n}) {\n const stacked = variant === \"stacked\";\n // Mirrored series carry negative values; show magnitudes on axis and tooltip.\n const seriesByKey = new Map(series.map((s) => [s.key, s]));\n // recharts needs a CSS color value; a series may carry a Tailwind class.\n const strokeOf = (s: ResolvedSeries) => resolveCssColor(s.color);\n // Distinct units map to a left and (when present) right axis; each series draws\n // against the axis for its unit so mixed-unit panels keep independent scales.\n const { axes: yAxes, axisOfKey } = useMemo(() => assignAxes(series), [series]);\n const axisOf = (key: string): \"left\" | \"right\" => axisOfKey.get(key) ?? \"left\";\n // A reference line draws against the axis formatting its unit, else the left.\n const axisOfUnit = (u: string | undefined): \"left\" | \"right\" =>\n yAxes.find((ax) => ax.unit === u)?.id ?? \"left\";\n const axes = (\n <>\n <CartesianGrid strokeOpacity={0.12} vertical={false} />\n <XAxis\n dataKey=\"at\"\n tickFormatter={formatClock}\n tick={{ fontSize: 10 }}\n stroke=\"currentColor\"\n strokeOpacity={0.35}\n minTickGap={32}\n />\n {yAxes.map((ax) => (\n <YAxis\n key={ax.id}\n yAxisId={ax.id}\n orientation={ax.orientation}\n tick={{ fontSize: 10 }}\n stroke=\"currentColor\"\n strokeOpacity={0.35}\n width={40}\n tickFormatter={(v: number) => formatUnit(Math.abs(v), ax.unit)}\n />\n ))}\n <Tooltip\n labelFormatter={(label) => formatClock(String(label))}\n formatter={(value, name) => {\n const s = seriesByKey.get(String(name));\n return [formatUnit(Math.abs(Number(value) || 0), s?.unit ?? unit), s?.label ?? String(name)];\n }}\n contentStyle={{ fontSize: 12 }}\n />\n {showZeroLine ? (\n <ReferenceLine yAxisId=\"left\" y={0} stroke=\"currentColor\" strokeOpacity={0.35} />\n ) : null}\n {referenceLines.map((ref, i) => (\n <ReferenceLine\n key={`ref-${i}`}\n yAxisId={axisOfUnit(ref.unit ?? unit)}\n y={ref.value}\n stroke={referenceColor(ref)}\n strokeDasharray=\"4 4\"\n ifOverflow=\"extendDomain\"\n />\n ))}\n </>\n );\n\n return (\n <ResponsiveContainer width=\"100%\" height=\"100%\">\n {variant === \"line\" ? (\n <LineChart data={rows} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>\n {axes}\n {series.map((s) => (\n <Line\n key={s.key}\n yAxisId={axisOf(s.key)}\n type=\"monotone\"\n dataKey={s.key}\n name={s.key}\n stroke={strokeOf(s)}\n strokeWidth={2}\n dot={false}\n connectNulls\n isAnimationActive={false}\n />\n ))}\n </LineChart>\n ) : (\n <AreaChart data={rows} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>\n {axes}\n {series.map((s) => (\n <Area\n key={s.key}\n yAxisId={axisOf(s.key)}\n type=\"monotone\"\n dataKey={s.key}\n name={s.key}\n stroke={strokeOf(s)}\n fill={strokeOf(s)}\n {...(stacked ? { stackId: `stack-${axisOf(s.key)}` } : {})}\n fillOpacity={stacked ? 0.7 : 0.12}\n strokeWidth={stacked ? 0 : 2}\n connectNulls\n isAnimationActive={false}\n />\n ))}\n </AreaChart>\n )}\n </ResponsiveContainer>\n );\n}\n\nfunction PanelMessage({ children }: { children: React.ReactNode }) {\n return (\n <div className=\"flex h-full items-center justify-center text-xs text-muted-foreground\">\n {children}\n </div>\n );\n}\n\nfunction PanelIcon({ icon }: { icon: string | StaticIconComponent }) {\n if (typeof icon === \"string\") {\n return <Icon name={icon} width={16} height={16} className=\"text-muted-foreground\" />;\n }\n return <Icon icon={icon} width={16} height={16} className=\"text-muted-foreground\" />;\n}\n"],"names":["defaultTimeseriesFetcher","useState","useMemo","resolveSeries","useTimeseriesQueries","_a","mergeSeries","resolveBreakdown","cn","jsxs","jsx","Icon","UiFullscreen","Modal","latestOf","formatUnit","resolveCssColor","ProgressBar","assignAxes","Fragment","CartesianGrid","XAxis","YAxis","Tooltip","ReferenceLine","ResponsiveContainer","LineChart","Line","AreaChart","Area"],"mappings":";;;;;;;;;;;;;;;AA+CA,SAAS,YAAY,IAAoB;AACvC,QAAM,OAAO,IAAI,KAAK,EAAE;AACxB,MAAI,OAAO,MAAM,KAAK,QAAA,CAAS,EAAG,QAAO;AACzC,SAAO,KAAK,mBAAmB,IAAI,EAAE,MAAM,WAAW,QAAQ,WAAW;AAC3E;AAEO,SAAS,gBAAgB,OAA6B;;AAC3D,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,SAAS;AAAA,IACT,UAAU;AAAA,IACV,aAAa;AAAA,IACb;AAAA,IACA;AAAA,IACA,UAAUA,gBAAAA;AAAAA,IACV;AAAA,EAAA,IACE;AACJ,QAAM,cAAc,YAAY;AAGhC,QAAM,eAA6B,cAAc,SAAS;AAC1D,QAAM,gBAAgB,MAAM,iBAAiB;AAE7C,QAAM,CAAC,UAAU,WAAW,IAAIC,MAAAA,SAAS,KAAK;AAE9C,QAAM,SAASC,MAAAA,QAAQ,MAAMC,sBAAAA,cAAc,KAAK,GAAG,CAAC,KAAK,CAAC;AAC1D,QAAM,UAAUC,gBAAAA;AAAAA,IACd,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,MAAM,EAAE,KAAA,EAAO;AAAA,IAC/C,EAAE,SAAS,MAAM,WAAW,IAAI,OAAO,WAAW,QAAA;AAAA,EAAQ;AAE5D,QAAM,OAAO,kBAAkB,CAAA;AAE/B,QAAM,UAAU,QAAQ,KAAK,CAAC,MAAM,EAAE,OAAO;AAC7C,QAAM,SAAQ,aAAQ,KAAK,CAAC,MAAM,EAAE,OAAO,MAA7B,mBAAgC;AAC9C,QAAM,YAAY,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,KAAK,QAAQ,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI;AAClF,QAAM,iBAAiB,QAAQ,IAAI,CAAC;;AAAM,YAAAC,MAAA,EAAE,SAAF,gBAAAA,IAAQ;AAAA,GAAM;AACxD,QAAM,OAAOH,cAAQ,MAAMI,kCAAY,QAAQ,cAAc,GAAG,CAAC,QAAQ,cAAc,CAAC;AACxF,QAAM,cAAc,OAAO,KAAK,CAAC,MAAM,EAAE,SAAS;AAClD,QAAM,YAAYJ,MAAAA;AAAAA,IAChB,MAAO,cAAcK,sBAAAA,iBAAiB,QAAQ,MAAM,KAAK,IAAI;AAAA,IAC7D,CAAC,aAAa,QAAQ,MAAM,KAAK;AAAA,EAAA;AAKnC,QAAM,YAAY,cAAc,KAAK,UAAU;AAE/C,yCACG,OAAA,EAAI,WAAWC,MAAAA,GAAG,6DAA6D,SAAS,GACvF,UAAA;AAAA,IAAAC,2BAAAA,KAAC,OAAA,EAAI,WAAU,yEACb,UAAA;AAAA,MAAAA,2BAAAA,KAAC,QAAA,EAAK,WAAU,4CACb,UAAA;AAAA,QAAA,OAAOC,2BAAAA,IAAC,WAAA,EAAU,KAAA,CAAY,IAAK;AAAA,QACpCA,2BAAAA,IAAC,QAAA,EAAK,WAAU,wCAAwC,UAAA,MAAA,CAAM;AAAA,MAAA,GAChE;AAAA,MACAD,2BAAAA,KAAC,QAAA,EAAK,WAAU,6CACb,UAAA;AAAA,QAAA,YACCC,2BAAAA,IAAC,kBAAA,EAAiB,OAAO,UAAU,OAAO,MAAY,OAAO,UAAU,MAAA,CAAO,IAE9EA,2BAAAA,IAAC,eAAA,EAAc,QAAgB,MAAY;AAAA,QAE5C,YACCA,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,MAAK;AAAA,YACL,cAAW;AAAA,YACX,SAAS,MAAM,YAAY,IAAI;AAAA,YAC/B,WAAU;AAAA,YAEV,yCAACC,KAAAA,MAAA,EAAK,MAAMC,aAAAA,cAAc,OAAO,IAAI,QAAQ,GAAA,CAAI;AAAA,UAAA;AAAA,QAAA,IAEjD;AAAA,MAAA,EAAA,CACN;AAAA,IAAA,GACF;AAAA,mCACC,OAAA,EAAI,WAAU,QAAO,OAAO,YAAY,SAAY,EAAE,OAAA,GACpD,sBACCF,2BAAAA,IAAC,eAAA,EAAc,OAAO,UAAU,OAAO,MAAY,OAAO,UAAU,MAAA,CAAO,IACzE,UACFA,2BAAAA,IAAC,cAAA,EAAe,0CAAiB,YAAW,wBAAA,CAAwB,IAClE,YACFA,+BAAC,cAAA,EAAa,UAAA,WAAA,CAAQ,IACpB,KAAK,SAAS,IAChBA,2BAAAA,IAAC,cAAA,EAAa,8BAAgB,IAE9BA,2BAAAA;AAAAA,MAAC;AAAA,MAAA;AAAA,QACC;AAAA,QACA;AAAA,QACA,SAAS;AAAA,QACT;AAAA,QACA,cAAc;AAAA,QACd,gBAAgB;AAAA,MAAA;AAAA,IAAA,GAGtB;AAAA,IACC,CAAC,aAAa,CAAC,WAAW,CAAC,aAAa,KAAK,UAAU,KAAK,OAAO,SAAS,KAAK,SAAS,IACzFA,2BAAAA,IAAC,aAAA,EAAY,QAAgB,MAAY,gBAAgB,MAAM,KAAA,CAAY,IACzE;AAAA,IACH,YACCD,2BAAAA,KAACI,MAAAA,OAAA,EAAM,MAAM,UAAU,SAAS,MAAM,YAAY,KAAK,GAAG,OAAc,MAAK,MAC3E,UAAA;AAAA,MAAAH,2BAAAA,IAAC,OAAA,EAAI,OAAO,EAAE,QAAQ,OACpB,UAAAA,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT;AAAA,UACA,cAAc;AAAA,UACd,gBAAgB;AAAA,QAAA;AAAA,MAAA,GAEpB;AAAA,MACC,OAAO,SAAS,KAAK,SAAS,IAC7BA,2BAAAA,IAAC,aAAA,EAAY,QAAgB,MAAY,gBAAgB,MAAM,KAAA,CAAY,IACzE;AAAA,IAAA,EAAA,CACN,IACE;AAAA,EAAA,GACN;AAEJ;AAOA,SAAS,cAAc,EAAE,QAAQ,QAAyD;AACxF,QAAM,OAAO,OAAO,WAAW,IAAI,OAAO,CAAC,IAAI;AAC/C,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,IAAII,qBAAAA,SAAS,MAAM,KAAK,GAAG;AACjC,MAAI,MAAM,OAAW,QAAO;AAC5B,SAAOJ,2BAAAA,IAAC,QAAA,EAAK,WAAU,gBAAgB,UAAAK,OAAAA,WAAW,KAAK,IAAI,CAAC,GAAG,KAAK,IAAI,EAAA,CAAE;AAC5E;AAEA,MAAM,uBAAuB;AAE7B,SAAS,eAAe,KAAsC;AAC5D,SAAOC,sBAAgB,IAAI,SAAS,oBAAoB;AAC1D;AAMA,SAAS,YAAY;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAKG;AACD,SACEP,2BAAAA,KAAC,MAAA,EAAG,WAAU,uEACX,UAAA;AAAA,IAAA,OAAO,IAAI,CAAC,MAAM;AACjB,YAAM,IAAIK,qBAAAA,SAAS,MAAM,EAAE,GAAG;AAC9B,aACEL,2BAAAA,KAAC,MAAA,EAAe,WAAU,4CACxB,UAAA;AAAA,QAAAC,2BAAAA;AAAAA,UAAC;AAAA,UAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO,EAAE,iBAAiBM,MAAAA,gBAAgB,EAAE,KAAK,EAAA;AAAA,YACjD,eAAW;AAAA,UAAA;AAAA,QAAA;AAAA,QAEbN,2BAAAA,IAAC,QAAA,EAAK,WAAU,4BAA4B,YAAE,OAAM;AAAA,QACnD,MAAM,SACLA,2BAAAA,IAAC,QAAA,EAAK,WAAU,gBAAgB,UAAAK,OAAAA,WAAW,KAAK,IAAI,CAAC,GAAG,EAAE,IAAI,GAAE,IAC9D;AAAA,MAAA,EAAA,GATG,EAAE,GAUX;AAAA,IAEJ,CAAC;AAAA,IACA,eAAe,IAAI,CAAC,KAAK,MACxBN,gCAAC,MAAA,EAAoB,WAAU,4CAC7B,UAAA;AAAA,MAAAC,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,iBAAiB,eAAe,GAAG,EAAA;AAAA,UAC5C,eAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAEbA,2BAAAA,IAAC,QAAA,EAAK,WAAU,4BAA4B,cAAI,OAAM;AAAA,MACtDA,2BAAAA,IAAC,QAAA,EAAK,WAAU,gBAAgB,UAAAK,kBAAW,IAAI,OAAO,IAAI,QAAQ,IAAI,EAAA,CAAE;AAAA,IAAA,EAAA,GAPjE,OAAO,CAAC,EAQjB,CACD;AAAA,EAAA,GACH;AAEJ;AAGA,SAAS,iBAAiB;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,OAAO,MAAM,OAAO,CAAC,KAAK,OAAO,MAAM,GAAG,OAAO,CAAC;AACxD,QAAM,MAAM,QAAQ,IAAI,KAAK,IAAI,KAAM,OAAO,QAAS,GAAG,IAAI;AAC9D,SACEN,2BAAAA,KAAC,QAAA,EAAK,WAAU,0BACb,UAAA;AAAA,IAAAM,OAAAA,WAAW,MAAM,IAAI;AAAA,IAAE;AAAA,IAAIA,OAAAA,WAAW,OAAO,IAAI;AAAA,IAAE;AAAA,IAAI,IAAI,QAAQ,CAAC;AAAA,IAAE;AAAA,EAAA,GACzE;AAEJ;AAGA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,WAA8B,MAAM,IAAI,CAAC,QAAQ;AAAA,IACrD,OAAO,GAAG;AAAA,IACV,OAAO,GAAG;AAAA,IACV,OAAO,GAAG;AAAA,EAAA,EACV;AACF,SACEN,2BAAAA,KAAC,OAAA,EAAI,WAAU,uBACb,UAAA;AAAA,IAAAC,2BAAAA,IAACO,YAAAA,aAAA,EAAY,UAAoB,OAAO,QAAQ,IAAI,QAAQ,GAAG,QAAO,MAAA,CAAM;AAAA,IAC3E,MAAM,SAAS,IACdP,2BAAAA,IAAC,QAAG,WAAU,oCACX,UAAA,MAAM,IAAI,CAAC,OACVD,2BAAAA,KAAC,MAAA,EAAgB,WAAU,2BACzB,UAAA;AAAA,MAAAC,2BAAAA;AAAAA,QAAC;AAAA,QAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO,EAAE,iBAAiBM,MAAAA,gBAAgB,GAAG,KAAK,EAAA;AAAA,UAClD,eAAW;AAAA,QAAA;AAAA,MAAA;AAAA,MAEbN,2BAAAA,IAAC,QAAA,EAAK,WAAU,2DACb,aAAG,OACN;AAAA,MACAA,2BAAAA,IAAC,QAAA,EAAK,WAAU,0CACb,UAAAK,OAAAA,WAAW,GAAG,OAAO,GAAG,QAAQ,IAAI,EAAA,CACvC;AAAA,MACAN,2BAAAA,KAAC,QAAA,EAAK,WAAU,gEACb,UAAA;AAAA,QAAA,GAAG,QAAQ,QAAQ,CAAC;AAAA,QAAE;AAAA,MAAA,EAAA,CACzB;AAAA,IAAA,EAAA,GAdO,GAAG,GAeZ,CACD,EAAA,CACH,IACE;AAAA,EAAA,GACN;AAEJ;AAIA,SAAS,MAAM;AAAA,EACb;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,UAAU,YAAY;AAE5B,QAAM,cAAc,IAAI,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAEzD,QAAM,WAAW,CAAC,MAAsBO,MAAAA,gBAAgB,EAAE,KAAK;AAG/D,QAAM,EAAE,MAAM,OAAO,cAAcd,MAAAA,QAAQ,MAAMgB,qBAAAA,WAAW,MAAM,GAAG,CAAC,MAAM,CAAC;AAC7E,QAAM,SAAS,CAAC,QAAkC,UAAU,IAAI,GAAG,KAAK;AAExE,QAAM,aAAa,CAAC,MAAA;;AAClB,wBAAM,KAAK,CAAC,OAAO,GAAG,SAAS,CAAC,MAAhC,mBAAmC,OAAM;AAAA;AAC3C,QAAM,OACJT,2BAAAA,KAAAU,WAAAA,UAAA,EACE,UAAA;AAAA,IAAAT,2BAAAA,IAACU,SAAAA,eAAA,EAAc,eAAe,MAAM,UAAU,OAAO;AAAA,IACrDV,2BAAAA;AAAAA,MAACW,SAAAA;AAAAA,MAAA;AAAA,QACC,SAAQ;AAAA,QACR,eAAe;AAAA,QACf,MAAM,EAAE,UAAU,GAAA;AAAA,QAClB,QAAO;AAAA,QACP,eAAe;AAAA,QACf,YAAY;AAAA,MAAA;AAAA,IAAA;AAAA,IAEb,MAAM,IAAI,CAAC,OACVX,2BAAAA;AAAAA,MAACY,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,GAAG;AAAA,QACZ,aAAa,GAAG;AAAA,QAChB,MAAM,EAAE,UAAU,GAAA;AAAA,QAClB,QAAO;AAAA,QACP,eAAe;AAAA,QACf,OAAO;AAAA,QACP,eAAe,CAAC,MAAcP,kBAAW,KAAK,IAAI,CAAC,GAAG,GAAG,IAAI;AAAA,MAAA;AAAA,MAPxD,GAAG;AAAA,IAAA,CASX;AAAA,IACDL,2BAAAA;AAAAA,MAACa,SAAAA;AAAAA,MAAA;AAAA,QACC,gBAAgB,CAAC,UAAU,YAAY,OAAO,KAAK,CAAC;AAAA,QACpD,WAAW,CAAC,OAAO,SAAS;AAC1B,gBAAM,IAAI,YAAY,IAAI,OAAO,IAAI,CAAC;AACtC,iBAAO,CAACR,OAAAA,WAAW,KAAK,IAAI,OAAO,KAAK,KAAK,CAAC,IAAG,uBAAG,SAAQ,IAAI,IAAG,uBAAG,UAAS,OAAO,IAAI,CAAC;AAAA,QAC7F;AAAA,QACA,cAAc,EAAE,UAAU,GAAA;AAAA,MAAG;AAAA,IAAA;AAAA,IAE9B,eACCL,2BAAAA,IAACc,SAAAA,eAAA,EAAc,SAAQ,QAAO,GAAG,GAAG,QAAO,gBAAe,eAAe,KAAA,CAAM,IAC7E;AAAA,IACH,eAAe,IAAI,CAAC,KAAK,MACxBd,2BAAAA;AAAAA,MAACc,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,WAAW,IAAI,QAAQ,IAAI;AAAA,QACpC,GAAG,IAAI;AAAA,QACP,QAAQ,eAAe,GAAG;AAAA,QAC1B,iBAAgB;AAAA,QAChB,YAAW;AAAA,MAAA;AAAA,MALN,OAAO,CAAC;AAAA,IAAA,CAOhB;AAAA,EAAA,GACH;AAGF,SACEd,+BAACe,SAAAA,uBAAoB,OAAM,QAAO,QAAO,QACtC,UAAA,YAAY,SACXhB,2BAAAA,KAACiB,SAAAA,WAAA,EAAU,MAAM,MAAM,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,EAAA,GACjE,UAAA;AAAA,IAAA;AAAA,IACA,OAAO,IAAI,CAAC,MACXhB,2BAAAA;AAAAA,MAACiB,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,OAAO,EAAE,GAAG;AAAA,QACrB,MAAK;AAAA,QACL,SAAS,EAAE;AAAA,QACX,MAAM,EAAE;AAAA,QACR,QAAQ,SAAS,CAAC;AAAA,QAClB,aAAa;AAAA,QACb,KAAK;AAAA,QACL,cAAY;AAAA,QACZ,mBAAmB;AAAA,MAAA;AAAA,MATd,EAAE;AAAA,IAAA,CAWV;AAAA,EAAA,EAAA,CACH,IAEAlB,2BAAAA,KAACmB,SAAAA,WAAA,EAAU,MAAM,MAAM,QAAQ,EAAE,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,KACjE,UAAA;AAAA,IAAA;AAAA,IACA,OAAO,IAAI,CAAC,MACXlB,2BAAAA;AAAAA,MAACmB,SAAAA;AAAAA,MAAA;AAAA,QAEC,SAAS,OAAO,EAAE,GAAG;AAAA,QACrB,MAAK;AAAA,QACL,SAAS,EAAE;AAAA,QACX,MAAM,EAAE;AAAA,QACR,QAAQ,SAAS,CAAC;AAAA,QAClB,MAAM,SAAS,CAAC;AAAA,QACf,GAAI,UAAU,EAAE,SAAS,SAAS,OAAO,EAAE,GAAG,CAAC,GAAA,IAAO,CAAA;AAAA,QACvD,aAAa,UAAU,MAAM;AAAA,QAC7B,aAAa,UAAU,IAAI;AAAA,QAC3B,cAAY;AAAA,QACZ,mBAAmB;AAAA,MAAA;AAAA,MAXd,EAAE;AAAA,IAAA,CAaV;AAAA,EAAA,EAAA,CACH,EAAA,CAEJ;AAEJ;AAEA,SAAS,aAAa,EAAE,YAA2C;AACjE,SACEnB,2BAAAA,IAAC,OAAA,EAAI,WAAU,yEACZ,SAAA,CACH;AAEJ;AAEA,SAAS,UAAU,EAAE,QAAgD;AACnE,MAAI,OAAO,SAAS,UAAU;AAC5B,WAAOA,+BAACC,KAAAA,QAAK,MAAM,MAAM,OAAO,IAAI,QAAQ,IAAI,WAAU,wBAAA,CAAwB;AAAA,EACpF;AACA,SAAOD,+BAACC,KAAAA,QAAK,MAAY,OAAO,IAAI,QAAQ,IAAI,WAAU,yBAAwB;AACpF;;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TimeseriesPanelProps } from './TimeseriesPanel.model';
|
|
2
2
|
export type { AxisAssignment, AxisOrientation, AxisSpec } from './TimeseriesPanel.axes';
|
|
3
|
-
export type { BreakdownItem, MergedRow, ResolvedSeries, TimeseriesPanelProps, TimeseriesPoint, TimeseriesResponse, TimeseriesSeries, } from './TimeseriesPanel.model';
|
|
3
|
+
export type { BreakdownItem, MergedRow, ResolvedSeries, SeriesLoadContext, SeriesLoader, TimeseriesPanelProps, TimeseriesPoint, TimeseriesReferenceLine, TimeseriesResponse, TimeseriesSeries, } from './TimeseriesPanel.model';
|
|
4
4
|
export declare function TimeseriesPanel(props: TimeseriesPanelProps): import("react").JSX.Element;
|
|
5
5
|
//# sourceMappingURL=TimeseriesPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimeseriesPanel.d.ts","sourceRoot":"","sources":["../../src/data/TimeseriesPanel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TimeseriesPanel.d.ts","sourceRoot":"","sources":["../../src/data/TimeseriesPanel.tsx"],"names":[],"mappings":"AAqBA,OAAO,EAOL,KAAK,oBAAoB,EAE1B,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACxF,YAAY,EACV,aAAa,EACb,SAAS,EACT,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,oBAAoB,EACpB,eAAe,EACf,uBAAuB,EACvB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,yBAAyB,CAAC;AAQjC,wBAAgB,eAAe,CAAC,KAAK,EAAE,oBAAoB,+BAgH1D"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useQueries } from "@tanstack/react-query";
|
|
3
2
|
import { useState, useMemo } from "react";
|
|
4
3
|
import { CartesianGrid, XAxis, YAxis, Tooltip, ReferenceLine, ResponsiveContainer, LineChart, Line, AreaChart, Area } from "recharts";
|
|
5
4
|
import { cn } from "../lib/utils.js";
|
|
@@ -10,12 +9,8 @@ import { ProgressBar } from "./ProgressBar.js";
|
|
|
10
9
|
import { Icon } from "./Icon.js";
|
|
11
10
|
import { latestOf, assignAxes } from "./TimeseriesPanel.axes.js";
|
|
12
11
|
import { resolveSeries, mergeSeries, resolveBreakdown } from "./TimeseriesPanel.model.js";
|
|
12
|
+
import { defaultTimeseriesFetcher, useTimeseriesQueries } from "./timeseries-query.js";
|
|
13
13
|
import { UiFullscreen } from "../icons/components/UiFullscreen.js";
|
|
14
|
-
const defaultFetcher = async (url) => {
|
|
15
|
-
const res = await fetch(url);
|
|
16
|
-
if (!res.ok) throw new Error(`metrics request failed: ${res.status}`);
|
|
17
|
-
return res.json();
|
|
18
|
-
};
|
|
19
14
|
function formatClock(at) {
|
|
20
15
|
const date = new Date(at);
|
|
21
16
|
if (Number.isNaN(date.getTime())) return "";
|
|
@@ -33,7 +28,8 @@ function TimeseriesPanel(props) {
|
|
|
33
28
|
variant = "area",
|
|
34
29
|
expandable = true,
|
|
35
30
|
total,
|
|
36
|
-
|
|
31
|
+
referenceLines,
|
|
32
|
+
fetcher = defaultTimeseriesFetcher,
|
|
37
33
|
className
|
|
38
34
|
} = props;
|
|
39
35
|
const isBreakdown = variant === "breakdown";
|
|
@@ -41,23 +37,11 @@ function TimeseriesPanel(props) {
|
|
|
41
37
|
const expandVariant = props.expandVariant ?? chartVariant;
|
|
42
38
|
const [expanded, setExpanded] = useState(false);
|
|
43
39
|
const series = useMemo(() => resolveSeries(props), [props]);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (range) u.searchParams.set("since", range);
|
|
48
|
-
return u.pathname + u.search;
|
|
49
|
-
}),
|
|
50
|
-
[series, range]
|
|
40
|
+
const results = useTimeseriesQueries(
|
|
41
|
+
series.map((s) => ({ id: s.key, load: s.load })),
|
|
42
|
+
{ baseUrl: props.baseUrl ?? "", range, refreshMs, fetcher }
|
|
51
43
|
);
|
|
52
|
-
const
|
|
53
|
-
queries: requestUrls.map((requestUrl) => ({
|
|
54
|
-
queryKey: ["timeseries", requestUrl],
|
|
55
|
-
queryFn: () => fetcher(requestUrl),
|
|
56
|
-
refetchInterval: refreshMs > 0 ? refreshMs : false,
|
|
57
|
-
staleTime: 0,
|
|
58
|
-
retry: 0
|
|
59
|
-
}))
|
|
60
|
-
});
|
|
44
|
+
const refs = referenceLines ?? [];
|
|
61
45
|
const isError = results.some((r) => r.isError);
|
|
62
46
|
const error = (_a = results.find((r) => r.isError)) == null ? void 0 : _a.error;
|
|
63
47
|
const isLoading = results.some((r) => r.isLoading) && results.every((r) => !r.data);
|
|
@@ -92,11 +76,31 @@ function TimeseriesPanel(props) {
|
|
|
92
76
|
) : null
|
|
93
77
|
] })
|
|
94
78
|
] }),
|
|
95
|
-
/* @__PURE__ */ jsx("div", { className: "mt-2", style: breakdown ? void 0 : { height }, children: breakdown ? /* @__PURE__ */ jsx(BreakdownView, { total: breakdown.total, unit, items: breakdown.items }) : isError ? /* @__PURE__ */ jsx(PanelMessage, { children: (error == null ? void 0 : error.message) ?? "Failed to load metric" }) : isLoading ? /* @__PURE__ */ jsx(PanelMessage, { children: "Loading…" }) : rows.length < 2 ? /* @__PURE__ */ jsx(PanelMessage, { children: "Collecting data…" }) : /* @__PURE__ */ jsx(
|
|
96
|
-
|
|
79
|
+
/* @__PURE__ */ jsx("div", { className: "mt-2", style: breakdown ? void 0 : { height }, children: breakdown ? /* @__PURE__ */ jsx(BreakdownView, { total: breakdown.total, unit, items: breakdown.items }) : isError ? /* @__PURE__ */ jsx(PanelMessage, { children: (error == null ? void 0 : error.message) ?? "Failed to load metric" }) : isLoading ? /* @__PURE__ */ jsx(PanelMessage, { children: "Loading…" }) : rows.length < 2 ? /* @__PURE__ */ jsx(PanelMessage, { children: "Collecting data…" }) : /* @__PURE__ */ jsx(
|
|
80
|
+
Chart,
|
|
81
|
+
{
|
|
82
|
+
rows,
|
|
83
|
+
series,
|
|
84
|
+
variant: chartVariant,
|
|
85
|
+
unit,
|
|
86
|
+
showZeroLine: hasMirrored,
|
|
87
|
+
referenceLines: refs
|
|
88
|
+
}
|
|
89
|
+
) }),
|
|
90
|
+
!breakdown && !isError && !isLoading && rows.length >= 2 && series.length + refs.length > 1 ? /* @__PURE__ */ jsx(ChartLegend, { series, rows, referenceLines: refs, unit }) : null,
|
|
97
91
|
canExpand ? /* @__PURE__ */ jsxs(Modal, { open: expanded, onClose: () => setExpanded(false), title, size: "xl", children: [
|
|
98
|
-
/* @__PURE__ */ jsx("div", { style: { height: 360 }, children: /* @__PURE__ */ jsx(
|
|
99
|
-
|
|
92
|
+
/* @__PURE__ */ jsx("div", { style: { height: 360 }, children: /* @__PURE__ */ jsx(
|
|
93
|
+
Chart,
|
|
94
|
+
{
|
|
95
|
+
rows,
|
|
96
|
+
series,
|
|
97
|
+
variant: expandVariant,
|
|
98
|
+
unit,
|
|
99
|
+
showZeroLine: hasMirrored,
|
|
100
|
+
referenceLines: refs
|
|
101
|
+
}
|
|
102
|
+
) }),
|
|
103
|
+
series.length + refs.length > 1 ? /* @__PURE__ */ jsx(ChartLegend, { series, rows, referenceLines: refs, unit }) : null
|
|
100
104
|
] }) : null
|
|
101
105
|
] });
|
|
102
106
|
}
|
|
@@ -107,22 +111,45 @@ function LatestReadout({ series, rows }) {
|
|
|
107
111
|
if (v === void 0) return null;
|
|
108
112
|
return /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatUnit(Math.abs(v), only.unit) });
|
|
109
113
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
const REFERENCE_LINE_COLOR = "var(--muted-foreground, #64748b)";
|
|
115
|
+
function referenceColor(ref) {
|
|
116
|
+
return resolveCssColor(ref.color ?? REFERENCE_LINE_COLOR);
|
|
117
|
+
}
|
|
118
|
+
function ChartLegend({
|
|
119
|
+
series,
|
|
120
|
+
rows,
|
|
121
|
+
referenceLines,
|
|
122
|
+
unit
|
|
123
|
+
}) {
|
|
124
|
+
return /* @__PURE__ */ jsxs("ul", { className: "mt-1.5 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
|
|
125
|
+
series.map((s) => {
|
|
126
|
+
const v = latestOf(rows, s.key);
|
|
127
|
+
return /* @__PURE__ */ jsxs("li", { className: "inline-flex min-w-0 items-center gap-1.5", children: [
|
|
128
|
+
/* @__PURE__ */ jsx(
|
|
129
|
+
"span",
|
|
130
|
+
{
|
|
131
|
+
className: "h-2 w-2 shrink-0 rounded-full",
|
|
132
|
+
style: { backgroundColor: resolveCssColor(s.color) },
|
|
133
|
+
"aria-hidden": true
|
|
134
|
+
}
|
|
135
|
+
),
|
|
136
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: s.label }),
|
|
137
|
+
v !== void 0 ? /* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatUnit(Math.abs(v), s.unit) }) : null
|
|
138
|
+
] }, s.key);
|
|
139
|
+
}),
|
|
140
|
+
referenceLines.map((ref, i) => /* @__PURE__ */ jsxs("li", { className: "inline-flex min-w-0 items-center gap-1.5", children: [
|
|
114
141
|
/* @__PURE__ */ jsx(
|
|
115
142
|
"span",
|
|
116
143
|
{
|
|
117
|
-
className: "h-
|
|
118
|
-
style: { backgroundColor:
|
|
144
|
+
className: "h-0.5 w-2.5 shrink-0",
|
|
145
|
+
style: { backgroundColor: referenceColor(ref) },
|
|
119
146
|
"aria-hidden": true
|
|
120
147
|
}
|
|
121
148
|
),
|
|
122
|
-
/* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children:
|
|
123
|
-
|
|
124
|
-
] },
|
|
125
|
-
|
|
149
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-foreground", children: ref.label }),
|
|
150
|
+
/* @__PURE__ */ jsx("span", { className: "tabular-nums", children: formatUnit(ref.value, ref.unit ?? unit) })
|
|
151
|
+
] }, `ref-${i}`))
|
|
152
|
+
] });
|
|
126
153
|
}
|
|
127
154
|
function BreakdownReadout({
|
|
128
155
|
total,
|
|
@@ -175,13 +202,18 @@ function Chart({
|
|
|
175
202
|
series,
|
|
176
203
|
variant,
|
|
177
204
|
unit,
|
|
178
|
-
showZeroLine
|
|
205
|
+
showZeroLine,
|
|
206
|
+
referenceLines
|
|
179
207
|
}) {
|
|
180
208
|
const stacked = variant === "stacked";
|
|
181
209
|
const seriesByKey = new Map(series.map((s) => [s.key, s]));
|
|
182
210
|
const strokeOf = (s) => resolveCssColor(s.color);
|
|
183
211
|
const { axes: yAxes, axisOfKey } = useMemo(() => assignAxes(series), [series]);
|
|
184
212
|
const axisOf = (key) => axisOfKey.get(key) ?? "left";
|
|
213
|
+
const axisOfUnit = (u) => {
|
|
214
|
+
var _a;
|
|
215
|
+
return ((_a = yAxes.find((ax) => ax.unit === u)) == null ? void 0 : _a.id) ?? "left";
|
|
216
|
+
};
|
|
185
217
|
const axes = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
186
218
|
/* @__PURE__ */ jsx(CartesianGrid, { strokeOpacity: 0.12, vertical: false }),
|
|
187
219
|
/* @__PURE__ */ jsx(
|
|
@@ -219,7 +251,18 @@ function Chart({
|
|
|
219
251
|
contentStyle: { fontSize: 12 }
|
|
220
252
|
}
|
|
221
253
|
),
|
|
222
|
-
showZeroLine ? /* @__PURE__ */ jsx(ReferenceLine, { yAxisId: "left", y: 0, stroke: "currentColor", strokeOpacity: 0.35 }) : null
|
|
254
|
+
showZeroLine ? /* @__PURE__ */ jsx(ReferenceLine, { yAxisId: "left", y: 0, stroke: "currentColor", strokeOpacity: 0.35 }) : null,
|
|
255
|
+
referenceLines.map((ref, i) => /* @__PURE__ */ jsx(
|
|
256
|
+
ReferenceLine,
|
|
257
|
+
{
|
|
258
|
+
yAxisId: axisOfUnit(ref.unit ?? unit),
|
|
259
|
+
y: ref.value,
|
|
260
|
+
stroke: referenceColor(ref),
|
|
261
|
+
strokeDasharray: "4 4",
|
|
262
|
+
ifOverflow: "extendDomain"
|
|
263
|
+
},
|
|
264
|
+
`ref-${i}`
|
|
265
|
+
))
|
|
223
266
|
] });
|
|
224
267
|
return /* @__PURE__ */ jsx(ResponsiveContainer, { width: "100%", height: "100%", children: variant === "line" ? /* @__PURE__ */ jsxs(LineChart, { data: rows, margin: { top: 4, right: 8, bottom: 0, left: 0 }, children: [
|
|
225
268
|
axes,
|