@centreon/ui 25.10.20 → 25.10.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/Form/Inputs/Grid.tsx +3 -2
  3. package/src/Form/Section/navigateToSection.ts +6 -6
  4. package/src/Graph/BarChart/BarChart.cypress.spec.tsx +19 -0
  5. package/src/Graph/BarChart/BarChart.stories.tsx +51 -1
  6. package/src/Graph/BarChart/BarChart.tsx +1 -1
  7. package/src/Graph/BarChart/BarGroup.tsx +22 -32
  8. package/src/Graph/BarChart/MemoizedGroup.tsx +8 -11
  9. package/src/Graph/BarChart/ResponsiveBarChart.tsx +2 -1
  10. package/src/Graph/Chart/BasicComponents/Lines/StackedLines/useStackedLines.ts +18 -45
  11. package/src/Graph/Chart/BasicComponents/Lines/index.tsx +32 -26
  12. package/src/Graph/Chart/Chart.cypress.spec.tsx +24 -5
  13. package/src/Graph/Chart/Chart.stories.tsx +43 -1
  14. package/src/Graph/Chart/Chart.tsx +3 -2
  15. package/src/Graph/Chart/Legend/index.tsx +26 -2
  16. package/src/Graph/Chart/models.ts +6 -1
  17. package/src/Graph/Chart/useChartData.ts +1 -1
  18. package/src/Graph/common/BaseChart/BaseChart.tsx +6 -1
  19. package/src/Graph/common/timeSeries/index.test.ts +20 -0
  20. package/src/Graph/common/timeSeries/index.ts +121 -11
  21. package/src/Graph/common/timeSeries/models.ts +4 -2
  22. package/src/Graph/common/utils.ts +10 -4
  23. package/src/Graph/mockedData/dataWithMissingPoint.json +74 -0
  24. package/src/Graph/mockedData/pingServiceWithStackedKeys.json +205 -0
  25. package/src/InputField/Select/index.tsx +1 -2
  26. package/src/Module/index.tsx +8 -2
  27. package/src/ThemeProvider/index.tsx +30 -21
  28. package/src/ThemeProvider/tailwindcss.css +10 -10
  29. package/src/components/Layout/PageLayout/PageLayout.tsx +9 -3
  30. package/src/components/Layout/PageLayout/PageLayoutActions.tsx +5 -3
  31. package/src/components/Layout/PageLayout/PageLayoutBody.tsx +5 -3
  32. package/src/components/Layout/PageLayout/PageLayoutHeader.tsx +5 -3
  33. package/src/components/Modal/Modal.styles.ts +1 -2
  34. package/src/components/Modal/ModalHeader.tsx +5 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "25.10.20",
