@centreon/ui 26.11.11 → 26.11.13

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": "26.11.11",
3
+ "version": "26.11.13",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "update:deps": "pnpx npm-check-updates -i --format group",
@@ -22,10 +22,12 @@ const ValueDisplay = (): JSX.Element => {
22
22
 
23
23
  const initialize = ({
24
24
  notificationOptions = [],
25
- disabled = false
25
+ disabled = false,
26
+ variant
26
27
  }: {
27
28
  disabled?: boolean;
28
29
  notificationOptions?: Array<string> | null;
30
+ variant?: 'checkboxes' | 'chips';
29
31
  } = {}): void => {
30
32
  cy.mount({
31
33
  Component: (
@@ -38,7 +40,8 @@ const initialize = ({
38
40
  direction: 'horizontal',
39
41
  exclusiveLabel: 'No notifications',
40
42
  exclusiveOption: 'None',
41
- options
43
+ options,
44
+ variant
42
45
  },
43
46
  fieldName: 'notificationOptions',
44
47
  getDisabled: () => disabled,
@@ -172,3 +175,62 @@ describe('Exclusive checkbox group', () => {
172
175
  expectValue(['Down']);
173
176
  });
174
177
  });
178
+
179
+ describe('Exclusive checkbox group, as chips', () => {
180
+ const chip = (option: string): Cypress.Chainable =>
181
+ cy.findByTestId(`notification-options-${option}`);
182
+
183
+ it('displays the options as chips and the exclusive one as the toggle', () => {
184
+ initialize({ variant: 'chips' });
185
+
186
+ ['Down', 'Unreachable', 'Recovery'].forEach((option) => {
187
+ chip(option).should('have.attr', 'aria-pressed', 'false');
188
+ });
189
+ chip('None').should('not.exist');
190
+ toggle().should('not.be.checked');
191
+ cy.findByRole('checkbox', { name: 'Down' }).should('not.exist');
192
+ });
193
+
194
+ it('toggles a chip on and off', () => {
195
+ initialize({ variant: 'chips' });
196
+
197
+ chip('Down').click();
198
+ chip('Recovery').click();
199
+
200
+ expectValue(['Down', 'Recovery']);
201
+ chip('Down').should('have.attr', 'aria-pressed', 'true');
202
+
203
+ chip('Down').click();
204
+
205
+ expectValue(['Recovery']);
206
+ });
207
+
208
+ it('clears and disables the chips while the toggle is on', () => {
209
+ initialize({ notificationOptions: ['Down', 'Recovery'], variant: 'chips' });
210
+
211
+ toggle().click();
212
+
213
+ expectValue(['None']);
214
+ chip('Down')
215
+ .should('have.attr', 'aria-pressed', 'false')
216
+ .and('have.attr', 'aria-disabled', 'true');
217
+ });
218
+
219
+ it('leaves the chips empty and usable once the toggle is off', () => {
220
+ initialize({ notificationOptions: ['None'], variant: 'chips' });
221
+
222
+ toggle().click();
223
+
224
+ expectValue([]);
225
+ chip('Unreachable').click();
226
+
227
+ expectValue(['Unreachable']);
228
+ });
229
+
230
+ it('disables the chips and the toggle when the input is disabled', () => {
231
+ initialize({ disabled: true, variant: 'chips' });
232
+
233
+ chip('Down').should('have.attr', 'aria-disabled', 'true');
234
+ toggle().should('be.disabled');
235
+ });
236
+ });
@@ -1,8 +1,9 @@
1
- import { Box } from '@mui/material';
1
+ import { Box, Chip } from '@mui/material';
2
2
 
3
3
  import { type FormikValues, useFormikContext } from 'formik';
4
4
  import { equals, includes, path, reject, split } from 'ramda';
5
5
  import type { ChangeEvent } from 'react';
6
+ import { useTranslation } from 'react-i18next';
6
7
 
7
8
  import { useMemoComponent } from '../..';
8
9
  import { CheckboxGroup as CheckboxGroupComponent } from '../../Checkbox';
@@ -19,6 +20,7 @@ const ExclusiveCheckboxGroup = ({
19
20
  getDisabled,
20
21
  dataTestId
21
22
  }: InputPropsWithoutGroup): JSX.Element => {
23
+ const { t } = useTranslation();
22
24
  const { values, setFieldValue } = useFormikContext<FormikValues>();
23
25
 
24
26
  const fieldNamePath = split('.', fieldName);
@@ -53,10 +55,63 @@ const ExclusiveCheckboxGroup = ({
53
55
  );
54
56
  };
55
57
 
58
+ const toggleChip = (option: string) => (): void => {
59
+ setFieldValue(
60
+ fieldName,
61
+ includes(option, value)
62
+ ? reject(equals(option), value)
63
+ : [...value, option]
64
+ );
65
+ };
66
+
56
67
  const baseTestId = dataTestId || fieldName;
57
68
 
69
+ const isChips = equals(exclusiveCheckboxGroup?.variant, 'chips');
70
+
71
+ const exclusiveToggle = (
72
+ <Switch
73
+ change={changeToggle}
74
+ dataTestId={`${baseTestId}-exclusive`}
75
+ fieldName={fieldName}
76
+ getDisabled={() => disabled}
77
+ label={exclusiveCheckboxGroup?.exclusiveLabel as string}
78
+ switchInput={{
79
+ getChecked: (fieldValue) =>
80
+ includes(exclusiveOption, (fieldValue as Array<string>) ?? [])
81
+ }}
82
+ type={InputType.Switch}
83
+ />
84
+ );
85
+
58
86
  return useMemoComponent({
59
- Component: (
87
+ Component: isChips ? (
88
+ <div
89
+ className="flex flex-wrap items-center gap-x-6 gap-y-3"
90
+ data-testid={baseTestId}
91
+ >
92
+ {options.map((option) => {
93
+ const isSelected = !isExclusive && includes(option, value);
94
+
95
+ return (
96
+ <Chip
97
+ aria-pressed={isSelected}
98
+ // The theme sizes chips for status badges, 12px or 20px tall.
99
+ className="h-6 rounded-3xl text-sm [&_.MuiChip-label]:px-3 [&_.MuiChip-label]:leading-[21px]"
100
+ clickable
101
+ color={isSelected ? 'primary' : 'default'}
102
+ data-testid={`${baseTestId}-${option}`}
103
+ disabled={disabled || isExclusive}
104
+ key={option}
105
+ label={t(option)}
106
+ onClick={toggleChip(option)}
107
+ size="small"
108
+ variant={isSelected ? 'filled' : 'outlined'}
109
+ />
110
+ );
111
+ })}
112
+ {exclusiveToggle}
113
+ </div>
114
+ ) : (
60
115
  <Box sx={{ alignItems: 'center', columnGap: 2, display: 'flex' }}>
61
116
  <CheckboxGroupComponent
62
117
  dataTestId={baseTestId}
@@ -67,18 +122,7 @@ const ExclusiveCheckboxGroup = ({
67
122
  options={options}
68
123
  values={isExclusive ? [] : value}
69
124
  />
70
- <Switch
71
- change={changeToggle}
72
- dataTestId={`${baseTestId}-exclusive`}
73
- fieldName={fieldName}
74
- getDisabled={() => disabled}
75
- label={exclusiveCheckboxGroup?.exclusiveLabel as string}
76
- switchInput={{
77
- getChecked: (fieldValue) =>
78
- includes(exclusiveOption, (fieldValue as Array<string>) ?? [])
79
- }}
80
- type={InputType.Switch}
81
- />
125
+ {exclusiveToggle}
82
126
  </Box>
83
127
  ),
84
128
  memoProps: [
@@ -88,7 +132,8 @@ const ExclusiveCheckboxGroup = ({
88
132
  exclusiveOption,
89
133
  exclusiveCheckboxGroup?.exclusiveLabel,
90
134
  exclusiveCheckboxGroup?.direction,
91
- exclusiveCheckboxGroup?.labelPlacement
135
+ exclusiveCheckboxGroup?.labelPlacement,
136
+ exclusiveCheckboxGroup?.variant
92
137
  ]
93
138
  });
94
139
  };
@@ -12,6 +12,10 @@ const getSkeleton = cond<Array<InputType>, JSX.Element>([
12
12
  equals(InputType.Radio) as (b: InputType) => boolean,
13
13
  always(<LoadingSkeleton height={104} />)
14
14
  ],
15
+ [
16
+ equals(InputType.SegmentedButtons) as (b: InputType) => boolean,
17
+ always(<LoadingSkeleton height={56} />)
18
+ ],
15
19
  [
16
20
  equals(InputType.Text) as (b: InputType) => boolean,
17
21
  always(<LoadingSkeleton height={52} />)
@@ -0,0 +1,101 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ import { useFormikContext } from 'formik';
4
+ import { object } from 'yup';
5
+
6
+ import { Form } from '../Form';
7
+ import { InputType } from './models';
8
+
9
+ const options = [
10
+ { label: 'Yes', value: 'true' },
11
+ { label: 'No', value: 'false' },
12
+ { label: 'Default', value: 'use_default' }
13
+ ];
14
+
15
+ const ValueDisplay = (): JSX.Element => {
16
+ const { values } = useFormikContext<{ enabled: string }>();
17
+
18
+ return (
19
+ <Typography data-testid="value">
20
+ {JSON.stringify(values.enabled)}
21
+ </Typography>
22
+ );
23
+ };
24
+
25
+ const initialize = ({
26
+ enabled = 'use_default',
27
+ disabled = false
28
+ }: {
29
+ disabled?: boolean;
30
+ enabled?: string;
31
+ } = {}): void => {
32
+ cy.mount({
33
+ Component: (
34
+ <Form
35
+ initialValues={{ enabled }}
36
+ inputs={[
37
+ {
38
+ dataTestId: 'enabled',
39
+ fieldName: 'enabled',
40
+ getDisabled: () => disabled,
41
+ group: '',
42
+ label: 'Notification Enabled',
43
+ segmentedButtons: { options },
44
+ type: InputType.SegmentedButtons
45
+ },
46
+ {
47
+ custom: { Component: ValueDisplay },
48
+ fieldName: 'value',
49
+ group: '',
50
+ label: 'value',
51
+ type: InputType.Custom
52
+ }
53
+ ]}
54
+ submit={cy.stub()}
55
+ validationSchema={object()}
56
+ />
57
+ )
58
+ });
59
+ };
60
+
61
+ const button = (label: string): Cypress.Chainable =>
62
+ cy.findByRole('button', { name: label });
63
+
64
+ describe('Segmented buttons', () => {
65
+ it('displays every option and marks the current one', () => {
66
+ initialize();
67
+
68
+ cy.contains('Notification Enabled').should('be.visible');
69
+ button('Yes').should('have.attr', 'aria-pressed', 'false');
70
+ button('No').should('have.attr', 'aria-pressed', 'false');
71
+ button('Default').should('have.attr', 'aria-pressed', 'true');
72
+ });
73
+
74
+ it('stores the value of the clicked option as given', () => {
75
+ initialize();
76
+
77
+ button('Yes').click();
78
+
79
+ // A string, not the boolean the radio would store.
80
+ cy.findByTestId('value').should('have.text', '"true"');
81
+ button('Yes').should('have.attr', 'aria-pressed', 'true');
82
+ button('Default').should('have.attr', 'aria-pressed', 'false');
83
+
84
+ button('No').click();
85
+
86
+ cy.findByTestId('value').should('have.text', '"false"');
87
+ });
88
+
89
+ it('starts on the initial value', () => {
90
+ initialize({ enabled: 'false' });
91
+
92
+ button('No').should('have.attr', 'aria-pressed', 'true');
93
+ });
94
+
95
+ it('does not change when disabled', () => {
96
+ initialize({ disabled: true });
97
+
98
+ button('Yes').should('be.disabled');
99
+ cy.findByTestId('value').should('have.text', '"use_default"');
100
+ });
101
+ });
@@ -0,0 +1,71 @@
1
+ import { Button, ButtonGroup, FormGroup, FormLabel } from '@mui/material';
2
+
3
+ import { type FormikValues, useFormikContext } from 'formik';
4
+ import { equals, path, split } from 'ramda';
5
+ import { useTranslation } from 'react-i18next';
6
+
7
+ import { useMemoComponent } from '../..';
8
+ import type { InputPropsWithoutGroup } from './models';
9
+
10
+ const SegmentedButtons = ({
11
+ dataTestId,
12
+ fieldName,
13
+ label,
14
+ segmentedButtons,
15
+ getDisabled,
16
+ additionalMemoProps
17
+ }: InputPropsWithoutGroup): JSX.Element => {
18
+ const { t } = useTranslation();
19
+
20
+ const { values, setFieldValue, setFieldTouched } =
21
+ useFormikContext<FormikValues>();
22
+
23
+ const value = path(split('.', fieldName), values);
24
+
25
+ const disabled = getDisabled?.(values) || false;
26
+
27
+ const select = (optionValue: string) => (): void => {
28
+ setFieldTouched(fieldName, true, false);
29
+ setFieldValue(fieldName, optionValue);
30
+ };
31
+
32
+ return useMemoComponent({
33
+ Component: (
34
+ <FormGroup>
35
+ <FormLabel>{t(label)}</FormLabel>
36
+ <ButtonGroup
37
+ aria-label={t(label)}
38
+ // As wide as its column, up to what three labels need.
39
+ className="max-w-[412px]"
40
+ color="primary"
41
+ data-testid={dataTestId}
42
+ disabled={disabled}
43
+ fullWidth
44
+ size="small"
45
+ >
46
+ {segmentedButtons?.options.map(
47
+ ({ label: optionLabel, value: optionValue }) => {
48
+ const isSelected = equals(value, optionValue);
49
+
50
+ return (
51
+ <Button
52
+ aria-pressed={isSelected}
53
+ className="h-7 px-3 py-0 font-medium text-sm leading-[21px] normal-case"
54
+ data-testid={`${dataTestId}-${optionValue}`}
55
+ key={optionValue}
56
+ onClick={select(optionValue)}
57
+ variant={isSelected ? 'contained' : 'outlined'}
58
+ >
59
+ {t(optionLabel)}
60
+ </Button>
61
+ );
62
+ }
63
+ )}
64
+ </ButtonGroup>
65
+ </FormGroup>
66
+ ),
67
+ memoProps: [value, disabled, segmentedButtons, additionalMemoProps]
68
+ });
69
+ };
70
+
71
+ export default SegmentedButtons;
@@ -48,6 +48,7 @@ import {
48
48
  InputType
49
49
  } from './models';
50
50
  import RadioInput from './Radio';
51
+ import SegmentedButtons from './SegmentedButtons';
51
52
  import { SubgroupDivider } from './SubGroupDivider';
52
53
  import SwitchInput from './Switch';
53
54
  import TextInput from './Text';
@@ -89,6 +90,10 @@ export const getInput = cond<
89
90
  equals(InputType.ExclusiveCheckboxGroup) as (b: InputType) => boolean,
90
91
  always(ExclusiveCheckboxGroup)
91
92
  ],
93
+ [
94
+ equals(InputType.SegmentedButtons) as (b: InputType) => boolean,
95
+ always(SegmentedButtons)
96
+ ],
92
97
  [equals(InputType.List) as (b: InputType) => boolean, always(List)],
93
98
  [equals(InputType.File) as (b: InputType) => boolean, always(File)],
94
99
  [equals(InputType.Text) as (b: InputType) => boolean, always(TextInput)],
@@ -51,7 +51,8 @@ export enum InputType {
51
51
  List = 13,
52
52
  File = 14,
53
53
  Divider = 15,
54
- ExclusiveCheckboxGroup = 16
54
+ ExclusiveCheckboxGroup = 16,
55
+ SegmentedButtons = 17
55
56
  }
56
57
 
57
58
  interface FieldsTableGetRequiredProps {
@@ -111,6 +112,8 @@ export interface InputProps {
111
112
  exclusiveOption: string;
112
113
  labelPlacement?: LabelPlacement;
113
114
  options: Array<string>;
115
+ // The options as chips instead of checkboxes; the exclusive one stays a switch.
116
+ variant?: 'checkboxes' | 'chips';
114
117
  };
115
118
  file?: {
116
119
  multiple?: boolean;
@@ -167,6 +170,12 @@ export interface InputProps {
167
170
  row?: boolean;
168
171
  };
169
172
  required?: boolean;
173
+ segmentedButtons?: {
174
+ options: Array<{
175
+ label: string;
176
+ value: string;
177
+ }>;
178
+ };
170
179
  switchInput?: {
171
180
  getChecked?: (value: unknown) => boolean;
172
181
  };
@@ -3,6 +3,7 @@ import { userAtom } from '@centreon/ui-context';
3
3
  import { renderHook } from '@testing-library/react';
4
4
  import dayjs from 'dayjs';
5
5
  import { useAtomValue } from 'jotai';
6
+ import { equals, update } from 'ramda';
6
7
 
7
8
  import { labelAvg, labelMax, labelMin } from '../Chart/translatedLabels';
8
9
  import dataMissingPoint from '../mockedData/dataWithMissingPoint.json';
@@ -19,6 +20,29 @@ const defaultStart = new Date(
19
20
 
20
21
  const defaultEnd = new Date(Date.now()).toISOString();
21
22
 
23
+ const hoveredStackedBarIndex = 6;
24
+
25
+ const dataPingServiceStackedWithUndefinedValues = {
26
+ ...dataPingServiceStacked,
27
+ metrics: dataPingServiceStacked.metrics.map((metric) => {
28
+ if (equals(metric.metric_id, 3)) {
29
+ return {
30
+ ...metric,
31
+ data: update(hoveredStackedBarIndex, null, metric.data)
32
+ };
33
+ }
34
+
35
+ if (equals(metric.metric_id, 4)) {
36
+ return {
37
+ ...metric,
38
+ data: update(hoveredStackedBarIndex, 0, metric.data)
39
+ };
40
+ }
41
+
42
+ return metric;
43
+ })
44
+ };
45
+
22
46
  const defaultArgs = {
23
47
  end: defaultEnd,
24
48
  height: 500,
@@ -229,6 +253,63 @@ describe('Bar chart', () => {
229
253
  });
230
254
  });
231
255
 
256
+ it('displays only metrics with a defined value in the tooltip when the mode is set to defined', () => {
257
+ initialize({
258
+ data: dataPingServiceStackedWithUndefinedValues,
259
+ orientation: 'horizontal',
260
+ tooltip: {
261
+ mode: 'defined',
262
+ sortOrder: 'name'
263
+ }
264
+ });
265
+
266
+ checkWidth();
267
+ cy.contains('0 ms').should('be.visible');
268
+ cy.contains(':40 AM').should('be.visible');
269
+
270
+ cy.get('body').realMouseMove(0, 0);
271
+ cy.findByTestId('stacked-bar-1-0-0.05296').realHover();
272
+
273
+ cy.contains('06/19/2024').should('be.visible');
274
+
275
+ cy.get('[data-metric="Centreon-Server: Round-Trip Average Time"]').should(
276
+ 'be.visible'
277
+ );
278
+ cy.get('[data-metric="Centreon-Server: Round-Trip Maximum Time"]').should(
279
+ 'not.exist'
280
+ );
281
+ cy.get('[data-metric="Centreon-Server: Round-Trip Minimum Time"]').should(
282
+ 'not.exist'
283
+ );
284
+ });
285
+
286
+ it('displays metrics with an undefined value in the tooltip when the mode is set to all', () => {
287
+ initialize({
288
+ data: dataPingServiceStackedWithUndefinedValues,
289
+ orientation: 'horizontal',
290
+ tooltip: {
291
+ mode: 'all',
292
+ sortOrder: 'name'
293
+ }
294
+ });
295
+
296
+ checkWidth();
297
+ cy.contains('0 ms').should('be.visible');
298
+ cy.contains(':40 AM').should('be.visible');
299
+
300
+ cy.get('body').realMouseMove(0, 0);
301
+ cy.findByTestId('stacked-bar-1-0-0.05296').realHover();
302
+
303
+ cy.contains('06/19/2024').should('be.visible');
304
+
305
+ cy.get('[data-metric="Centreon-Server: Round-Trip Maximum Time"]').should(
306
+ 'be.visible'
307
+ );
308
+ cy.get('[data-metric="Centreon-Server: Round-Trip Minimum Time"]').should(
309
+ 'be.visible'
310
+ );
311
+ });
312
+
232
313
  it('does not display a tooltip when a bar is hovered and a props is set', () => {
233
314
  initialize({
234
315
  data: dataPingServiceStacked,
@@ -5,7 +5,6 @@ import {
5
5
  always,
6
6
  cond,
7
7
  equals,
8
- filter,
9
8
  gt,
10
9
  has,
11
10
  isNil,
@@ -20,6 +19,7 @@ import { useLocaleDateTimeFormat } from '../../../utils';
20
19
  import type { Tooltip } from '../../Chart/models';
21
20
  import { formatMetricValueWithUnit } from '../../common/timeSeries';
22
21
  import type { TimeValue } from '../../common/timeSeries/models';
22
+ import { isMetricDisplayedInTooltip } from '../../common/utils';
23
23
  import { tooltipDataAtom } from '../atoms';
24
24
  import { useBarChartTooltipStyles } from './useBarChartTooltipStyles';
25
25
 
@@ -49,14 +49,13 @@ const BarChartTooltip = ({
49
49
  const date = timeSeries[tooltipData.index].timeTick;
50
50
  const formattedDateTime = format({ date, formatString: 'L LTS' });
51
51
 
52
- const isSingleMode = equals(mode, 'single');
53
-
54
- const filteredMetrics = isSingleMode
55
- ? filter(
56
- ({ metric }) => equals(metric.metric_id, tooltipData.highlightedMetric),
57
- tooltipData.data
58
- )
59
- : tooltipData.data;
52
+ const filteredMetrics = tooltipData.data.filter(({ metric, value }) =>
53
+ isMetricDisplayedInTooltip({
54
+ isHighlighted: equals(metric.metric_id, tooltipData.highlightedMetric),
55
+ mode,
56
+ value
57
+ })
58
+ );
60
59
 
61
60
  const displayHighLightedMetric = gt(filteredMetrics.length, 1);
62
61
 
@@ -1,6 +1,7 @@
1
1
  import { userAtom } from '@centreon/ui-context';
2
2
 
3
3
  import { createStore, Provider } from 'jotai';
4
+ import { always, equals } from 'ramda';
4
5
  import { useState } from 'react';
5
6
 
6
7
  import type { LineChartData } from '../common/models';
@@ -17,6 +18,15 @@ import { args as argumentsData } from './helpers/doc';
17
18
  import type { LineChartProps } from './models';
18
19
  import { labelAvg, labelMin } from './translatedLabels';
19
20
 
21
+ const dataLastDayWithZeroConnTime = {
22
+ ...dataLastDay,
23
+ metrics: dataLastDay.metrics.map((metric) =>
24
+ equals(metric.metric, 'connTime')
25
+ ? { ...metric, data: metric.data.map(always(0)) }
26
+ : metric
27
+ )
28
+ };
29
+
20
30
  interface Props
21
31
  extends Pick<
22
32
  LineChartProps,
@@ -281,6 +291,38 @@ describe('Line chart', () => {
281
291
 
282
292
  cy.makeSnapshot();
283
293
  });
294
+
295
+ it('displays only metrics with a defined value when the mode is set to defined', () => {
296
+ initialize({
297
+ data: dataLastDayWithZeroConnTime,
298
+ tooltip: { mode: 'defined', sortOrder: 'name' }
299
+ });
300
+
301
+ checkGraphWidth();
302
+
303
+ cy.contains('Min: 70.31').should('be.visible');
304
+
305
+ cy.findByTestId('graph-interaction-zone').realMouseMove(452, 26);
306
+
307
+ cy.get('[data-metric="querytime"]').should('be.visible');
308
+ cy.get('[data-metric="connTime"]').should('not.exist');
309
+ });
310
+
311
+ it('displays metrics with a zero value when the mode is set to all', () => {
312
+ initialize({
313
+ data: dataLastDayWithZeroConnTime,
314
+ tooltip: { mode: 'all', sortOrder: 'name' }
315
+ });
316
+
317
+ checkGraphWidth();
318
+
319
+ cy.contains('Min: 70.31').should('be.visible');
320
+
321
+ cy.findByTestId('graph-interaction-zone').realMouseMove(452, 26);
322
+
323
+ cy.get('[data-metric="querytime"]').should('be.visible');
324
+ cy.get('[data-metric="connTime"]').should('be.visible');
325
+ });
284
326
  });
285
327
 
286
328
  it('displays the curves with different shades when curves have same color', () => {
@@ -48,7 +48,7 @@ const GraphValueTooltip = ({
48
48
  equals('hidden', tooltip?.mode) ? null : (
49
49
  <GraphValueTooltipContent
50
50
  base={baseAxis}
51
- isSingleMode={equals('single', tooltip?.mode)}
51
+ mode={tooltip?.mode}
52
52
  // @ts-expect-error - suppressing pre-existing type mismatch
53
53
  sortOrder={tooltip?.sortOrder}
54
54
  />
@@ -9,20 +9,21 @@ import { mousePositionAtom } from '../interactionWithGraphAtoms';
9
9
  import { useGraphValueTooltip } from './useGraphValueTooltip';
10
10
  import { useGraphValueTooltipStyles } from './useGraphValueTooltipStyles';
11
11
 
12
- interface Props extends Pick<Tooltip, 'sortOrder'> {
12
+ interface Props
13
+ extends Pick<Tooltip, 'sortOrder'>,
14
+ Partial<Pick<Tooltip, 'mode'>> {
13
15
  base: number;
14
- isSingleMode: boolean;
15
16
  }
16
17
 
17
18
  const GraphValueTooltipContent = ({
18
19
  base,
19
- isSingleMode,
20
+ mode,
20
21
  sortOrder
21
22
  }: Props): JSX.Element | null => {
22
23
  const { classes } = useGraphValueTooltipStyles();
23
24
  const mousePosition = useAtomValue(mousePositionAtom);
24
25
 
25
- const graphValue = useGraphValueTooltip({ isSingleMode, sortOrder });
26
+ const graphValue = useGraphValueTooltip({ mode, sortOrder });
26
27
 
27
28
  if (isNil(graphValue) || isNil(mousePosition)) {
28
29
  return null;
@@ -1,17 +1,8 @@
1
1
  import { useAtomValue } from 'jotai';
2
- import {
3
- always,
4
- cond,
5
- equals,
6
- filter,
7
- isNil,
8
- prop,
9
- reverse,
10
- sortBy,
11
- T
12
- } from 'ramda';
2
+ import { always, cond, equals, isNil, prop, reverse, sortBy, T } from 'ramda';
13
3
 
14
4
  import { useLocaleDateTimeFormat } from '../../../../utils';
5
+ import { isMetricDisplayedInTooltip } from '../../../common/utils';
15
6
  import type { GraphTooltipData, Tooltip } from '../../models';
16
7
  import { graphTooltipDataAtom } from '../interactionWithGraphAtoms';
17
8
 
@@ -19,12 +10,12 @@ interface UseGraphValueTooltipState extends Omit<GraphTooltipData, 'date'> {
19
10
  dateTime: string;
20
11
  }
21
12
 
22
- interface UseGraphValueTooltipProps extends Pick<Tooltip, 'sortOrder'> {
23
- isSingleMode: boolean;
24
- }
13
+ interface UseGraphValueTooltipProps
14
+ extends Pick<Tooltip, 'sortOrder'>,
15
+ Partial<Pick<Tooltip, 'mode'>> {}
25
16
 
26
17
  export const useGraphValueTooltip = ({
27
- isSingleMode,
18
+ mode,
28
19
  sortOrder
29
20
  }: UseGraphValueTooltipProps): UseGraphValueTooltipState | null => {
30
21
  const { format } = useLocaleDateTimeFormat();
@@ -34,12 +25,13 @@ export const useGraphValueTooltip = ({
34
25
  return null;
35
26
  }
36
27
 
37
- const filteredMetrics = isSingleMode
38
- ? filter(
39
- ({ id }) => equals(id, graphTooltipData.highlightedMetricId),
40
- graphTooltipData.metrics
41
- )
42
- : graphTooltipData.metrics;
28
+ const filteredMetrics = graphTooltipData.metrics.filter(({ id, value }) =>
29
+ isMetricDisplayedInTooltip({
30
+ isHighlighted: equals(id, graphTooltipData.highlightedMetricId),
31
+ mode,
32
+ value
33
+ })
34
+ );
43
35
 
44
36
  const sortedMetrics = cond([
45
37
  [equals('name'), always(sortBy(prop('name'), filteredMetrics))],
@@ -74,7 +74,7 @@ export interface ThresholdTooltip {
74
74
  thresholdLabel?: string;
75
75
  }
76
76
  export interface Tooltip {
77
- mode: 'all' | 'single' | 'hidden';
77
+ mode: 'all' | 'single' | 'hidden' | 'defined';
78
78
  renderComponent?: (args: TooltipData) => ReactNode;
79
79
  sortOrder: 'name' | 'ascending' | 'descending';
80
80
  }
@@ -22,7 +22,7 @@ import {
22
22
  } from 'ramda';
23
23
 
24
24
  import type { BarStyle } from '../BarChart/models';
25
- import type { LineStyle } from '../Chart/models';
25
+ import type { LineStyle, Tooltip } from '../Chart/models';
26
26
  import type { Threshold, Thresholds } from './models';
27
27
  import { formatMetricValueWithUnit } from './timeSeries';
28
28
  import type { Line, TimeValue } from './timeSeries/models';
@@ -278,3 +278,25 @@ export const computPixelsToShiftMouse = (
278
278
 
279
279
  return Math.round(8 / hoursDiffInGraph);
280
280
  };
281
+
282
+ interface IsMetricDisplayedInTooltipProps
283
+ extends Partial<Pick<Tooltip, 'mode'>> {
284
+ isHighlighted: boolean;
285
+ value?: number | null;
286
+ }
287
+
288
+ export const isMetricDisplayedInTooltip = ({
289
+ mode,
290
+ isHighlighted,
291
+ value
292
+ }: IsMetricDisplayedInTooltipProps): boolean => {
293
+ if (equals(mode, 'single')) {
294
+ return isHighlighted;
295
+ }
296
+
297
+ if (equals(mode, 'defined')) {
298
+ return Boolean(value);
299
+ }
300
+
301
+ return true;
302
+ };