@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 +1 -1
- package/src/Form/Inputs/ExclusiveCheckboxGroup.cypress.spec.tsx +64 -2
- package/src/Form/Inputs/ExclusiveCheckboxGroup.tsx +60 -15
- package/src/Form/Inputs/LoadingSkeleton.tsx +4 -0
- package/src/Form/Inputs/SegmentedButtons.cypress.spec.tsx +101 -0
- package/src/Form/Inputs/SegmentedButtons.tsx +71 -0
- package/src/Form/Inputs/index.tsx +5 -0
- package/src/Form/Inputs/models.ts +10 -1
- package/src/Graph/BarChart/BarChart.cypress.spec.tsx +81 -0
- package/src/Graph/BarChart/Tooltip/BarChartTooltip.tsx +8 -9
- package/src/Graph/Chart/Chart.cypress.spec.tsx +42 -0
- package/src/Graph/Chart/InteractiveComponents/GraphValueTooltip/GraphValueTooltip.tsx +1 -1
- package/src/Graph/Chart/InteractiveComponents/GraphValueTooltip/GraphValueTooltipContent.tsx +5 -4
- package/src/Graph/Chart/InteractiveComponents/GraphValueTooltip/useGraphValueTooltip.ts +13 -21
- package/src/Graph/Chart/models.ts +1 -1
- package/src/Graph/common/utils.ts +23 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
51
|
+
mode={tooltip?.mode}
|
|
52
52
|
// @ts-expect-error - suppressing pre-existing type mismatch
|
|
53
53
|
sortOrder={tooltip?.sortOrder}
|
|
54
54
|
/>
|
package/src/Graph/Chart/InteractiveComponents/GraphValueTooltip/GraphValueTooltipContent.tsx
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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({
|
|
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
|
|
23
|
-
|
|
24
|
-
}
|
|
13
|
+
interface UseGraphValueTooltipProps
|
|
14
|
+
extends Pick<Tooltip, 'sortOrder'>,
|
|
15
|
+
Partial<Pick<Tooltip, 'mode'>> {}
|
|
25
16
|
|
|
26
17
|
export const useGraphValueTooltip = ({
|
|
27
|
-
|
|
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 =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
+
};
|