3
+ "version": "25.10.22",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -21,8 +21,9 @@ const Grid = ({
21
21
  <div
22
22
  className={`${className} grid gap-3`}
23
23
  style={{
24
- gridTemplateColumns:
25
- className ? grid?.gridTemplateColumns || undefined : grid?.gridTemplateColumns ||
24
+ gridTemplateColumns: className
25
+ ? grid?.gridTemplateColumns || undefined
26
+ : grid?.gridTemplateColumns ||
26
27
  `repeat(${grid?.columns.length || 1}, 1fr)`,
27
28
  alignItems: grid?.alignItems || 'flex-start'
28
29
  }}
@@ -1,9 +1,9 @@
1
1
  export const useNavigateToSection = () => {
2
- return (sectionName: string) => {
3
- const section = document.querySelector(
4
- `[data-section-group-form-id="${sectionName}"]`
5
- );
2
+ return (sectionName: string) => {
3
+ const section = document.querySelector(
4
+ `[data-section-group-form-id="${sectionName}"]`
5
+ );
6
6
 
7
- section?.scrollIntoView({ behavior: 'smooth' });
8
- };
7
+ section?.scrollIntoView({ behavior: 'smooth' });
8
+ };
9
9
  };
@@ -4,10 +4,12 @@ import { useAtomValue } from 'jotai';
4
4
 
5
5
  import { userAtom } from '@centreon/ui-context';
6
6
 
7
+ import dataMissingPoint from '../mockedData/dataWithMissingPoint.json';
7
8
  import dataLastWeek from '../mockedData/lastWeek.json';
8
9
  import dataPingService from '../mockedData/pingService.json';
9
10
  import dataPingServiceMixedStacked from '../mockedData/pingServiceMixedStacked.json';
10
11
  import dataPingServiceStacked from '../mockedData/pingServiceStacked.json';
12
+ import dataPingServiceLinesStackKeys from '../mockedData/pingServiceWithStackedKeys.json';
11
13
 
12
14
  import BarChart, { BarChartProps } from './BarChart';
13
15
 
@@ -312,4 +314,21 @@ describe('Bar chart', () => {
312
314
  cy.contains('1 s').should('be.visible');
313
315
  cy.contains('1%').should('be.visible');
314
316
  });
317
+
318
+ it('displays the stacked bar chart correctly when a point is missing compare to the time serie', () => {
319
+ initialize({ data: dataMissingPoint });
320
+
321
+ cy.findByTestId('stacked-bar-2-0-139').should('be.visible');
322
+
323
+ cy.makeSnapshot();
324
+ });
325
+
326
+ it('displays the stacked bar chart with bars stacked together', () => {
327
+ initialize({ data: dataPingServiceLinesStackKeys });
328
+
329
+ cy.findByTestId('stacked-bar-3-0-0.05336').should('be.visible');
330
+ cy.findByTestId('stacked-bar-4-0-0.06684').should('be.visible');
331
+
332
+ cy.makeSnapshot();
333
+ });
315
334
  });
@@ -5,7 +5,10 @@ import { LineChartData } from '../common/models';
5
5
  import dataPingService from '../mockedData/pingService.json';
6
6
  import dataPingServiceMixedStacked from '../mockedData/pingServiceMixedStacked.json';
7
7
  import dataPingServiceStacked from '../mockedData/pingServiceStacked.json';
8
+ import dataPingServiceStackeKey from '../mockedData/pingServiceWithStackedKeys.json';
8
9
 
10
+ import { ClickAwayListener } from '@mui/material';
11
+ import { useState } from 'react';
9
12
  import BarChart from './BarChart';
10
13
 
11
14
  const meta: Meta<typeof BarChart> = {
@@ -259,4 +262,51 @@ export const mixedStackedMinMax: Story = {
259
262
  max: 20
260
263
  },
261
264
  render: Template
262
- };
265
+ };
266
+
267
+ const LegendSecondaryClick = (args) => {
268
+ const [position, setPosition] = useState<Array<[number, number]> | null>(
269
+ null
270
+ );
271
+
272
+ return (
273
+ <>
274
+ <Template
275
+ {...args}
276
+ legend={{
277
+ secondaryClick: ({ position }) => setPosition(position)
278
+ }}
279
+ />
280
+ {position && (
281
+ <ClickAwayListener onClickAway={() => setPosition(null)}>
282
+ <div
283
+ className="absolute py-1 px-2 rounded-sm bg-background-widget shadow-md"
284
+ style={{ left: position?.[0], top: position?.[1] }}
285
+ open={Boolean(position)}
286
+ onClose={() => setPosition(null)}
287
+ >
288
+ menu
289
+ </div>
290
+ </ClickAwayListener>
291
+ )}
292
+ </>
293
+ );
294
+ };
295
+
296
+ export const withLegendSecondaryClick: Story = {
297
+ args: defaultArgs,
298
+ render: (args) => (
299
+ <LegendSecondaryClick
300
+ {...args}
301
+ data={dataPingService as unknown as LineChartData}
302
+ />
303
+ )
304
+ };
305
+
306
+ export const stackKey: Story = {
307
+ args: {
308
+ ...defaultArgs,
309
+ data: dataPingServiceStackeKey
310
+ },
311
+ render: Template
312
+ };
@@ -116,4 +116,4 @@ const BarChart = ({
116
116
  );
117
117
  };
