@centreon/ui 25.11.2 → 25.11.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "25.11.2",
3
+ "version": "25.11.4",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -142,6 +142,7 @@
142
142
  "@visx/visx": "3.12.0",
143
143
  "@visx/zoom": "^3.12.0",
144
144
  "anylogger": "^1.0.11",
145
+ "chromatic": "^13.3.3",
145
146
  "d3-array": "3.2.4",
146
147
  "dayjs": "^1.11.13",
147
148
  "highlight.js": "^11.11.1",
@@ -137,6 +137,8 @@ const ConnectedAutocomplete = ({
137
137
  getEndpoint={getEndpoint}
138
138
  decoder={connectedAutocomplete?.decoder}
139
139
  getRenderedOptionText={connectedAutocomplete?.getRenderedOptionText}
140
+ getOptionLabel={connectedAutocomplete?.getOptionLabel}
141
+ optionProperty={connectedAutocomplete?.optionProperty}
140
142
  initialPage={1}
141
143
  isOptionEqualToValue={isOptionEqualToValue}
142
144
  label={t(label)}
@@ -62,6 +62,8 @@ export interface InputProps {
62
62
  endpoint?: string;
63
63
  filterKey?: string;
64
64
  getRenderedOptionText?: (option) => string | JSX.Element;
65
+ getOptionLabel?: (option) => string;
66
+ optionProperty?: string;
65
67
  disableSelectAll?: boolean;
66
68
  limitTags?: number;
67
69
  decoder?;
@@ -11,6 +11,7 @@ import dataPingServiceMixedStacked from '../mockedData/pingServiceMixedStacked.j
11
11
  import dataPingServiceStacked from '../mockedData/pingServiceStacked.json';
12
12
  import dataPingServiceLinesStackKeys from '../mockedData/pingServiceWithStackedKeys.json';
13
13
 
14
+ import { labelAvg, labelMax, labelMin } from '../Chart/translatedLabels';
14
15
  import BarChart, { BarChartProps } from './BarChart';
15
16
 
16
17
  const defaultStart = new Date(
@@ -331,4 +332,24 @@ describe('Bar chart', () => {
331
332
 
332
333
  cy.makeSnapshot();
333
334
  });
335
+
336
+ it('does not displays corresponding calculations when props are set', () => {
337
+ initialize({
338
+ data: dataLastWeek,
339
+ orientation: 'horizontal',
340
+ legend: {
341
+ placement: 'bottom',
342
+ mode: 'grid',
343
+ showCalculations: {
344
+ min: true,
345
+ max: false,
346
+ avg: false
347
+ }
348
+ }
349
+ });
350
+
351
+ cy.contains(labelMin).should('be.visible');
352
+ cy.contains(labelMax).should('not.exist');
353
+ cy.contains(labelAvg).should('not.exist');
354
+ });
334
355
  });
@@ -1,5 +1,6 @@
1
1
  import { Meta, StoryObj } from '@storybook/react';
2
2
  import dayjs from 'dayjs';
3
+ import '../../ThemeProvider/tailwindcss.css';
3
4
 
4
5
  import { LineChartData } from '../common/models';
5
6
  import dataPingService from '../mockedData/pingService.json';
@@ -45,6 +46,11 @@ export const withCenteredZero: Story = {
45
46
  ...defaultArgs,
46
47
  axis: {
47
48
  isCenteredZero: true
49
+ },
50
+ legend: {
51
+ showCalculations: { avg: true, max: false, min: false },
52
+ mode: 'grid',
53
+ placement: 'bottom'
48
54
  }
49
55
  },
50
56
  render: Template
@@ -310,3 +316,15 @@ export const stackKey: Story = {
310
316
  },
311
317
  render: Template
312
318
  };
319
+
320
+ export const withControlledCalculations: Story = {
321
+ args: {
322
+ ...defaultArgs,
323
+ legend: {
324
+ showCalculations: { avg: true, max: false, min: false },
325
+ mode: 'grid',
326
+ placement: 'bottom'
327
+ }
328
+ },
329
+ render: Template
330
+ };
@@ -61,7 +61,16 @@ const BarChart = ({
61
61
  height = 500,
62
62
  tooltip,
63
63
  axis,
64
- legend,
64
+ legend = {
65
+ display: true,
66
+ mode: 'grid',
67
+ placement: 'bottom',
68
+ showCalculations: {
69
+ min: true,
70
+ max: true,
71
+ avg: true
72
+ }
73
+ },
65
74
  loading,
66
75
  limitLegend,
67
76
  thresholdUnit,
@@ -235,7 +235,8 @@ const ResponsiveBarChart = ({
235
235
  mode: legend?.mode,
236
236
  placement: legend?.placement,
237
237
  renderExtraComponent: legend?.renderExtraComponent,
238
- secondaryClick: legend?.secondaryClick
238
+ secondaryClick: legend?.secondaryClick,
239
+ showCalculations: legend?.showCalculations
239
240
  }}
240
241
  legendRef={legendRef}
241
242
  limitLegend={limitLegend}
@@ -72,15 +72,15 @@ export const useBarStack = ({
72
72
 
73
73
  const commonBarStackProps = isHorizontal
74
74
  ? {
75
- x: (d) => d.timeTick,
76
- xScale,
77
- yScale
78
- }
75
+ x: (d) => d.timeTick,
76
+ xScale,
77
+ yScale
78
+ }
79
79
  : {
80
- xScale: yScale,
81
- y: (d) => d.timeTick,
82
- yScale: xScale
83
- };
80
+ xScale: yScale,
81
+ y: (d) => d.timeTick,
82
+ yScale: xScale
83
+ };
84
84
 
85
85
  const hoverBar = useCallback(
86
86
  ({ highlightedMetric, barIndex }: HoverBarProps) =>
@@ -96,7 +96,7 @@ export const useBarStack = ({
96
96
  index: barIndex
97
97
  });
98
98
  },
99
- []
99
+ [lines, timeSeries]
100
100
  );
101
101
 
102
102
  const exitBar = useCallback((): void => {
@@ -18,6 +18,7 @@ import { args as argumentsData } from './helpers/doc';
18
18
  import { LineChartProps } from './models';
19
19
 
20
20
  import WrapperChart from '.';
21
+ import { labelAvg, labelMin } from './translatedLabels';
21
22
 
22
23
  interface Props
23
24
  extends Pick<
@@ -153,7 +154,7 @@ const initializeCustomUnits = ({
153
154
  const checkGraphWidth = (): void => {
154
155
  cy.findByTestId('graph-interaction-zone')
155
156
  .should('have.attr', 'height')
156
- .and('equal', '376');
157
+ .and('equal', '389');
157
158
 
158
159
  cy.findByTestId('graph-interaction-zone').then((graph) => {
159
160
  expect(Number(graph[0].attributes.width.value)).to.be.greaterThan(1170);
@@ -295,23 +296,23 @@ describe('Line chart', () => {
295
296
  .should('have.attr', 'width')
296
297
  .and('equal', '1200');
297
298
 
298
- cy.findByLabelText('Centreon-Server: Round-Trip Average Time')
299
- .find('[data-icon="true"]')
299
+ cy.get('[data-icon="true"]')
300
+ .eq(0)
300
301
  .should('have.css', 'background-color', 'rgb(41, 175, 238)');
301
- cy.findByLabelText('Centreon-Server_5: Round-Trip Average Time')
302
- .find('[data-icon="true"]')
302
+ cy.get('[data-icon="true"]')
303
+ .eq(1)
303
304
  .should('have.css', 'background-color', 'rgb(83, 191, 241)');
304
- cy.findByLabelText('Centreon-Server_4: Round-Trip Average Time')
305
- .find('[data-icon="true"]')
305
+ cy.get('[data-icon="true"]')
306
+ .eq(2)
306
307
  .should('have.css', 'background-color', 'rgb(8, 34, 47)');
307
- cy.findByLabelText('Centreon-Server_3: Round-Trip Average Time')
308
- .find('[data-icon="true"]')
308
+ cy.get('[data-icon="true"]')
309
+ .eq(3)
309
310
  .should('have.css', 'background-color', 'rgb(16, 70, 95)');
310
- cy.findByLabelText('Centreon-Server_2: Round-Trip Average Time')
311
- .find('[data-icon="true"]')
311
+ cy.get('[data-icon="true"]')
312
+ .eq(4)
312
313
  .should('have.css', 'background-color', 'rgb(24, 105, 142)');
313
- cy.findByLabelText('Centreon-Server_1: Round-Trip Average Time')
314
- .find('[data-icon="true"]')
314
+ cy.get('[data-icon="true"]')
315
+ .eq(5)
315
316
  .should('have.css', 'background-color', 'rgb(32, 140, 190)');
316
317
 
317
318
  cy.get('[data-metric="1"]').should(
@@ -451,7 +452,7 @@ describe('Line chart', () => {
451
452
 
452
453
  cy.contains(':00 AM').should('be.visible');
453
454
 
454
- cy.get('text[transform="rotate(-35, -2, 274.47726401277305)"]').should(
455
+ cy.get('text[transform="rotate(-35, -2, 205.66612100897808)"]').should(
455
456
  'be.visible'
456
457
  );
457
458
 
@@ -533,7 +534,7 @@ describe('Line chart', () => {
533
534
  checkGraphWidth();
534
535
  cy.contains(':00 AM').should('be.visible');
535
536
  cy.get('circle[cx="248.33333333333334"]').should('be.visible');
536
- cy.get('circle[cy="251.79089393069725"]').should('be.visible');
537
+ cy.get('circle[cy="257.3178022124914"]').should('be.visible');
537
538
 
538
539
  cy.makeSnapshot();
539
540
  });
@@ -747,10 +748,10 @@ describe('Lines and bars', () => {
747
748
  checkGraphWidth();
748
749
 
749
750
  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"]'
751
+ 'path[d="M7.501377410468319,287.7051801494232 h56.51239669421488 h1v1 v99.2948198505768 a1,1 0 0 1 -1,1 h-56.51239669421488 a1,1 0 0 1 -1,-1 v-99.2948198505768 v-1h1z"]'
751
752
  ).should('be.visible');
752
753
  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"]'
754
+ 'path[d="M24.05509641873278,233.5659996490948 h23.404958677685954 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,17.553719008264462 v19.03174248379947 v17.553719008264462h-17.553719008264462 h-23.404958677685954 h-17.553719008264462v-17.553719008264462 v-19.03174248379947 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,-17.553719008264462z"]'
754
755
  ).should('be.visible');
755
756
 
756
757
  cy.makeSnapshot();
@@ -833,4 +834,23 @@ describe('Lines and bars', () => {
833
834
  cy.contains('Packet Loss').rightclick();
834
835
  cy.get('@secondaryClick').should('have.been.called');
835
836
  });
837
+
838
+ it('does not displays corresponding calculations when props are set', () => {
839
+ initialize({
840
+ data: dataPingServiceLines,
841
+ legend: {
842
+ placement: 'bottom',
843
+ mode: 'grid',
844
+ showCalculations: {
845
+ min: true,
846
+ max: false,
847
+ avg: false
848
+ }
849
+ }
850
+ });
851
+
852
+ cy.contains(labelMin).should('be.visible');
853
+ cy.contains(/^Max$/).should('not.exist');
854
+ cy.contains(labelAvg).should('not.exist');
855
+ });
836
856
  });
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
 
3
3
  import { Meta, StoryObj } from '@storybook/react';
4
+ import '../../ThemeProvider/tailwindcss.css';
4
5
 
5
6
  import { Button, Menu } from '@mui/material';
6
7
  import ButtonGroup from '@mui/material/ButtonGroup';
@@ -800,3 +801,23 @@ export const stackedKey: Story = {
800
801
  data: dataPingServiceLinesStackKeys
801
802
  }
802
803
  };
804
+
805
+ export const WithControlledCalculations: Story = {
806
+ ...Template,
807
+ argTypes,
808
+ args: {
809
+ ...argumentsData,
810
+ lineStyle: {
811
+ curve: 'step'
812
+ },
813
+ legend: {
814
+ mode: 'grid',
815
+ placement: 'bottom',
816
+ showCalculations: {
817
+ avg: true,
818
+ max: true,
819
+ min: false
820
+ }
821
+ }
822
+ }
823
+ };
@@ -273,11 +273,13 @@ const Chart = ({
273
273
  header={header}
274
274
  height={height}
275
275
  legend={{
276
+ ...legend,
276
277
  displayLegend,
277
278
  legendHeight: legend?.height,
278
279
  mode: legend?.mode,
279
280
  placement: legend?.placement,
280
281
  renderExtraComponent: legend?.renderExtraComponent,
282
+ showCalculations: legend?.showCalculations,
281
283
  secondaryClick: legend?.secondaryClick
282
284
  }}
283
285
  legendRef={legendRef}
@@ -1,7 +1,5 @@
1
1
  import { makeStyles } from 'tss-react/mui';
2
2
 
3
- import { equals, lt } from 'ramda';
4
- import { margin } from '../common';
5
3
  import type { LegendModel } from '../models';
6
4
 
7
5
  interface MakeStylesProps {
@@ -14,20 +12,8 @@ export const legendWidth = 21;
14
12
  const legendItemHeight = 5.25;
15
13
  const legendItemHeightCompact = 2;
16
14
 
17
- const getLegendMaxHeight = ({ placement, height }) => {
18
- if (!equals(placement, 'bottom')) {
19
- return height || 0;
20
- }
21
-
22
- if (lt(height || 0, 220)) {
23
- return 40;
24
- }
25
-
26
- return 90;
27
- };
28
-
29
15
  export const useStyles = makeStyles<MakeStylesProps>()(
30
- (theme, { limitLegendRows, placement, height = 0 }) => ({
16
+ (theme, { limitLegendRows }) => ({
31
17
  highlight: {
32
18
  color: theme.typography.body1.color
33
19
  },
@@ -54,21 +40,6 @@ export const useStyles = makeStyles<MakeStylesProps>()(
54
40
  rowGap: theme.spacing(1),
55
41
  width: '100%'
56
42
  },
57
- legend: {
58
- '&[data-display-side="false"]': {
59
- marginLeft: margin.left,
60
- marginRight: margin.right
61
- },
62
- '&[data-display-side="true"]': {
63
- height: '100%',
64
- marginTop: `${margin.top / 2}px`
65
- },
66
- maxHeight: limitLegendRows
67
- ? theme.spacing(legendItemHeight * 2 + 1)
68
- : getLegendMaxHeight({ placement, height }),
69
- overflowY: 'auto',
70
- overflowX: 'hidden'
71
- },
72
43
  minMaxAvgContainer: {
73
44
  columnGap: theme.spacing(0.5),
74
45
  display: 'grid',
@@ -15,7 +15,7 @@ const LegendContent = ({ data, label }: Props): JSX.Element => {
15
15
  const { t } = useTranslation();
16
16
 
17
17
  return (
18
- <div className={classes.text} data-testid={label}>
18
+ <div className="leading-[1.2]" data-testid={label}>
19
19
  <Typography className={classes.text} component="span" variant="caption">
20
20
  {t(label)}:{' '}
21
21
  <Typography
@@ -8,13 +8,11 @@ import {
8
8
  import { Tooltip } from '../../../components';
9
9
  import { Line } from '../../common/timeSeries/models';
10
10
 
11
- import { useLegendHeaderStyles } from './Legend.styles';
11
+ import { ReactElement } from 'react';
12
12
  import LegendContent from './LegendContent';
13
13
  import { LegendDisplayMode } from './models';
14
14
 
15
15
  interface Props {
16
- color: string;
17
- disabled?: boolean;
18
16
  isDisplayedOnSide: boolean;
19
17
  isListMode: boolean;
20
18
  line: Line;
@@ -25,16 +23,12 @@ interface Props {
25
23
 
26
24
  const LegendHeader = ({
27
25
  line,
28
- color,
29
- disabled,
30
26
  value,
31
27
  minMaxAvg,
32
28
  isListMode,
33
29
  isDisplayedOnSide,
34
30
  unit
35
- }: Props): JSX.Element => {
36
- const { classes, cx } = useLegendHeaderStyles({ color });
37
-
31
+ }: Props): ReactElement => {
38
32
  const { name, legend } = line;
39
33
 
40
34
  const metricName = formatMetricName({ legend, name });
@@ -42,16 +36,14 @@ const LegendHeader = ({
42
36
  const legendName = legend || name;
43
37
 
44
38
  return (
45
- <div
46
- className={cx(!isListMode ? classes.container : classes.containerList)}
47
- >
39
+ <div className={isListMode ? 'w-fit' : 'w-full'}>
48
40
  <Tooltip
49
41
  followCursor={false}
50
42
  label={
51
43
  minMaxAvg ? (
52
44
  <div>
53
45
  <Typography>{legendName}</Typography>
54
- <div className={classes.minMaxAvgContainer}>
46
+ <div className="flex flex-wrap gap-1 whitespace-nowrap">
55
47
  {minMaxAvg.map(({ label, value: subValue }) => (
56
48
  <LegendContent
57
49
  data={formatMetricValue({
@@ -70,18 +62,10 @@ const LegendHeader = ({
70
62
  }
71
63
  placement={isListMode ? 'right' : 'top'}
72
64
  >
73
- <div className={classes.markerAndLegendName}>
74
- <div
75
- data-icon
76
- className={cx(classes.icon, { [classes.disabled]: disabled })}
77
- />
65
+ <div className="flex items-center gap-1">
78
66
  <EllipsisTypography
79
- className={classes.text}
80
- containerClassname={cx(
81
- !isListMode && classes.legendName,
82
- isListMode && !isDisplayedOnSide && classes.textListBottom,
83
- isListMode && isDisplayedOnSide && classes.legendName
84
- )}
67
+ className="text-xs leading-none font-medium"
68
+ containerClassname={`w-auto ${(!isListMode || (isListMode && isDisplayedOnSide)) && 'max-w-[166px]'}`}
85
69
  data-mode={
86
70
  value ? LegendDisplayMode.Compact : LegendDisplayMode.Normal
87
71
  }
@@ -1,8 +1,16 @@
1
- import { Dispatch, ReactNode, SetStateAction, useMemo } from 'react';
1
+ import {
2
+ Dispatch,
3
+ KeyboardEvent,
4
+ MouseEvent,
5
+ ReactElement,
6
+ ReactNode,
7
+ SetStateAction,
8
+ useMemo
9
+ } from 'react';
2
10
 
3
11
  import { equals, prop, slice, sortBy } from 'ramda';
4
12
 
5
- import { Box, alpha, useTheme } from '@mui/material';
13
+ import { alpha, useTheme } from '@mui/material';
6
14
 
7
15
  import { useMemoComponent } from '@centreon/ui';
8
16
 
@@ -10,14 +18,13 @@ import { formatMetricValue } from '../../common/timeSeries';
10
18
  import { Line } from '../../common/timeSeries/models';
11
19
  import { LegendModel } from '../models';
12
20
  import { labelAvg, labelMax, labelMin } from '../translatedLabels';
13
-
14
- import { useStyles } from './Legend.styles';
15
21
  import LegendContent from './LegendContent';
16
22
  import LegendHeader from './LegendHeader';
17
23
  import { GetMetricValueProps, LegendDisplayMode } from './models';
18
24
  import useLegend from './useLegend';
19
25
 
20
- interface Props extends Pick<LegendModel, 'placement' | 'mode'> {
26
+ interface Props
27
+ extends Pick<LegendModel, 'placement' | 'mode' | 'showCalculations'> {
21
28
  base: number;
22
29
  height: number | null;
23
30
  limitLegend?: false | number;
@@ -42,15 +49,14 @@ const MainLegend = ({
42
49
  setLinesGraph,
43
50
  shouldDisplayLegendInCompactMode,
44
51
  placement,
45
- height,
46
52
  mode,
53
+ showCalculations = {
54
+ min: true,
55
+ max: true,
56
+ avg: true
57
+ },
47
58
  secondaryClick
48
- }: Props): JSX.Element => {
49
- const { classes, cx } = useStyles({
50
- limitLegendRows: Boolean(limitLegend),
51
- placement,
52
- height
53
- });
59
+ }: Props): ReactElement => {
54
60
  const theme = useTheme();
55
61
 
56
62
  const { selectMetricLine, clearHighlight, highlightLine, toggleMetricLine } =
@@ -73,22 +79,25 @@ const MainLegend = ({
73
79
 
74
80
  const contextMenuClick =
75
81
  (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
- };
82
+ (event: MouseEvent): void => {
83
+ if (!secondaryClick) {
84
+ return;
85
+ }
86
+ event.preventDefault();
87
+ secondaryClick({
88
+ element: event.target,
89
+ metricId,
90
+ position: [event.pageX, event.pageY]
91
+ });
92
+ };
87
93
 
88
94
  const selectMetric = ({
89
95
  event,
90
96
  metric_id
91
- }: { event: MouseEvent; metric_id: number }): void => {
97
+ }: {
98
+ event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>;
99
+ metric_id: number;
100
+ }): void => {
92
101
  if (!toggable) {
93
102
  return;
94
103
  }
@@ -109,83 +118,87 @@ const MainLegend = ({
109
118
 
110
119
  return (
111
120
  <div
112
- className={classes.legend}
121
+ className={`overflow-x-hidden overflow-y-auto ${!equals(placement, 'bottom') ? 'h-full mt-[15px]' : 'ml-[50px] mr-[40px]'} legend`}
113
122
  data-display-side={!equals(placement, 'bottom')}
114
123
  >
115
- <div
116
- className={classes.items}
124
+ <ul
125
+ className={`list-none flex gap-3 w-full ${!isListMode && equals(placement, 'bottom') && 'flex-wrap'} ${(isListMode || !equals(placement, 'bottom')) && 'flex-col h-full w-fit'} ${equals(placement, 'bottom') ? 'max-h-[67px]' : 'max-h-0'}`}
117
126
  data-as-list={isListMode || !equals(placement, 'bottom')}
118
127
  data-mode={itemMode}
119
128
  >
120
129
  {displayedLines.map((line) => {
121
- const { color, display, highlight, metric_id, unit } = line;
130
+ const { color, display, metric_id, unit } = line;
122
131
 
123
132
  const markerColor = display
124
133
  ? color
125
134
  : alpha(theme.palette.text.disabled, 0.2);
126
135
 
127
136
  const minMaxAvg = [
128
- {
137
+ showCalculations.min && {
129
138
  label: labelMin,
130
139
  value: line.minimum_value
131
140
  },
132
- {
141
+ showCalculations.max && {
133
142
  label: labelMax,
134
143
  value: line.maximum_value
135
144
  },
136
- {
145
+ showCalculations.avg && {
137
146
  label: labelAvg,
138
147
  value: line.average_value
139
148
  }
140
- ];
149
+ ].filter(Boolean);
141
150
 
142
151
  return (
143
- <Box
144
- className={cx(
145
- classes.item,
146
- highlight ? classes.highlight : classes.normal,
147
- toggable && classes.toggable
148
- )}
152
+ <li
153
+ className={`${!display ? 'text-text-disabled' : 'text-text-primary'} flex gap-1 ${toggable && 'cursor-pointer'}`}
149
154
  key={metric_id}
150
155
  onClick={(event): void => selectMetric({ event, metric_id })}
156
+ onKeyUp={(event) =>
157
+ event.key === 'Enter' && selectMetric({ event, metric_id })
158
+ }
151
159
  onMouseEnter={(): void => highlightLine(metric_id)}
152
160
  onMouseLeave={(): void => clearHighlight()}
153
161
  onContextMenu={contextMenuClick(metric_id)}
154
162
  >
155
- <LegendHeader
156
- color={markerColor}
157
- disabled={!display}
158
- isDisplayedOnSide={!equals(placement, 'bottom')}
159
- isListMode={isListMode}
160
- line={line}
161
- minMaxAvg={
162
- shouldDisplayLegendInCompactMode ? minMaxAvg : undefined
163
- }
164
- unit={unit}
163
+ <div
164
+ className="h-full rounded-sm w-1"
165
+ style={{ backgroundColor: markerColor }}
166
+ data-icon
165
167
  />
166
- {!shouldDisplayLegendInCompactMode && !isListMode && (
167
- <div>
168
- <div className={classes.minMaxAvgContainer}>
169
- {minMaxAvg.map(({ label, value }) => (
170
- <LegendContent
171
- data={getMetricValue({ unit: line.unit, value })}
172
- key={label}
173
- label={label}
174
- />
175
- ))}
168
+ <div>
169
+ <LegendHeader
170
+ isDisplayedOnSide={!equals(placement, 'bottom')}
171
+ isListMode={isListMode}
172
+ line={line}
173
+ minMaxAvg={
174
+ shouldDisplayLegendInCompactMode ? minMaxAvg : undefined
175
+ }
176
+ unit={unit}
177
+ />
178
+ {!shouldDisplayLegendInCompactMode && !isListMode && (
179
+ <div>
180
+ <div className="flex flex-wrap gap-1 whitespace-nowrap">
181
+ {minMaxAvg.map(({ label, value }) => (
182
+ <LegendContent
183
+ data={getMetricValue({ unit: line.unit, value })}
184
+ key={label}
185
+ label={label}
186
+ />
187
+ ))}
188
+ </div>
176
189
  </div>
177
- </div>
178
- )}
179
- </Box>
190
+ )}
191
+ </div>
192
+ </li>
180
193
  );
181
194
  })}
182
- </div>
195
+ </ul>
183
196
  {renderExtraComponent}
184
197
  </div>
185
198
  );
186
199
  };
187
200
 
188
- const Legend = (props: Props): JSX.Element => {
201
+ const Legend = (props: Props): ReactElement => {
189
202
  const {
190
203
  toggable,
191
204
  limitLegend,
@@ -59,7 +59,12 @@ const WrapperChart = ({
59
59
  legend = {
60
60
  display: true,
61
61
  mode: 'grid',
62
- placement: 'bottom'
62
+ placement: 'bottom',
63
+ showCalculations: {
64
+ min: true,
65
+ max: true,
66
+ avg: true
67
+ }
63
68
  },
64
69
  header,
65
70
  lineStyle,
@@ -175,6 +175,11 @@ export interface LegendModel {
175
175
  mode: 'grid' | 'list';
176
176
  placement: 'bottom' | 'left' | 'right';
177
177
  renderExtraComponent?: ReactNode;
178
+ showCalculations?: {
179
+ min: boolean;
180
+ max: boolean;
181
+ avg: boolean;
182
+ };
178
183
  secondaryClick?: (props: {
179
184
  element: EventTarget | null;
180
185
  metricId: number | string;
@@ -21,7 +21,11 @@ interface Props {
21
21
  isHorizontal?: boolean;
22
22
  legend: Pick<
23
23
  LegendModel,
24
- 'renderExtraComponent' | 'placement' | 'mode' | 'secondaryClick'
24
+ | 'renderExtraComponent'
25
+ | 'placement'
26
+ | 'mode'
27
+ | 'secondaryClick'
28
+ | 'showCalculations'
25
29
  > & {
26
30
  displayLegend: boolean;
27
31
  legendHeight?: number;
@@ -104,6 +108,7 @@ const BaseChart = ({
104
108
  shouldDisplayLegendInCompactMode={
105
109
  shouldDisplayLegendInCompactMode
106
110
  }
111
+ showCalculations={legend?.showCalculations}
107
112
  secondaryClick={legend?.secondaryClick}
108
113
  />
109
114
  </div>
@@ -127,6 +132,7 @@ const BaseChart = ({
127
132
  setLinesGraph={setLines}
128
133
  shouldDisplayLegendInCompactMode={shouldDisplayLegendInCompactMode}
129
134
  secondaryClick={legend?.secondaryClick}
135
+ showCalculations={legend?.showCalculations}
130
136
  />
131
137
  </div>
132
138
  )}
@@ -65,7 +65,7 @@ const ConnectedAutocompleteField = (
65
65
  open,
66
66
  exclusionOptionProperty = 'id',
67
67
  searchConditions = [],
68
- getRenderedOptionText = (option): string => option.name?.toString(),
68
+ getRenderedOptionText = (option): string => option?.name?.toString(),
69
69
  getRequestHeaders,
70
70
  displayOptionThumbnail,
71
71
  queryKey,
@@ -36,7 +36,7 @@ const MultiAutocompleteField = ({
36
36
  disableSortedOptions = false,
37
37
  disableSelectAll = true,
38
38
  optionProperty = 'name',
39
- getOptionLabel = (option): string => option.name,
39
+ getOptionLabel = (option): string => option?.name,
40
40
  getTagLabel = (option): string => option[optionProperty],
41
41
  getOptionTooltipLabel,
42
42
  chipProps,