Skip to content

Commit ef645fb

Browse files
[DI-29167] - Integration changes for graph data zoom in for CloudPulse metrics (#13317)
* [DI-29167] - Changes for integrating zoom in feature in cloudpulse widget and line graph * [DI-29167] - Changeset * [DI-29167] - Typecheck fix * [DI-29167] - Zoom key modification * [DI-29167] - Zoom key modification * [DI-29167] - fallback syntax change
1 parent accfa5f commit ef645fb

File tree

8 files changed

+234
-43
lines changed

8 files changed

+234
-43
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
Changes for providing ability to zoom in inside the `CloudPulse Metrics Graphs` ([#13317](https://github.com/linode/manager/pull/13317))

packages/manager/src/components/AreaChart/AreaChart.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Area,
99
CartesianGrid,
1010
Legend,
11+
ReferenceArea,
1112
ResponsiveContainer,
1213
Tooltip,
1314
XAxis,
@@ -26,6 +27,7 @@ import {
2627
} from './utils';
2728

2829
import type { TooltipProps } from 'recharts';
30+
import type { CategoricalChartFunc } from 'recharts/types/chart/generateCategoricalChart';
2931
import type { MetricsDisplayRow } from 'src/components/LineGraph/MetricsDisplay';
3032

3133
export interface DataSet {
@@ -47,6 +49,32 @@ export interface AreaProps {
4749
dataKey: string;
4850
}
4951

52+
interface ZoomCallbacks {
53+
/**
54+
* Callback fired on mouse down event on the chart
55+
*/
56+
onMouseDown?: CategoricalChartFunc;
57+
/**
58+
* Callback fired on mouse move event on the chart
59+
*/
60+
onMouseMove?: CategoricalChartFunc;
61+
/**
62+
* Callback fired on mouse up event on the chart
63+
*/
64+
onMouseUp?: CategoricalChartFunc;
65+
}
66+
67+
interface ReferenceAreaProps {
68+
/**
69+
* Ending x-axis value of the reference area
70+
*/
71+
referenceEnd: number;
72+
/**
73+
* Starting x-axis value of the reference area
74+
*/
75+
referenceStart: number;
76+
}
77+
5078
interface XAxisProps {
5179
/**
5280
* format for the x-axis timestamp
@@ -118,6 +146,11 @@ export interface AreaChartProps {
118146
*/
119147
margin?: { bottom: number; left: number; right: number; top: number };
120148

149+
/**
150+
* reference area to be highlighted on the chart
151+
*/
152+
referenceArea?: null | ReferenceAreaProps;
153+
121154
/**
122155
* control the visibility of dots for each data points
123156
*/
@@ -171,6 +204,11 @@ export interface AreaChartProps {
171204
* y-axis properties
172205
*/
173206
yAxisProps?: YAxisProps;
207+
208+
/**
209+
* zoom callbacks (onMouseDown, onMouseMove, onMouseUp)
210+
*/
211+
zoomCallbacks?: ZoomCallbacks;
174212
}
175213

176214
export const AreaChart = (props: AreaChartProps) => {
@@ -195,9 +233,13 @@ export const AreaChart = (props: AreaChartProps) => {
195233
xAxisTickCount,
196234
yAxisProps,
197235
tooltipCustomValueFormatter,
236+
zoomCallbacks,
237+
referenceArea,
198238
} = props;
199239

200240
const theme = useTheme();
241+
const { onMouseDown, onMouseMove, onMouseUp } = zoomCallbacks ?? {};
242+
const { referenceStart, referenceEnd } = referenceArea ?? {};
201243

202244
const [activeSeries, setActiveSeries] = React.useState<Array<string>>([]);
203245
const handleLegendClick = (dataKey: string) => {
@@ -280,7 +322,14 @@ export const AreaChart = (props: AreaChartProps) => {
280322
height={height}
281323
width={width}
282324
>
283-
<_AreaChart aria-label={ariaLabel} data={data} margin={margin}>
325+
<_AreaChart
326+
aria-label={ariaLabel}
327+
data={data}
328+
margin={margin}
329+
onMouseDown={onMouseDown}
330+
onMouseMove={onMouseMove}
331+
onMouseUp={onMouseUp}
332+
>
284333
<CartesianGrid
285334
stroke={theme.color.grey7}
286335
strokeDasharray="3 3"
@@ -341,6 +390,13 @@ export const AreaChart = (props: AreaChartProps) => {
341390
wrapperStyle={legendStyles}
342391
/>
343392
)}
393+
{referenceStart !== undefined && referenceEnd !== undefined && (
394+
<ReferenceArea
395+
strokeOpacity={0.3}
396+
x1={referenceStart}
397+
x2={referenceEnd}
398+
/>
399+
)}
344400
{areas.map(({ color, dataKey }) => (
345401
<Area
346402
connectNulls={connectNulls}

packages/manager/src/featureFlags.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ interface AclpFlag {
9999
*/
100100
enabled: boolean;
101101

102+
/**
103+
* This property indicates whether to enable zoom in charts or not
104+
*/
105+
enableZoomInCharts?: boolean;
106+
102107
/**
103108
* This property indicates for which unit, we need to humanize the values e.g., count, iops etc.,
104109
*/

packages/manager/src/features/CloudPulse/Utils/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,5 +694,5 @@ export const humanizeLargeData = (value: number) => {
694694
if (value >= 1000) {
695695
return +(value / 1000).toFixed(1) + 'K';
696696
}
697-
return `${roundTo(value, 1)}`;
697+
return `${roundTo(value, 2)}`;
698698
};

packages/manager/src/features/CloudPulse/Widget/CloudPulseWidget.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
172172
const [groupBy, setGroupBy] = React.useState<string[] | undefined>(
173173
props.widget.group_by
174174
);
175+
const [isZoomed, setIsZoomed] = React.useState(false);
175176
const theme = useTheme();
176177

177178
const {
@@ -401,6 +402,10 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
401402
},
402403
[savePref, updatePreferences, widget.label]
403404
);
405+
406+
const handleZoomStateChange = React.useCallback((zoomed: boolean) => {
407+
setIsZoomed(zoomed);
408+
}, []);
404409
const {
405410
data: metricsList,
406411
error,
@@ -428,6 +433,7 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
428433
label: widget.label,
429434
timeStamp,
430435
url: flags.aclpReadEndpoint!,
436+
shouldRefresh: !isZoomed,
431437
}
432438
);
433439
let data: DataSet[] = [];
@@ -462,6 +468,19 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
462468
const hours = end.diff(start, 'hours').hours;
463469
const tickFormat = hours <= 24 ? 'hh:mm a' : 'LLL dd';
464470

471+
const zoomResetKey = React.useMemo(() => {
472+
const { preset, start, end, timeZone } = props.duration;
473+
474+
if (preset) {
475+
return `preset:${preset}`;
476+
}
477+
if (!start || !end || !timeZone) {
478+
return 'custom:missing-params';
479+
}
480+
481+
return `custom:${start},${end},${timeZone}`;
482+
}, [props.duration]);
483+
465484
React.useEffect(() => {
466485
if (
467486
filteredSelections.length !== (dimensionFilters?.length ?? 0) &&
@@ -585,12 +604,16 @@ export const CloudPulseWidget = (props: CloudPulseWidgetProperties) => {
585604
metricsApiCallError === jweTokenExpiryError ||
586605
isJweTokenFetching
587606
} // keep loading until we are trying to fetch the refresh token
607+
onZoomChange={handleZoomStateChange}
588608
showDot
589609
showLegend={data.length !== 0}
590610
timezone={timezone}
591611
unit={`${currentUnit}${unit.endsWith('ps') ? '/s' : ''}`}
592612
variant={variant}
593613
xAxis={{ tickFormat, tickGap: 60 }}
614+
zoomResetKey={
615+
zoomResetKey // key to reset zoom when duration changes
616+
}
594617
/>
595618
</Paper>
596619
</Stack>

packages/manager/src/features/CloudPulse/Widget/components/CloudPulseLineGraph.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,14 @@ class ResizeObserver {
3333
unobserve() {}
3434
}
3535

36+
const zoomResetKey = 'test-zoom';
37+
3638
describe('CloudPulseLineGraph', () => {
3739
window.ResizeObserver = ResizeObserver;
3840

3941
it('should render AreaChart when data is provided', () => {
4042
const { container, getByRole } = renderWithTheme(
41-
<CloudPulseLineGraph {...mockData} />
43+
<CloudPulseLineGraph {...mockData} zoomResetKey={zoomResetKey} />
4244
);
4345
const table = getByRole('table');
4446

@@ -53,7 +55,11 @@ describe('CloudPulseLineGraph', () => {
5355

5456
it('should show error state', () => {
5557
const { getByText } = renderWithTheme(
56-
<CloudPulseLineGraph {...mockData} error="Test error" />
58+
<CloudPulseLineGraph
59+
{...mockData}
60+
error="Test error"
61+
zoomResetKey={zoomResetKey}
62+
/>
5763
);
5864

5965
expect(getByText('Test error')).toBeInTheDocument();
@@ -66,7 +72,7 @@ describe('CloudPulseLineGraph', () => {
6672
};
6773

6874
const { getByText } = renderWithTheme(
69-
<CloudPulseLineGraph {...emptyData} />
75+
<CloudPulseLineGraph {...emptyData} zoomResetKey={zoomResetKey} />
7076
);
7177

7278
expect(getByText('No data to display')).toBeInTheDocument();

0 commit comments

Comments
 (0)