118
118
 
119
- export default BarChart;
119
+ export default BarChart;
@@ -1,12 +1,13 @@
1
1
  import { scaleBand, scaleOrdinal } from '@visx/scale';
2
2
  import { BarGroupHorizontal, BarGroup as VisxBarGroup } from '@visx/shape';
3
3
  import { ScaleLinear } from 'd3-scale';
4
- import { difference, equals, keys, omit, pick, pluck, uniq } from 'ramda';
4
+ import { difference, equals, keys, omit, pick } from 'ramda';
5
5
  import { memo, useMemo } from 'react';
6
6
 
7
7
  import { useDeepMemo } from '../../utils';
8
8
  import {
9
9
  getSortedStackedLines,
10
+ getStackedLinesTimeSeriesPerStackAndUnit,
10
11
  getTime,
11
12
  getTimeSeriesForLines,
12
13
  getUnits
@@ -54,41 +55,18 @@ const BarGroup = ({
54
55
  );
55
56
 
56
57
  const stackedLines = getSortedStackedLines(lines);
57
- const stackedUnits = uniq(pluck('unit', stackedLines));
58
58
  const notStackedLines = difference(lines, stackedLines);
59
-
60
- const stackedKeys = stackedUnits.reduce(
61
- (acc, unit) => ({
62
- ...acc,
63
- [`stacked-${unit}`]: null
64
- }),
65
- {}
66
- );
67
- const stackedLinesTimeSeriesPerUnit = stackedUnits.reduce(
68
- (acc, stackedUnit) => {
69
- const relatedLines = stackedLines.filter(({ unit }) =>
70
- equals(unit, stackedUnit)
71
- );
72
-
73
- return {
74
- ...acc,
75
- [stackedUnit]: {
76
- lines: relatedLines,
77
- timeSeries: getTimeSeriesForLines({
78
- lines: relatedLines,
79
- timeSeries
80
- })
81
- }
82
- };
83
- },
84
- {}
85
- );
86
-
87
59
  const notStackedTimeSeries = getTimeSeriesForLines({
88
60
  lines: notStackedLines,
89
61
  timeSeries
90
62
  });
91
63
 
64
+ const { stackedLinesTimeSeriesPerStackKeyAndUnit, stackedKeys } = useMemo(
65
+ () =>
66
+ getStackedLinesTimeSeriesPerStackAndUnit({ stackedLines, timeSeries }),
67
+ [stackedLines, timeSeries]
68
+ );
69
+
92
70
  const normalizedTimeSeries = notStackedTimeSeries.map((timeSerie) => ({
93
71
  ...timeSerie,
94
72
  ...stackedKeys
@@ -98,6 +76,16 @@ const BarGroup = ({
98
76
  deps: [normalizedTimeSeries],
99
77
  variable: keys(omit(['timeTick'], normalizedTimeSeries[0]))
100
78
  });
79
+ const sortedLineKeys = lineKeys.sort((lineKeyA: string, lineKeyB: string) => {
80
+ if (lineKeyA.startsWith('stacked-') && !lineKeyB.startsWith('stacked-')) {
81
+ return true;
82
+ }
83
+
84
+ const lineKeysA = lineKeyA.split('-');
85
+ const lineKeysB = lineKeyB.split('-');
86
+
87
+ return lineKeysA[2] === '' && lineKeysB[2] !== '';
88
+ });
101
89
  const colors = useDeepMemo({
102
90
  deps: [lineKeys, lines],
103
91
  variable: lineKeys.map((key) => {
@@ -154,7 +142,7 @@ const BarGroup = ({
154
142
  color={colorScale}
155
143
  data={normalizedTimeSeries}
156
144
  height={size}
157
- keys={lineKeys}
145
+ keys={sortedLineKeys}
158
146
  {...barComponentBaseProps}
159
147
  >
160
148
  {(barGroups) =>
@@ -164,7 +152,9 @@ const BarGroup = ({
164
152
  key={`bar-group-${barGroup.index}-${barGroup.x0}`}
165
153
  barGroup={barGroup}
166
154
  barStyle={barStyle}
167
- stackedLinesTimeSeriesPerUnit={stackedLinesTimeSeriesPerUnit}
155
+ stackedLinesTimeSeriesPerStackKeyAndUnit={
156
+ stackedLinesTimeSeriesPerStackKeyAndUnit
157
+ }
168
158
  notStackedTimeSeries={notStackedTimeSeries}
169
159
  notStackedLines={notStackedLines}
170
160
  isTooltipHidden={isTooltipHidden}
@@ -12,7 +12,7 @@ interface Props {
12
12
  isTooltipHidden: boolean;
13
13
  barStyle: BarStyle;
14
14
  yScalesPerUnit: Record<string, ScaleLinear<number, number>>;
15
- stackedLinesTimeSeriesPerUnit: Record<
15
+ stackedLinesTimeSeriesPerStackKeyAndUnit: Record<
16
16
  string,
17
17
  { lines: Array<Line>; timeSeries: Array<TimeValue> }
18
18
  >;
@@ -25,7 +25,7 @@ interface Props {
25
25
 
26
26
  const MemoizedGroup = ({
27
27
  barGroup,
28
- stackedLinesTimeSeriesPerUnit,
28
+ stackedLinesTimeSeriesPerStackKeyAndUnit,
29
29
  notStackedLines,
30
30
  notStackedTimeSeries,
31
31
  isHorizontal,
@@ -38,9 +38,7 @@ const MemoizedGroup = ({
38
38
  const hasEmptyValues = barGroup.bars.every(({ key, value }) => {
39
39
  if (key.startsWith('stacked-')) {
40
40
  const timeValueBar =
41
- stackedLinesTimeSeriesPerUnit[key.replace('stacked-', '')].timeSeries[
42
- barIndex
43
- ];
41
+ stackedLinesTimeSeriesPerStackKeyAndUnit[key].timeSeries[barIndex];
44
42
 
45
43
  return Object.values(omit(['timeTick'], timeValueBar)).every(
46
44
  (value) => !value
@@ -59,13 +57,12 @@ const MemoizedGroup = ({
59
57
  {barGroup.bars.map((bar) => {
60
58
  const isStackedBar = bar.key.startsWith('stacked-');
61
59
  const linesBar = isStackedBar
62
- ? stackedLinesTimeSeriesPerUnit[bar.key.replace('stacked-', '')].lines
60
+ ? stackedLinesTimeSeriesPerStackKeyAndUnit[bar.key].lines
63
61
  : (notStackedLines.find(({ metric_id }) =>
64
62
  equals(metric_id, Number(bar.key))
65
63
  ) as Line);
66
64
  const timeSeriesBar = isStackedBar
67
- ? stackedLinesTimeSeriesPerUnit[bar.key.replace('stacked-', '')]
68
- .timeSeries
65
+ ? stackedLinesTimeSeriesPerStackKeyAndUnit[bar.key].timeSeries
69
66
  : notStackedTimeSeries.map((timeSerie) => ({
70
67
  timeTick: timeSerie.timeTick,
71
68
  [bar.key]: timeSerie[Number(bar.key)]
@@ -82,7 +79,7 @@ const MemoizedGroup = ({
82
79
  isTooltipHidden={isTooltipHidden}
83
80
  lines={linesBar as Array<Line>}
84
81
  timeSeries={timeSeriesBar}
85
- yScale={yScalesPerUnit[bar.key.replace('stacked-', '')]}
82
+ yScale={yScalesPerUnit[bar.key.split('-')[1] || undefined]}
86
83
  neutralValue={neutralValue}
87
84
  />
88
85
  ) : (
@@ -110,8 +107,8 @@ export default memo(
110
107
  (prevProps, nextProps) =>
111
108
  equals(prevProps.barGroup, nextProps.barGroup) &&
112
109
  equals(
113
- prevProps.stackedLinesTimeSeriesPerUnit,
114
- nextProps.stackedLinesTimeSeriesPerUnit
110
+ prevProps.stackedLinesTimeSeriesPerStackKeyAndUnit,
111
+ nextProps.stackedLinesTimeSeriesPerStackKeyAndUnit
115
112
  ) &&
116
113
  equals(prevProps.notStackedLines, nextProps.notStackedLines) &&
117
114
  equals(prevProps.notStackedTimeSeries, nextProps.notStackedTimeSeries) &&
@@ -196,7 +196,8 @@ const ResponsiveBarChart = ({
196
196
  displayLegend,
197
197
  mode: legend?.mode,
198
198
  placement: legend?.placement,
199
- renderExtraComponent: legend?.renderExtraComponent
199
+ renderExtraComponent: legend?.renderExtraComponent,
200
+ secondaryClick: legend?.secondaryClick
200
201
  }}
201
202
  legendRef={legendRef}
202
203
  limitLegend={limitLegend}
@@ -1,9 +1,7 @@
1
- import { equals, pluck, uniq } from 'ramda';
2
-
3
1
  import {
4
2
  getInvertedStackedLines,
5
3
  getNotInvertedStackedLines,
6
- getTimeSeriesForLines
4
+ getStackedLinesTimeSeriesPerStackAndUnit
7
5
  } from '../../../../common/timeSeries';
8
6
  import { LinesData } from '../models';
9
7
 
@@ -14,53 +12,28 @@ interface StackedLinesState {
14
12
 
15
13
  const useStackedLines = ({ lines, timeSeries }): StackedLinesState => {
16
14
  const regularStackedLines = getNotInvertedStackedLines(lines);
17
- const regularStackedUnits = uniq(pluck('unit', regularStackedLines));
18
- const regularStackedLinesTimeSeriesPerUnit = regularStackedUnits.reduce(
19
- (acc, stackedUnit) => {
20
- const relatedLines = regularStackedLines.filter(({ unit }) =>
21
- equals(unit, stackedUnit)
22
- );
23
-
24
- return {
25
- ...acc,
26
- [stackedUnit]: {
27
- lines: relatedLines,
28
- timeSeries: getTimeSeriesForLines({
29
- lines: relatedLines,
30
- timeSeries
31
- })
32
- }
33
- };
34
- },
35
- {}
36
- );
15
+ const {
16
+ stackedLinesTimeSeriesPerStackKeyAndUnit:
17
+ regularStackedLinesTimeSeriesPerStackKeyAndUnit
18
+ } = getStackedLinesTimeSeriesPerStackAndUnit({
19
+ stackedLines: regularStackedLines,
20
+ timeSeries
21
+ });
37
22
 
38
23
  const invertedStackedLines = getInvertedStackedLines(lines);
39
- const invertedStackedUnits = uniq(pluck('unit', invertedStackedLines));
40
- const invertedStackedLinesTimeSeriesPerUnit = invertedStackedUnits.reduce(
41
- (acc, stackedUnit) => {
42
- const relatedLines = invertedStackedLines.filter(({ unit }) =>
43
- equals(unit, stackedUnit)
44
- );
45
24
 
46
- return {
47
- ...acc,
48
- [stackedUnit]: {
49
- lines: relatedLines,
50
- timeSeries: getTimeSeriesForLines({
51
- invert: true,
52
- lines: relatedLines,
53
- timeSeries
54
- })
55
- }
56
- };
57
- },
58
- {}
59
- );
25
+ const {
26
+ stackedLinesTimeSeriesPerStackKeyAndUnit:
27
+ invertedStackedLinesTimeSeriesPerStackKeyAndUnit
28
+ } = getStackedLinesTimeSeriesPerStackAndUnit({
29
+ stackedLines: invertedStackedLines,
30
+ timeSeries,
31
+ invert: true
32
+ });
60
33
 
61
34
  return {
62
- invertedStackedLinesData: invertedStackedLinesTimeSeriesPerUnit,
63
- stackedLinesData: regularStackedLinesTimeSeriesPerUnit
35
+ invertedStackedLinesData: invertedStackedLinesTimeSeriesPerStackKeyAndUnit,
36
+ stackedLinesData: regularStackedLinesTimeSeriesPerStackKeyAndUnit
64
37
  };
65
38
  };
66
39
 
@@ -101,34 +101,40 @@ const Lines = ({
101
101
  {(areaStackedLines?.display ?? true) && (
102
102
  <>
103
103
  {Object.entries(stackedLinesData).map(
104
- ([unit, { lines, timeSeries: stackedTimeSeries }]) => (
105
- <StackedLines
106
- lineStyle={lineStyle}
107
- key={`stacked-${unit}`}
108
- lines={lines}
109
- timeSeries={stackedTimeSeries}
110
- yScale={yScalesPerUnit[unit]}
111
- {...commonStackedLinesProps}
112
- />
113
- )
104
+ ([stackedKey, { lines, timeSeries: stackedTimeSeries }]) => {
105
+ const [, unit] = stackedKey.split('-');
106
+ return (
107
+ <StackedLines
108
+ lineStyle={lineStyle}
109
+ key={`stacked-${unit}`}
110
+ lines={lines}
111
+ timeSeries={stackedTimeSeries}
112
+ yScale={yScalesPerUnit[unit || undefined]}
113
+ {...commonStackedLinesProps}
114
+ />
115
+ );
116
+ }
114
117
  )}
115
118
  {Object.entries(invertedStackedLinesData).map(
116
- ([unit, { lines, timeSeries: stackedTimeSeries }]) => (
117
- <StackedLines
118
- lineStyle={lineStyle}
119
- key={`invert-stacked-${unit}`}
120
- lines={lines}
121
- timeSeries={stackedTimeSeries}
122
- yScale={getYScale({
123
- invert: '1',
124
- scale,
125
- scaleLogarithmicBase,
126
- unit,
127
- yScalesPerUnit
128
- })}
129
- {...commonStackedLinesProps}
130
- />
131
- )
119
+ ([stackedKey, { lines, timeSeries: stackedTimeSeries }]) => {
120
+ const [, unit] = stackedKey.split('-');
121
+ return (
122
+ <StackedLines
123
+ lineStyle={lineStyle}
124
+ key={`invert-stacked-${unit}`}
125
+ lines={lines}
126
+ timeSeries={stackedTimeSeries}
127
+ yScale={getYScale({
128
+ invert: '1',
129
+ scale,
130
+ scaleLogarithmicBase,
131
+ unit: unit || undefined,
132
+ yScalesPerUnit
133
+ })}
134
+ {...commonStackedLinesProps}
135
+ />
136
+ );
137
+ }
132
138
  )}
133
139
  </>
134
140
  )}
@@ -180,7 +180,7 @@ describe('Line chart', () => {
180
180
  cy.contains('06/18/2023').should('be.visible');
181
181
 
182
182
  cy.contains('0.4 s').should('be.visible');
183
- cy.contains('75.64%').should('be.visible');
183
+ cy.contains('75.64%').should('be.visible');
184
184
 
185
185
  cy.makeSnapshot();
186
186
  });
@@ -532,7 +532,7 @@ describe('Line chart', () => {
532
532
 
533
533
  checkGraphWidth();
534
534
  cy.contains(':00 AM').should('be.visible');
535
- cy.get('circle[cx="250.83333333333334"]').should('be.visible');
535
+ cy.get('circle[cx="248.33333333333334"]').should('be.visible');
536
536
  cy.get('circle[cy="251.79089393069725"]').should('be.visible');
537
537
 
538
538
  cy.makeSnapshot();
@@ -591,7 +591,7 @@ describe('Line chart', () => {
591
591
  cy.get('path.visx-area-closed')
592
592
  .should('have.attr', 'stroke-dasharray')
593
593
  .and('equals', '5 4');
594
- cy.get('circle[cx="33.44444444444444"]').should('be.visible');
594
+ cy.get('circle[cx="33.11111111111111"]').should('be.visible');
595
595
 
596
596
  cy.makeSnapshot();
597
597
  });
@@ -746,9 +746,11 @@ describe('Lines and bars', () => {
746
746
 
747
747
  checkGraphWidth();
748
748
 
749
- cy.get('path[d="M7.501377410468319,350.5553648585503 h56.51239669421488 h1v1 v23.44463514144968 a1,1 0 0 1 -1,1 h-56.51239669421488 a1,1 0 0 1 -1,-1 v-23.44463514144968 v-1h1z"]'
749
+ cy.get(
750
+ 'path[d="M7.501377410468319,350.5553648585503 h56.51239669421488 h1v1 v23.44463514144968 a1,1 0 0 1 -1,1 h-56.51239669421488 a1,1 0 0 1 -1,-1 v-23.44463514144968 v-1h1z"]'
750
751
  ).should('be.visible');
751
- cy.get('path[d="M24.05509641873278,201.58170928199803 h23.404958677685954 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,17.553719008264462 v113.86621756002336 v17.553719008264462h-17.553719008264462 h-23.404958677685954 h-17.553719008264462v-17.553719008264462 v-113.86621756002336 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,-17.553719008264462z"]'
752
+ cy.get(
753
+ 'path[d="M24.05509641873278,201.58170928199803 h23.404958677685954 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,17.553719008264462 v113.86621756002336 v17.553719008264462h-17.553719008264462 h-23.404958677685954 h-17.553719008264462v-17.553719008264462 v-113.86621756002336 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,-17.553719008264462z"]'
752
754
  ).should('be.visible');
753
755
 
754
756
  cy.makeSnapshot();
@@ -814,4 +816,21 @@ describe('Lines and bars', () => {
814
816
 
815
817
  cy.makeSnapshot();
816
818
  });
819
+
820
+ it('calls the secondary function when a metric is clicked in the legend', () => {
821
+ const secondaryClick = cy.stub().as('secondaryClick');
822
+ initialize({
823
+ data: dataPingServiceLines,
824
+ legend: {
825
+ mode: 'grid',
826
+ placement: 'bottom',
827
+ secondaryClick
828
+ }
829
+ });
830
+
831
+ checkGraphWidth();
832
+
833
+ cy.contains('Packet Loss').rightclick();
834
+ cy.get('@secondaryClick').should('have.been.called');
835
+ });
817
836
  });
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
2
2
 
3
3
  import { Meta, StoryObj } from '@storybook/react';
4
4
 
5
- import { Button } from '@mui/material';
5
+ import { Button, Menu } from '@mui/material';
6
6
  import ButtonGroup from '@mui/material/ButtonGroup';
7
7
  import Switch from '@mui/material/Switch';
8
8
  import Tooltip from '@mui/material/Tooltip';
@@ -26,6 +26,7 @@ import dataPingService from '../mockedData/pingService.json';
26
26
  import dataPingServiceLinesBars from '../mockedData/pingServiceLinesBars.json';
27
27
  import dataPingServiceLinesBarsMixed from '../mockedData/pingServiceLinesBarsMixed.json';
28
28
  import dataPingServiceLinesBarsStacked from '../mockedData/pingServiceLinesBarsStacked.json';
29
+ import dataPingServiceLinesStackKeys from '../mockedData/pingServiceWithStackedKeys.json';
29
30
  import dataZoomPreview from '../mockedData/zoomPreview.json';
30
31
 
31
32
  import { dateTimeFormat } from './common';
@@ -758,3 +759,44 @@ export const linesAndBarsMinMaxForUnit: Story = {
758
759
  />
759
760
  )
760
761
  };
762
+
763
+ const LegendSecondaryClick = (args) => {
764
+ const [anchor, setAnchor] = useState<EventTarget | null>(null);
765
+
766
+ return (
767
+ <>
768
+ <WrapperChart
769
+ {...args}
770
+ legend={{
771
+ secondaryClick: ({ element }) => setAnchor(element)
772
+ }}
773
+ />
774
+ <Menu
775
+ anchorEl={anchor}
776
+ open={Boolean(anchor)}
777
+ onClose={() => setAnchor(null)}
778
+ >
779
+ menu
780
+ </Menu>
781
+ </>
782
+ );
783
+ };
784
+
785
+ export const withLegendSecondaryClick: Story = {
786
+ argTypes,
787
+ args: argumentsData,
788
+ render: (args) => (
789
+ <LegendSecondaryClick
790
+ {...args}
791
+ data={dataPingService as unknown as LineChartData}
792
+ />
793
+ )
794
+ };
795
+
796
+ export const stackedKey: Story = {
797
+ argTypes,
798
+ args: {
799
+ ...argumentsData,
800
+ data: dataPingServiceLinesStackKeys
801
+ }
802
+ };
@@ -277,7 +277,8 @@ const Chart = ({
277
277
  legendHeight: legend?.height,
278
278
  mode: legend?.mode,
279
279
  placement: legend?.placement,
280
- renderExtraComponent: legend?.renderExtraComponent
280
+ renderExtraComponent: legend?.renderExtraComponent,
281
+ secondaryClick: legend?.secondaryClick
281
282
  }}
282
283
  legendRef={legendRef}
283
284
  limitLegend={limitLegend}
@@ -394,4 +395,4 @@ const Chart = ({
394
395
  );
395
396
  };
396
397
 
397
- export default Chart;
398
+ export default Chart;
@@ -26,6 +26,11 @@ interface Props extends Pick<LegendModel, 'placement' | 'mode'> {
26
26
  setLinesGraph: Dispatch<SetStateAction<Array<Line> | null>>;
27
27
  shouldDisplayLegendInCompactMode: boolean;
28
28
  toggable?: boolean;
29
+ secondaryClick?: (props: {
30
+ element: EventTarget | null;
31
+ metricId: number | string;
32
+ position: [number, number];
33
+ }) => void;
29
34
  }
30
35
 
31
36
  const MainLegend = ({
@@ -38,7 +43,8 @@ const MainLegend = ({
38
43
  shouldDisplayLegendInCompactMode,
39
44
  placement,
40
45
  height,
41
- mode
46
+ mode,
47
+ secondaryClick
42
48
  }: Props): JSX.Element => {
43
49
  const { classes, cx } = useStyles({
44
50
  limitLegendRows: Boolean(limitLegend),
@@ -65,7 +71,24 @@ const MainLegend = ({
65
71
  value
66
72
  }) || 'N/A';
67
73
 
68
- const selectMetric = ({ event, metric_id }): void => {
74
+ const contextMenuClick =
75
+ (metricId: number) =>
76
+ (event: MouseEvent): void => {
77
+ if (!secondaryClick) {
78
+ return;
79
+ }
80
+ event.preventDefault();
81
+ secondaryClick({
82
+ element: event.target,
83
+ metricId,
84
+ position: [event.pageX, event.pageY]
85
+ });
86
+ };
87
+
88
+ const selectMetric = ({
89
+ event,
90
+ metric_id
91
+ }: { event: MouseEvent; metric_id: number }): void => {
69
92
  if (!toggable) {
70
93
  return;
71
94
  }
@@ -127,6 +150,7 @@ const MainLegend = ({
127
150
  onClick={(event): void => selectMetric({ event, metric_id })}
128
151
  onMouseEnter={(): void => highlightLine(metric_id)}
129
152
  onMouseLeave={(): void => clearHighlight()}
153
+ onContextMenu={contextMenuClick(metric_id)}
130
154
  >
131
155
  <LegendHeader
132
156
  color={markerColor}