@centreon/ui 25.11.3 → 25.11.5

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.3",
3
+ "version": "25.11.5",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -75,7 +75,7 @@
75
75
  "@storybook/test": "^8.6.3",
76
76
  "@storybook/test-runner": "^0.22.0",
77
77
  "@storybook/theming": "^8.6.3",
78
- "@tailwindcss/cli": "^4.1.7",
78
+ "@tailwindcss/cli": "^4.1.17",
79
79
  "@tailwindcss/postcss": "^4.1.7",
80
80
  "@tailwindcss/vite": "^4.1.7",
81
81
  "@testing-library/cypress": "^10.0.3",
@@ -106,7 +106,7 @@
106
106
  "storybook-dark-mode": "^4.0.2",
107
107
  "storybook-react-rsbuild": "^1.0.1",
108
108
  "style-dictionary": "^4.3.3",
109
- "tailwindcss": "^4.1.7",
109
+ "tailwindcss": "^4.1.17",
110
110
  "ts-node": "^10.9.2",
111
111
  "use-resize-observer": "^9.1.0",
112
112
  "vite": "^6.3.5",
@@ -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",
@@ -8,8 +8,8 @@
8
8
  * - Please do NOT serve this file on production.
9
9
  */
10
10
 
11
- const PACKAGE_VERSION = '2.7.3'
12
- const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
11
+ const PACKAGE_VERSION = '2.2.14'
12
+ const INTEGRITY_CHECKSUM = '26357c79639bfa20d64c0efca2a87423'
13
13
  const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
14
14
  const activeClientIds = new Set()
15
15
 
