Skip to content

Commit 549c266

Browse files
Convert old charts to new style (#2762)
* remove width and height from TimeSeriesChart * use new chart style on system and silo utilization * fix lint error * Tighten spacing and more consistent with other layouts (#2764) * Tighten spacing and more consistent with other layouts * Proper chart spacing on silo page * very important and helpful comment --------- Co-authored-by: Benjamin Leonard <benji@oxide.computer>
1 parent d41ef17 commit 549c266

File tree

7 files changed

+127
-171
lines changed

7 files changed

+127
-171
lines changed

app/components/RefetchIntervalPicker.tsx

Lines changed: 26 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
* Copyright Oxide Computer Company
77
*/
88
import cn from 'classnames'
9-
import { useEffect, useState } from 'react'
9+
import { useState } from 'react'
1010

11-
import { Refresh16Icon, Time16Icon } from '@oxide/design-system/icons/react'
11+
import { Refresh16Icon } from '@oxide/design-system/icons/react'
1212

1313
import { Listbox, type ListboxItem } from '~/ui/lib/Listbox'
1414
import { SpinnerLoader } from '~/ui/lib/Spinner'
1515
import { useInterval } from '~/ui/lib/use-interval'
16-
import { toLocaleTimeString } from '~/util/date'
1716

1817
const intervalPresets = {
1918
Off: undefined,
@@ -37,63 +36,41 @@ type Props = {
3736
enabled: boolean
3837
isLoading: boolean
3938
fn: () => void
40-
showLastFetched?: boolean
4139
className?: string
42-
isSlim?: boolean
4340
}
4441

45-
export function useIntervalPicker({
46-
enabled,
47-
isLoading,
48-
fn,
49-
showLastFetched = false,
50-
className,
51-
isSlim = false,
52-
}: Props) {
42+
export function useIntervalPicker({ enabled, isLoading, fn, className }: Props) {
5343
const [intervalPreset, setIntervalPreset] = useState<IntervalPreset>('10s')
5444

55-
const [lastFetched, setLastFetched] = useState(new Date())
56-
useEffect(() => {
57-
if (isLoading) setLastFetched(new Date())
58-
}, [isLoading])
59-
6045
const delay = enabled ? intervalPresets[intervalPreset] : null
6146
useInterval({ fn, delay })
6247

6348
return {
6449
intervalMs: (enabled && intervalPresets[intervalPreset]) || undefined,
6550
intervalPicker: (
66-
<div className={cn('flex items-center justify-between', className)}>
67-
{showLastFetched && (
68-
<div className="hidden items-center gap-2 text-right text-mono-sm text-tertiary lg+:flex">
69-
<Time16Icon className="text-quaternary" /> Refreshed{' '}
70-
{toLocaleTimeString(lastFetched)}
71-
</div>
72-
)}
73-
<div className="flex">
74-
<button
75-
type="button"
76-
className={cn(
77-
'flex w-10 items-center justify-center rounded-l border-b border-l border-t border-default disabled:cursor-default',
78-
isLoading && 'hover:bg-hover',
79-
!enabled && 'cursor-not-allowed bg-disabled'
80-
)}
81-
onClick={fn}
82-
disabled={isLoading || !enabled}
83-
>
84-
<SpinnerLoader isLoading={isLoading}>
85-
<Refresh16Icon className="text-secondary" />
86-
</SpinnerLoader>
87-
</button>
88-
<Listbox
89-
selected={enabled ? intervalPreset : 'Off'}
90-
className={cn('[&_button]:!rounded-l-none', isSlim ? '' : 'w-24')}
91-
items={intervalItems}
92-
onChange={setIntervalPreset}
93-
disabled={!enabled}
94-
hideSelected={isSlim}
95-
/>
96-
</div>
51+
<div className={cn('flex', className)}>
52+
<button
53+
type="button"
54+
className={cn(
55+
'flex w-10 items-center justify-center rounded-l border-b border-l border-t border-default disabled:cursor-default',
56+
isLoading && 'hover:bg-hover',
57+
!enabled && 'cursor-not-allowed bg-disabled'
58+
)}
59+
onClick={fn}
60+
disabled={isLoading || !enabled}
61+
>
62+
<SpinnerLoader isLoading={isLoading}>
63+
<Refresh16Icon className="text-secondary" />
64+
</SpinnerLoader>
65+
</button>
66+
<Listbox
67+
selected={enabled ? intervalPreset : 'Off'}
68+
className="[&_button]:!rounded-l-none"
69+
items={intervalItems}
70+
onChange={setIntervalPreset}
71+
disabled={!enabled}
72+
hideSelected
73+
/>
9774
</div>
9875
),
9976
}

app/components/SystemMetric.tsx

Lines changed: 24 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,15 @@ import {
1414
type SystemMetricName,
1515
} from '@oxide/api'
1616

17-
import { Spinner } from '~/ui/lib/Spinner'
18-
19-
import { TimeSeriesChart } from './TimeSeriesChart'
17+
import { ChartContainer, ChartHeader, TimeSeriesChart } from './TimeSeriesChart'
2018

2119
// The difference between system metric and silo metric is
2220
// 1. different endpoints
2321
// 2. silo metric doesn't have capacity
2422

2523
type MetricProps = {
2624
title: string
27-
unit?: string
25+
unit: string
2826
startTime: Date
2927
endTime: Date
3028
metricName: SystemMetricName
@@ -93,25 +91,17 @@ export function SiloMetric({
9391
// in the tooltip. could be just once on the end of the x-axis like GCP
9492

9593
return (
96-
<div>
97-
<h2 className="flex items-center gap-1.5 px-3 text-mono-sm text-default">
98-
{title} {unit && <span className="text-tertiary">({unit})</span>}{' '}
99-
{(inRange.isPending || beforeStart.isPending) && <Spinner />}
100-
</h2>
101-
{/* TODO: proper skeleton for empty chart */}
102-
<div className="mt-3 h-[300px]">
103-
<TimeSeriesChart
104-
data={data}
105-
title={title}
106-
width={480}
107-
height={240}
108-
interpolation="stepAfter"
109-
startTime={startTime}
110-
endTime={endTime}
111-
unit={unit !== 'count' ? unit : undefined}
112-
/>
113-
</div>
114-
</div>
94+
<ChartContainer>
95+
<ChartHeader title={title} label={`(${unit})`} />
96+
<TimeSeriesChart
97+
data={data}
98+
title={title}
99+
interpolation="stepAfter"
100+
startTime={startTime}
101+
endTime={endTime}
102+
unit={unit !== 'Count' ? unit : undefined}
103+
/>
104+
</ChartContainer>
115105
)
116106
}
117107

@@ -169,24 +159,16 @@ export function SystemMetric({
169159
// in the tooltip. could be just once on the end of the x-axis like GCP
170160

171161
return (
172-
<div>
173-
<h2 className="flex items-center gap-1.5 px-3 text-mono-sm text-default">
174-
{title} {unit && <span className="text-tertiary">({unit})</span>}{' '}
175-
{(inRange.isPending || beforeStart.isPending) && <Spinner />}
176-
</h2>
177-
{/* TODO: proper skeleton for empty chart */}
178-
<div className="mt-3 h-[300px]">
179-
<TimeSeriesChart
180-
data={data}
181-
title={title}
182-
width={480}
183-
height={240}
184-
interpolation="stepAfter"
185-
startTime={startTime}
186-
endTime={endTime}
187-
unit={unit !== 'count' ? unit : undefined}
188-
/>
189-
</div>
190-
</div>
162+
<ChartContainer>
163+
<ChartHeader title={title} label={`(${unit})`} />
164+
<TimeSeriesChart
165+
data={data}
166+
title={title}
167+
interpolation="stepAfter"
168+
startTime={startTime}
169+
endTime={endTime}
170+
unit={unit !== 'Count' ? unit : undefined}
171+
/>
172+
</ChartContainer>
191173
)
192174
}

app/components/TimeSeriesChart.tsx

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import type { TooltipProps } from 'recharts/types/component/Tooltip'
2222
import type { ChartDatum } from '@oxide/api'
2323
import { Error12Icon } from '@oxide/design-system/icons/react'
2424

25+
import { classed } from '~/util/classed'
26+
2527
// Recharts's built-in ticks behavior is useless and probably broken
2628
/**
2729
* Split the data into n evenly spaced ticks, with one at the left end and one a
@@ -110,14 +112,11 @@ type TimeSeriesChartProps = {
110112
className?: string
111113
data: ChartDatum[] | undefined
112114
title: string
113-
width: number
114-
height: number
115115
interpolation?: 'linear' | 'stepAfter'
116116
startTime: Date
117117
endTime: Date
118118
unit?: string
119119
yAxisTickFormatter?: (val: number) => string
120-
hasBorder?: boolean
121120
hasError?: boolean
122121
}
123122

@@ -166,17 +165,13 @@ const SkeletonMetric = ({
166165
)
167166

168167
export function TimeSeriesChart({
169-
className,
170168
data: rawData,
171169
title,
172-
width,
173-
height,
174170
interpolation = 'linear',
175171
startTime,
176172
endTime,
177173
unit,
178174
yAxisTickFormatter = (val) => val.toLocaleString(),
179-
hasBorder = true,
180175
hasError = false,
181176
}: TimeSeriesChartProps) {
182177
// We use the largest data point +20% for the graph scale. !rawData doesn't
@@ -210,34 +205,28 @@ export function TimeSeriesChart({
210205
// re-render on every render of the parent when the data is undefined
211206
const data = useMemo(() => rawData || [], [rawData])
212207

213-
const wrapperClass = cn(className, hasBorder && 'rounded-lg border border-default')
214-
215208
if (hasError) {
216209
return (
217-
<SkeletonMetric className={wrapperClass}>
210+
<SkeletonMetric>
218211
<MetricsError />
219212
</SkeletonMetric>
220213
)
221214
}
222215

223216
if (!data || data.length === 0) {
224217
return (
225-
<SkeletonMetric shimmer className={wrapperClass}>
218+
<SkeletonMetric shimmer>
226219
<MetricsLoadingIndicator />
227220
</SkeletonMetric>
228221
)
229222
}
230223

224+
// ResponsiveContainer has default height and width of 100%
225+
// https://recharts.org/en-US/api/ResponsiveContainer
231226
return (
232-
<div className="h-[300px] w-full">
233-
{/* temporary until we migrate the old metrics to the new style */}
234-
<ResponsiveContainer className={wrapperClass}>
235-
<AreaChart
236-
width={width}
237-
height={height}
238-
data={data}
239-
margin={{ top: 0, right: hasBorder ? 16 : 0, bottom: 0, left: 0 }}
240-
>
227+
<div className="px-5 pb-5 pt-8">
228+
<ResponsiveContainer height={300}>
229+
<AreaChart data={data} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
241230
<CartesianGrid stroke={GRID_GRAY} vertical={false} />
242231
<XAxis
243232
axisLine={{ stroke: GRID_GRAY }}
@@ -277,7 +266,7 @@ export function TimeSeriesChart({
277266
/>
278267
<Area
279268
dataKey="value"
280-
name={title}
269+
name={title} // Provides name for value in hover tooltip
281270
type={interpolation}
282271
stroke={GREEN_600}
283272
fill={GREEN_400}
@@ -320,3 +309,27 @@ const MetricsError = () => (
320309
/>
321310
</>
322311
)
312+
313+
export const ChartContainer = classed.div`flex w-full grow flex-col rounded-lg border border-default`
314+
315+
type ChartHeaderProps = {
316+
title: string
317+
label: string
318+
description?: string
319+
children?: ReactNode
320+
}
321+
322+
export function ChartHeader({ title, label, description, children }: ChartHeaderProps) {
323+
return (
324+
<div className="flex items-center justify-between border-b px-5 pb-4 pt-5 border-secondary">
325+
<div>
326+
<h2 className="flex items-baseline gap-1.5">
327+
<div className="text-sans-semi-lg">{title}</div>
328+
<div className="text-sans-md text-secondary">{label}</div>
329+
</h2>
330+
<div className="mt-0.5 text-sans-md text-secondary">{description}</div>
331+
</div>
332+
{children}
333+
</div>
334+
)
335+
}

app/components/oxql-metrics/OxqlMetric.tsx

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import * as Dropdown from '~/ui/lib/DropdownMenu'
2525
import { classed } from '~/util/classed'
2626
import { links } from '~/util/links'
2727

28-
import { TimeSeriesChart } from '../TimeSeriesChart'
28+
import { ChartContainer, ChartHeader, TimeSeriesChart } from '../TimeSeriesChart'
2929
import { HighlightedOxqlQuery, toOxqlStr } from './HighlightedOxqlQuery'
3030
import {
3131
composeOxqlData,
@@ -77,15 +77,8 @@ export function OxqlMetric({ title, description, unit, ...queryObj }: OxqlMetric
7777
const [modalOpen, setModalOpen] = useState(false)
7878

7979
return (
80-
<div className="flex w-full grow flex-col rounded-lg border border-default">
81-
<div className="flex items-center justify-between border-b px-5 pb-4 pt-5 border-secondary">
82-
<div>
83-
<h2 className="flex items-baseline gap-1.5">
84-
<div className="text-sans-semi-lg">{title}</div>
85-
<div className="text-sans-md text-secondary">{label}</div>
86-
</h2>
87-
<div className="mt-0.5 text-sans-md text-secondary">{description}</div>
88-
</div>
80+
<ChartContainer>
81+
<ChartHeader title={title} label={label} description={description}>
8982
<MoreActionsMenu label="Instance actions" isSmall>
9083
<Dropdown.LinkItem to={links.oxqlSchemaDocs(queryObj.metricName)}>
9184
About this metric
@@ -102,22 +95,17 @@ export function OxqlMetric({ title, description, unit, ...queryObj }: OxqlMetric
10295
>
10396
<HighlightedOxqlQuery {...queryObj} />
10497
</CopyCodeModal>
105-
</div>
106-
<div className="px-5 pb-5 pt-8">
107-
<TimeSeriesChart
108-
title={title}
109-
startTime={startTime}
110-
endTime={endTime}
111-
unit={unitForSet}
112-
data={data}
113-
yAxisTickFormatter={yAxisTickFormatter}
114-
width={480}
115-
height={240}
116-
hasBorder={false}
117-
hasError={!!error}
118-
/>
119-
</div>
120-
</div>
98+
</ChartHeader>
99+
<TimeSeriesChart
100+
title={title}
101+
startTime={startTime}
102+
endTime={endTime}
103+
unit={unitForSet}
104+
data={data}
105+
yAxisTickFormatter={yAxisTickFormatter}
106+
hasError={!!error}
107+
/>
108+
</ChartContainer>
121109
)
122110
}
123111

0 commit comments

Comments
 (0)