)
}
diff --git a/app/components/TimeSeriesChart.tsx b/app/components/TimeSeriesChart.tsx
index 5fd8f6c2c4..e15b05bed9 100644
--- a/app/components/TimeSeriesChart.tsx
+++ b/app/components/TimeSeriesChart.tsx
@@ -7,7 +7,7 @@
*/
import cn from 'classnames'
import { format } from 'date-fns'
-import { useMemo } from 'react'
+import { useMemo, type ReactNode } from 'react'
import {
Area,
AreaChart,
@@ -20,6 +20,7 @@ import {
import type { TooltipProps } from 'recharts/types/component/Tooltip'
import type { ChartDatum } from '@oxide/api'
+import { Error12Icon } from '@oxide/design-system/icons/react'
// Recharts's built-in ticks behavior is useless and probably broken
/**
@@ -73,6 +74,12 @@ const textMonoMd = {
fill: 'var(--content-quaternary)',
}
+// The length of a character in pixels at 11px with GT America Mono
+// Used for dynamically sizing the yAxis. If this were to fallback
+// the font would likely be thinner than the monospaced character
+// and therefore not overflow
+const TEXT_CHAR_WIDTH = 6.82
+
function renderTooltip(props: TooltipProps, unit?: string) {
const { payload } = props
if (!payload || payload.length < 1) return null
@@ -110,18 +117,55 @@ type TimeSeriesChartProps = {
endTime: Date
unit?: string
yAxisTickFormatter?: (val: number) => string
+ hasBorder?: boolean
+ hasError?: boolean
}
const TICK_COUNT = 6
+const TICK_MARGIN = 8
+const TICK_SIZE = 6
/** Round `value` up to nearest number divisible by `divisor` */
function roundUpToDivBy(value: number, divisor: number) {
return Math.ceil(value / divisor) * divisor
}
-// default export is most convenient for dynamic import
-// eslint-disable-next-line import/no-default-export
-export default function TimeSeriesChart({
+// this top margin is also in the chart, probably want a way of unifying the sizing between the two
+const SkeletonMetric = ({
+ children,
+ shimmer = false,
+ className,
+}: {
+ children: ReactNode
+ shimmer?: boolean
+ className?: string
+}) => (
+
+
+
+ {[...Array(4)].map((_e, i) => (
+
+ ))}
+
+
+ {[...Array(8)].map((_e, i) => (
+
+ ))}
+
+
+
+ {children}
+
+
+)
+
+export function TimeSeriesChart({
className,
data: rawData,
title,
@@ -132,13 +176,17 @@ export default function TimeSeriesChart({
endTime,
unit,
yAxisTickFormatter = (val) => val.toLocaleString(),
+ hasBorder = true,
+ hasError = false,
}: TimeSeriesChartProps) {
// We use the largest data point +20% for the graph scale. !rawData doesn't
// mean it's empty (it will never be empty because we fill in artificial 0s at
// beginning and end), it means the metrics requests haven't come back yet
const maxY = useMemo(() => {
if (!rawData) return null
- const dataMax = Math.max(...rawData.map((datum) => datum.value))
+ const dataMax = Math.max(
+ ...rawData.map((datum) => datum.value).filter((x) => x !== null)
+ )
return roundUpToDivBy(dataMax * 1.2, TICK_COUNT) // avoid uneven ticks
}, [rawData])
@@ -149,18 +197,46 @@ export default function TimeSeriesChart({
? { domain: [0, maxY], ticks: getVerticalTicks(TICK_COUNT, maxY) }
: undefined
+ // We get the longest label length and multiply that with our `TICK_CHAR_WIDTH`
+ // and add the extra space for the tick stroke and spacing
+ // It's possible to get clever and calculate the width using the canvas or font metrics
+ // But our font is monospace so we can just use the length of the text * the baked width of the character
+ const maxLabelLength = yTicks
+ ? Math.max(...yTicks.ticks.map((tick) => yAxisTickFormatter(tick).length))
+ : 0
+ const maxLabelWidth = maxLabelLength * TEXT_CHAR_WIDTH + TICK_SIZE + TICK_MARGIN
+
// falling back here instead of in the parent lets us avoid causing a
// re-render on every render of the parent when the data is undefined
const data = useMemo(() => rawData || [], [rawData])
+ const wrapperClass = cn(className, hasBorder && 'rounded-lg border border-default')
+
+ if (hasError) {
+ return (
+
+
+
+ )
+ }
+
+ if (!data || data.length === 0) {
+ return (
+
+
+
+ )
+ }
+
return (
-
+ {/* temporary until we migrate the old metrics to the new style */}
+
{/* TODO: stop tooltip being focused by default on pageload if nothing else has been clicked */}
@@ -211,3 +290,33 @@ export default function TimeSeriesChart({
)
}
+
+const MetricsLoadingIndicator = () => (
+
+
+
+
+
+)
+
+const MetricsError = () => (
+ <>
+
+
+
+
+
+
Something went wrong
+
+ Please try again. If the problem persists, contact your administrator.
+