@@ -62,12 +62,7 @@ self.addEventListener('message', async function (event) {
62
62
 
63
63
  sendToClient(client, {
64
64
  type: 'MOCKING_ENABLED',
65
- payload: {
66
- client: {
67
- id: client.id,
68
- frameType: client.frameType,
69
- },
70
- },
65
+ payload: true,
71
66
  })
72
67
  break
73
68
  }
@@ -160,10 +155,6 @@ async function handleRequest(event, requestId) {
160
155
  async function resolveMainClient(event) {
161
156
  const client = await self.clients.get(event.clientId)
162
157
 
163
- if (activeClientIds.has(event.clientId)) {
164
- return client
165
- }
166
-
167
158
  if (client?.frameType === 'top-level') {
168
159
  return client
169
160
  }
@@ -192,26 +183,12 @@ async function getResponse(event, client, requestId) {
192
183
  const requestClone = request.clone()
193
184
 
194
185
  function passthrough() {
195
- // Cast the request headers to a new Headers instance
196
- // so the headers can be manipulated with.
197
- const headers = new Headers(requestClone.headers)
198
-
199
- // Remove the "accept" header value that marked this request as passthrough.
200
- // This prevents request alteration and also keeps it compliant with the
201
- // user-defined CORS policies.
202
- const acceptHeader = headers.get('accept')
203
- if (acceptHeader) {
204
- const values = acceptHeader.split(',').map((value) => value.trim())
205
- const filteredValues = values.filter(
206
- (value) => value !== 'msw/passthrough',
207
- )
186
+ const headers = Object.fromEntries(requestClone.headers.entries())
208
187
 
209
- if (filteredValues.length > 0) {
210
- headers.set('accept', filteredValues.join(', '))
211
- } else {
212
- headers.delete('accept')
213
- }
214
- }
188
+ // Remove internal MSW request header so the passthrough request
189
+ // complies with any potential CORS preflight checks on the server.
190
+ // Some servers forbid unknown request headers.
191
+ delete headers['x-msw-intention']
215
192
 
216
193
  return fetch(requestClone, { headers })
217
194
  }
@@ -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}
@@ -243,6 +244,7 @@ const ResponsiveBarChart = ({
243
244
  setLines={setLinesGraph}
244
245
  title={title}
245
246
  titleRef={titleRef}
247
+ graphHeight={graphHeight}
246
248
  >
247
249
  <Tooltip
248
250
  classes={{
@@ -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', '387');
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, 204.60871164646406)"]').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="225.07536552649066"]').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,286.2259761383722 h56.51239669421488 h1v1 v98.77402386162782 a1,1 0 0 1 -1,1 h-56.51239669421488 a1,1 0 0 1 -1,-1 v-98.77402386162782 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,232.36514618046195 h23.404958677685954 a17.553719008264462,17.553719008264462 0 0 1 17.553719008264462,17.553719008264462 v18.753391941381302 v17.553719008264462h-17.553719008264462 h-23.404958677685954 h-17.553719008264462v-17.553719008264462 v-18.753391941381302 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}
@@ -286,6 +288,7 @@ const Chart = ({
286
288
  setLines={setLinesGraph}
287
289
  title={title}
288
290
  titleRef={titleRef}
291
+ graphHeight={graphHeight}
289
292
  >
290
293
  <GraphValueTooltip
291
294
  baseAxis={baseAxis}
@@ -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-[1.2] 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;
@@ -31,6 +38,7 @@ interface Props extends Pick<LegendModel, 'placement' | 'mode'> {
31
38
  metricId: number | string;
32
39
  position: [number, number];
33
40
  }) => void;
41
+ graphHeight: number;
34
42
  }
35
43
 
36
44
  const MainLegend = ({
@@ -42,15 +50,15 @@ const MainLegend = ({
42
50
  setLinesGraph,
43
51
  shouldDisplayLegendInCompactMode,
44
52
  placement,
45
- height,
46
53
  mode,
47
- secondaryClick
48
- }: Props): JSX.Element => {
49
- const { classes, cx } = useStyles({
50
- limitLegendRows: Boolean(limitLegend),
51
- placement,
52
- height
53
- });
54
+ showCalculations = {
55
+ min: true,
56
+ max: true,
57
+ avg: true
58
+ },
59
+ secondaryClick,
60
+ graphHeight
61
+ }: Props): ReactElement => {
54
62
  const theme = useTheme();
55
63
 
56
64
  const { selectMetricLine, clearHighlight, highlightLine, toggleMetricLine } =
@@ -73,22 +81,25 @@ const MainLegend = ({
73
81
 
74
82
  const contextMenuClick =
75
83
  (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
- };
84
+ (event: MouseEvent): void => {
85
+ if (!secondaryClick) {
86
+ return;
87
+ }
88
+ event.preventDefault();
89
+ secondaryClick({
90
+ element: event.target,
91
+ metricId,
92
+ position: [event.pageX, event.pageY]
93
+ });
94
+ };
87
95
 
88
96
  const selectMetric = ({
89
97
  event,
90
98
  metric_id
91
- }: { event: MouseEvent; metric_id: number }): void => {
99
+ }: {
100
+ event: MouseEvent<HTMLLIElement> | KeyboardEvent<HTMLLIElement>;
101
+ metric_id: number;
102
+ }): void => {
92
103
  if (!toggable) {
93
104
  return;
94
105
  }
@@ -109,83 +120,90 @@ const MainLegend = ({
109
120
 
110
121
  return (
111
122
  <div
112
- className={classes.legend}
123
+ className={`overflow-x-hidden overflow-y-auto ${!equals(placement, 'bottom') ? 'h-full mt-[15px]' : 'ml-[50px] mr-[40px]'} legend`}
113
124
  data-display-side={!equals(placement, 'bottom')}
114
125
  >
115
- <div
116
- className={classes.items}
126
+ <ul
127
+ 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-17' : 'max-h-0'}`}
128
+ style={{
129
+ height: equals(placement, 'bottom') ? 'auto' : `${graphHeight}px`
130
+ }}
117
131
  data-as-list={isListMode || !equals(placement, 'bottom')}
118
132
  data-mode={itemMode}
119
133
  >
120
134
  {displayedLines.map((line) => {
121
- const { color, display, highlight, metric_id, unit } = line;
135
+ const { color, display, metric_id, unit } = line;
122
136
 
123
137
  const markerColor = display
124
138
  ? color
125
139
  : alpha(theme.palette.text.disabled, 0.2);
126
140
 
127
141
  const minMaxAvg = [
128
- {
142
+ showCalculations.min && {
129
143
  label: labelMin,
130
144
  value: line.minimum_value
131
145
  },
132
- {
146
+ showCalculations.max && {
133
147
  label: labelMax,
134
148
  value: line.maximum_value
135
149
  },
136
- {
150
+ showCalculations.avg && {
137
151
  label: labelAvg,
138
152
  value: line.average_value
139
153
  }
140
- ];
154
+ ].filter(Boolean);
141
155
 
142
156
  return (
143
- <Box
144
- className={cx(
145
- classes.item,
146
- highlight ? classes.highlight : classes.normal,
147
- toggable && classes.toggable
148
- )}
157
+ <li
158
+ className={`${!display ? 'text-text-disabled' : 'text-text-primary'} flex gap-1 ${toggable && 'cursor-pointer'}`}
149
159
  key={metric_id}
150
160
  onClick={(event): void => selectMetric({ event, metric_id })}
161
+ onKeyUp={(event) =>
162
+ event.key === 'Enter' && selectMetric({ event, metric_id })
163
+ }
151
164
  onMouseEnter={(): void => highlightLine(metric_id)}
152
165
  onMouseLeave={(): void => clearHighlight()}
153
166
  onContextMenu={contextMenuClick(metric_id)}
154
167
  >
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}
168
+ <div
169
+ className="h-full rounded-sm w-1 min-h-5"
170
+ style={{ backgroundColor: markerColor }}
171
+ data-icon
165
172
  />
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
- ))}
173
+ <div>
174
+ <LegendHeader
175
+ isDisplayedOnSide={!equals(placement, 'bottom')}
176
+ isListMode={isListMode}
177
+ line={line}
178
+ minMaxAvg={
179
+ shouldDisplayLegendInCompactMode ? minMaxAvg : undefined
180
+ }
181
+ unit={unit}
182
+ />
183
+ {!shouldDisplayLegendInCompactMode && !isListMode && (
184
+ <div>
185
+ <div className="flex flex-wrap gap-1 whitespace-nowrap">
186
+ {minMaxAvg.map(({ label, value }) => (
187
+ <LegendContent
188
+ data={getMetricValue({ unit: line.unit, value })}
189
+ key={label}
190
+ label={label}
191
+ />
192
+ ))}
193
+ </div>
176
194
  </div>
177
- </div>
178
- )}
179
- </Box>
195
+ )}
196
+ </div>
197
+ </li>
180
198
  );
181
199
  })}
182
- </div>
200
+ </ul>
183
201
  {renderExtraComponent}
184
202
  </div>
185
203
  );
186
204
  };
187
205
 
188
- const Legend = (props: Props): JSX.Element => {
206
+ const Legend = (props: Props): ReactElement => {
189
207
  const {
190
208
  toggable,
191
209
  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;
@@ -31,9 +35,10 @@ interface Props {
31
35
  limitLegend?: number | false;
32
36
  lines: Array<Line>;
33
37
  setLines:
34
- | Dispatch<SetStateAction<Array<Line> | null>>
35
- | Dispatch<SetStateAction<Array<Line>>>;
38
+ | Dispatch<SetStateAction<Array<Line> | null>>
39
+ | Dispatch<SetStateAction<Array<Line>>>;
36
40
  title: string;
41
+ graphHeight: number;
37
42
  }
38
43
 
39
44
  const BaseChart = ({
@@ -49,7 +54,8 @@ const BaseChart = ({
49
54
  titleRef,
50
55
  title,
51
56
  header,
52
- isHorizontal = true
57
+ isHorizontal = true,
58
+ graphHeight
53
59
  }: Props): JSX.Element => {
54
60
  const { classes, cx } = useBaseChartStyles();
55
61
 
@@ -87,8 +93,8 @@ const BaseChart = ({
87
93
  className={cx(
88
94
  classes.legendContainer,
89
95
  equals(legend?.placement, 'right') &&
90
- !isHorizontal &&
91
- classes.legendContainerVerticalSide
96
+ !isHorizontal &&
97
+ classes.legendContainerVerticalSide
92
98
  )}
93
99
  ref={legendRef}
94
100
  >
@@ -104,7 +110,9 @@ const BaseChart = ({
104
110
  shouldDisplayLegendInCompactMode={
105
111
  shouldDisplayLegendInCompactMode
106
112
  }
113
+ showCalculations={legend?.showCalculations}
107
114
  secondaryClick={legend?.secondaryClick}
115
+ graphHeight={graphHeight}
108
116
  />
109
117
  </div>
110
118
  )}
@@ -127,6 +135,8 @@ const BaseChart = ({
127
135
  setLinesGraph={setLines}
128
136
  shouldDisplayLegendInCompactMode={shouldDisplayLegendInCompactMode}
129
137
  secondaryClick={legend?.secondaryClick}
138
+ showCalculations={legend?.showCalculations}
139
+ graphHeight={graphHeight}
130
140
  />
131
141
  </div>
132
142
  )}