diff --git a/app/components/TimeSeriesChart.tsx b/app/components/TimeSeriesChart.tsx
index f20828f46..a1834c034 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,8 +20,7 @@ import {
import type { TooltipProps } from 'recharts/types/component/Tooltip'
import type { ChartDatum } from '@oxide/api'
-
-import { Spinner } from '~/ui/lib/Spinner'
+import { Error12Icon } from '@oxide/design-system/icons/react'
// Recharts's built-in ticks behavior is useless and probably broken
/**
@@ -119,6 +118,7 @@ type TimeSeriesChartProps = {
unit?: string
yAxisTickFormatter?: (val: number) => string
hasBorder?: boolean
+ hasError?: boolean
}
const TICK_COUNT = 6
@@ -130,6 +130,41 @@ function roundUpToDivBy(value: number, divisor: number) {
return Math.ceil(value / divisor) * divisor
}
+// 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}
+
+
+)
+
// default export is most convenient for dynamic import
// eslint-disable-next-line import/no-default-export
export default function TimeSeriesChart({
@@ -144,6 +179,7 @@ export default function TimeSeriesChart({
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
@@ -174,22 +210,44 @@ export default function TimeSeriesChart({
// re-render on every render of the parent when the data is undefined
const data = useMemo(() => rawData || [], [rawData])
- if (!data || data.length === 0) {
+ const wrapperClass = cn(className, hasBorder && 'rounded-lg border border-default')
+
+ if (hasError) {
return (
-
-
-
-
-
+
+ <>
+
+
+
+
+
+
Something went wrong
+
+ Try refreshing the page, or contact your project admin
+