diff --git a/app/components/SystemMetric.tsx b/app/components/SystemMetric.tsx index f24db19427..3b24d367e2 100644 --- a/app/components/SystemMetric.tsx +++ b/app/components/SystemMetric.tsx @@ -72,6 +72,8 @@ export function SystemMetric({ width={480} height={240} interpolation="stepAfter" + startTime={startTime} + endTime={endTime} /> ) diff --git a/app/components/TimeSeriesChart.tsx b/app/components/TimeSeriesChart.tsx index 76d57aa29d..a784829f39 100644 --- a/app/components/TimeSeriesChart.tsx +++ b/app/components/TimeSeriesChart.tsx @@ -1,8 +1,11 @@ +import cn from 'classnames' import { format } from 'date-fns' import { Area, AreaChart, CartesianGrid, + Line, + LineChart, ResponsiveContainer, Tooltip, XAxis, @@ -19,25 +22,42 @@ function getTicks(data: { timestamp: number }[], n: number): number[] { if (data.length === 0) return [] if (n < 2) throw Error('n must be at least 2 because of the start and end ticks') // bring the last tick in a bit from the end - const maxIdx = data.length > 10 ? Math.floor((data.length - 1) * 0.9) : data.length - 1 + const maxIdx = data.length > 10 ? Math.floor((data.length - 1) * 0.8) : data.length - 1 + const startOffset = Math.floor((data.length - maxIdx) * 0.6) // if there are 4 ticks, their positions are 0/3, 1/3, 2/3, 3/3 (as fractions of maxIdx) - const idxs = new Array(n).fill(0).map((_, i) => Math.floor((maxIdx * i) / (n - 1))) + const idxs = new Array(n) + .fill(0) + .map((_, i) => Math.floor((maxIdx * i) / (n - 1) + startOffset)) return idxs.map((i) => data[i].timestamp) } +/** + * Check if the start and end time are on the same day + * If they are we can omit the day/month in the date time format + */ +function isSameDay(d1: Date, d2: Date) { + return ( + d1.getFullYear() === d2.getFullYear() && + d1.getMonth() === d2.getMonth() && + d1.getDate() === d2.getDate() + ) +} + const shortDateTime = (ts: number) => format(new Date(ts), 'M/d HH:mm') +const shortTime = (ts: number) => format(new Date(ts), 'HH:mm') const longDateTime = (ts: number) => format(new Date(ts), 'MMM d, yyyy HH:mm:ss zz') -// TODO: change these to theme colors so they work in light mode -const LIGHT_GRAY = 'var(--stroke-default)' const GRID_GRAY = 'var(--stroke-secondary)' -const GREEN = 'var(--base-green-500)' +const GREEN_400 = 'var(--theme-accent-400)' +const GREEN_600 = 'var(--theme-accent-600)' +const GREEN_800 = 'var(--theme-accent-800)' // TODO: figure out how to do this with TW classes instead. As far as I can tell // ticks only take direct styling const textMonoMd = { - fontSize: '0.75rem', + fontSize: '0.6875rem', fontFamily: '"GT America Mono", monospace', + fill: 'var(--content-quaternary)', } function renderTooltip(props: TooltipProps) { @@ -50,10 +70,10 @@ function renderTooltip(props: TooltipProps) { } = payload[0] if (!timestamp || !value) return null return ( -
+
{longDateTime(timestamp)}
-
{name}
+
{name}
{value}
{/* TODO: unit on value if relevant */}
@@ -76,6 +96,8 @@ type Props = { height: number interpolation?: 'linear' | 'stepAfter' customXTicks?: boolean + startTime: Date + endTime: Date } // Limitations @@ -89,31 +111,83 @@ export function TimeSeriesAreaChart({ height, interpolation = 'linear', customXTicks, + startTime, + endTime, }: Props) { return ( - + + + + {/* TODO: stop tooltip being focused by default on pageload if nothing else has been clicked */} + + + + ) +} + +export function TimeSeriesLineChart({ + className, + data, + title, + width, + height, + interpolation = 'linear', + customXTicks, + startTime, + endTime, +}: Props) { + return ( + + + {/* TODO: stop tooltip being focused by default on pageload if nothing else has been clicked */} - + + ) } diff --git a/app/pages/project/instances/instance/tabs/MetricsTab.tsx b/app/pages/project/instances/instance/tabs/MetricsTab.tsx index feb3753858..60a15d92c9 100644 --- a/app/pages/project/instances/instance/tabs/MetricsTab.tsx +++ b/app/pages/project/instances/instance/tabs/MetricsTab.tsx @@ -5,12 +5,13 @@ import type { Cumulativeint64, Disk, DiskMetricName } from '@oxide/api' import { useApiQuery } from '@oxide/api' import { Listbox, Spinner } from '@oxide/ui' -import { TimeSeriesAreaChart } from 'app/components/TimeSeriesChart' +import { TimeSeriesLineChart } from 'app/components/TimeSeriesChart' import { useDateTimeRangePicker } from 'app/components/form' import { useRequiredParams } from 'app/hooks' type DiskMetricParams = { title: string + unit?: string startTime: Date endTime: Date metricName: DiskMetricName @@ -20,6 +21,7 @@ type DiskMetricParams = { function DiskMetric({ title, + unit, startTime, endTime, metricName, @@ -47,17 +49,20 @@ function DiskMetric({ // in the tooltip. could be just once on the end of the x-axis like GCP return ( -
-

- {title} {isLoading && } +
+

+ {title} {unit &&
{unit}
} + {isLoading && }

-
) @@ -96,18 +101,32 @@ function DiskMetrics({ disks }: { disks: Disk[] }) { {dateTimeRangePicker}

- {/* TODO: separate "Reads" from "(count)" so we can - a) style them differently in the title, and - b) show "Reads" but not "(count)" in the Tooltip? - */} -
+
{/* see the following link for the source of truth on what these mean https://github.com/oxidecomputer/crucible/blob/258f162b/upstairs/src/stats.rs#L9-L50 */} - - - - - +
+ + +
+ +
+ + +
+ +
+ +
)