@scality/core-ui 0.164.0 → 0.166.0
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/dist/components/barchartv2/Barchart.component.d.ts +20 -1
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +33 -5
- package/dist/components/barchartv2/ChartTooltip.d.ts +18 -0
- package/dist/components/barchartv2/ChartTooltip.d.ts.map +1 -0
- package/dist/components/barchartv2/ChartTooltip.js +31 -0
- package/dist/components/barchartv2/utils.d.ts.map +1 -1
- package/dist/components/barchartv2/utils.js +1 -28
- package/dist/components/chartlegend/ChartLegend.d.ts +9 -0
- package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -1
- package/dist/components/chartlegend/ChartLegend.js +38 -9
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts +4 -0
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -1
- package/dist/components/chartlegend/ChartLegendWrapper.js +23 -2
- package/dist/components/constants.d.ts +2 -0
- package/dist/components/constants.d.ts.map +1 -1
- package/dist/components/constants.js +6 -0
- package/dist/components/constrainedtext/Constrainedtext.component.d.ts +3 -1
- package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
- package/dist/components/constrainedtext/Constrainedtext.component.js +2 -2
- package/dist/components/date/FormattedDateTime.d.ts +2 -1
- package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.js +10 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +3 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +5 -7
- package/dist/components/text/Text.component.js +1 -1
- package/dist/components/toast/Toast.component.d.ts.map +1 -1
- package/dist/components/toast/Toast.component.js +24 -11
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/next.d.ts +2 -0
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +2 -0
- package/package.json +1 -2
- package/src/lib/components/barchartv2/Barchart.component.test.tsx +100 -4
- package/src/lib/components/barchartv2/Barchart.component.tsx +46 -11
- package/src/lib/components/barchartv2/ChartTooltip.tsx +76 -0
- package/src/lib/components/barchartv2/utils.test.ts +72 -45
- package/src/lib/components/barchartv2/utils.ts +2 -32
- package/src/lib/components/chartlegend/ChartLegend.test.tsx +218 -0
- package/src/lib/components/chartlegend/ChartLegend.tsx +42 -8
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +75 -29
- package/src/lib/components/constants.ts +11 -0
- package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +5 -2
- package/src/lib/components/date/FormattedDateTime.tsx +15 -1
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +8 -6
- package/src/lib/components/text/Text.component.tsx +1 -1
- package/src/lib/components/toast/Toast.component.tsx +27 -19
- package/src/lib/index.ts +0 -1
- package/src/lib/next.ts +2 -0
- package/stories/constrainedtext.stories.tsx +4 -1
- package/stories/linetimeseriechart.stories.tsx +30 -25
|
@@ -13,6 +13,21 @@ import {
|
|
|
13
13
|
UnitRange,
|
|
14
14
|
} from './utils';
|
|
15
15
|
|
|
16
|
+
// Test date constants to avoid repetition
|
|
17
|
+
const TEST_DATES = {
|
|
18
|
+
JULY_5_2024: new Date('2024-07-05T00:00:00'),
|
|
19
|
+
JULY_6_2024: new Date('2024-07-06T00:00:00'),
|
|
20
|
+
JULY_7_2024: new Date('2024-07-07T00:00:00'),
|
|
21
|
+
JULY_5_10AM: new Date('2024-07-05T10:00:00'),
|
|
22
|
+
JULY_5_11AM: new Date('2024-07-05T11:00:00'),
|
|
23
|
+
JULY_5_8_30AM: new Date('2024-07-05T08:30:00'),
|
|
24
|
+
JULY_5_2_45PM: new Date('2024-07-05T14:45:00'),
|
|
25
|
+
JULY_6_9_15AM: new Date('2024-07-06T09:15:00'),
|
|
26
|
+
JULY_5_8AM: new Date('2024-07-05T08:00:00'),
|
|
27
|
+
JULY_6_2PM: new Date('2024-07-06T14:00:00'),
|
|
28
|
+
JULY_7_10AM: new Date('2024-07-07T10:00:00'),
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
16
31
|
// Mock theme object for tests
|
|
17
32
|
const mockTheme = {
|
|
18
33
|
statusHealthy: '#00D100',
|
|
@@ -46,8 +61,8 @@ describe('transformTimeData', () => {
|
|
|
46
61
|
{
|
|
47
62
|
label: 'Success',
|
|
48
63
|
data: [
|
|
49
|
-
[
|
|
50
|
-
[
|
|
64
|
+
[TEST_DATES.JULY_5_2024, 10],
|
|
65
|
+
[TEST_DATES.JULY_6_2024, 20],
|
|
51
66
|
] as [Date, number][],
|
|
52
67
|
},
|
|
53
68
|
];
|
|
@@ -55,8 +70,8 @@ describe('transformTimeData', () => {
|
|
|
55
70
|
const type = {
|
|
56
71
|
type: 'time' as const,
|
|
57
72
|
timeRange: {
|
|
58
|
-
startDate:
|
|
59
|
-
endDate:
|
|
73
|
+
startDate: TEST_DATES.JULY_5_2024,
|
|
74
|
+
endDate: TEST_DATES.JULY_6_2024,
|
|
60
75
|
interval: 24 * 60 * 60 * 1000, // 1 day
|
|
61
76
|
},
|
|
62
77
|
};
|
|
@@ -66,8 +81,8 @@ describe('transformTimeData', () => {
|
|
|
66
81
|
const result = transformTimeData(bars, type, barDataKeys);
|
|
67
82
|
|
|
68
83
|
expect(result).toEqual([
|
|
69
|
-
{ category:
|
|
70
|
-
{ category:
|
|
84
|
+
{ category: TEST_DATES.JULY_5_2024.valueOf(), Success: 10 },
|
|
85
|
+
{ category: TEST_DATES.JULY_6_2024.valueOf(), Success: 20 },
|
|
71
86
|
]);
|
|
72
87
|
});
|
|
73
88
|
|
|
@@ -76,9 +91,9 @@ describe('transformTimeData', () => {
|
|
|
76
91
|
{
|
|
77
92
|
label: 'Success',
|
|
78
93
|
data: [
|
|
79
|
-
[
|
|
94
|
+
[TEST_DATES.JULY_5_2024, 10],
|
|
80
95
|
// Missing July 6th
|
|
81
|
-
[
|
|
96
|
+
[TEST_DATES.JULY_7_2024, 30],
|
|
82
97
|
] as [Date, number][],
|
|
83
98
|
},
|
|
84
99
|
];
|
|
@@ -86,8 +101,8 @@ describe('transformTimeData', () => {
|
|
|
86
101
|
const type = {
|
|
87
102
|
type: 'time' as const,
|
|
88
103
|
timeRange: {
|
|
89
|
-
startDate:
|
|
90
|
-
endDate:
|
|
104
|
+
startDate: TEST_DATES.JULY_5_2024,
|
|
105
|
+
endDate: TEST_DATES.JULY_7_2024,
|
|
91
106
|
interval: 24 * 60 * 60 * 1000, // 1 day
|
|
92
107
|
},
|
|
93
108
|
};
|
|
@@ -97,9 +112,9 @@ describe('transformTimeData', () => {
|
|
|
97
112
|
const result = transformTimeData(bars, type, barDataKeys);
|
|
98
113
|
|
|
99
114
|
expect(result).toEqual([
|
|
100
|
-
{ category:
|
|
101
|
-
{ category:
|
|
102
|
-
{ category:
|
|
115
|
+
{ category: TEST_DATES.JULY_5_2024.valueOf(), Success: 10 },
|
|
116
|
+
{ category: TEST_DATES.JULY_6_2024.valueOf(), Success: 0 }, // Missing data filled with 0
|
|
117
|
+
{ category: TEST_DATES.JULY_7_2024.valueOf(), Success: 30 },
|
|
103
118
|
]);
|
|
104
119
|
});
|
|
105
120
|
|
|
@@ -108,8 +123,8 @@ describe('transformTimeData', () => {
|
|
|
108
123
|
{
|
|
109
124
|
label: 'Success',
|
|
110
125
|
data: [
|
|
111
|
-
[
|
|
112
|
-
[
|
|
126
|
+
[TEST_DATES.JULY_5_10AM, 10],
|
|
127
|
+
[TEST_DATES.JULY_5_11AM, 20],
|
|
113
128
|
] as [Date, number][],
|
|
114
129
|
},
|
|
115
130
|
];
|
|
@@ -117,8 +132,8 @@ describe('transformTimeData', () => {
|
|
|
117
132
|
const type = {
|
|
118
133
|
type: 'time' as const,
|
|
119
134
|
timeRange: {
|
|
120
|
-
startDate:
|
|
121
|
-
endDate:
|
|
135
|
+
startDate: TEST_DATES.JULY_5_10AM,
|
|
136
|
+
endDate: TEST_DATES.JULY_5_11AM,
|
|
122
137
|
interval: 60 * 60 * 1000, // 1 hour
|
|
123
138
|
},
|
|
124
139
|
};
|
|
@@ -128,8 +143,8 @@ describe('transformTimeData', () => {
|
|
|
128
143
|
const result = transformTimeData(bars, type, barDataKeys);
|
|
129
144
|
|
|
130
145
|
expect(result).toEqual([
|
|
131
|
-
{ category:
|
|
132
|
-
{ category:
|
|
146
|
+
{ category: TEST_DATES.JULY_5_10AM.valueOf(), Success: 10 },
|
|
147
|
+
{ category: TEST_DATES.JULY_5_11AM.valueOf(), Success: 20 },
|
|
133
148
|
]);
|
|
134
149
|
});
|
|
135
150
|
|
|
@@ -138,9 +153,9 @@ describe('transformTimeData', () => {
|
|
|
138
153
|
{
|
|
139
154
|
label: 'Success',
|
|
140
155
|
data: [
|
|
141
|
-
[
|
|
142
|
-
[
|
|
143
|
-
[
|
|
156
|
+
[TEST_DATES.JULY_5_8_30AM, 10], // 8:30 AM
|
|
157
|
+
[TEST_DATES.JULY_5_2_45PM, 25], // 2:45 PM (should overwrite 8:30 AM)
|
|
158
|
+
[TEST_DATES.JULY_6_9_15AM, 15], // Next day
|
|
144
159
|
] as [Date, number][],
|
|
145
160
|
},
|
|
146
161
|
];
|
|
@@ -148,8 +163,8 @@ describe('transformTimeData', () => {
|
|
|
148
163
|
const type = {
|
|
149
164
|
type: 'time' as const,
|
|
150
165
|
timeRange: {
|
|
151
|
-
startDate:
|
|
152
|
-
endDate:
|
|
166
|
+
startDate: TEST_DATES.JULY_5_2024,
|
|
167
|
+
endDate: TEST_DATES.JULY_6_2024,
|
|
153
168
|
interval: 24 * 60 * 60 * 1000, // 1 day
|
|
154
169
|
},
|
|
155
170
|
};
|
|
@@ -159,8 +174,8 @@ describe('transformTimeData', () => {
|
|
|
159
174
|
const result = transformTimeData(bars, type, barDataKeys);
|
|
160
175
|
|
|
161
176
|
expect(result).toEqual([
|
|
162
|
-
{ category:
|
|
163
|
-
{ category:
|
|
177
|
+
{ category: TEST_DATES.JULY_5_2024.valueOf(), Success: 25 }, // Last value for July 5th
|
|
178
|
+
{ category: TEST_DATES.JULY_6_2024.valueOf(), Success: 15 }, // July 6th value
|
|
164
179
|
]);
|
|
165
180
|
});
|
|
166
181
|
|
|
@@ -169,9 +184,9 @@ describe('transformTimeData', () => {
|
|
|
169
184
|
{
|
|
170
185
|
label: 'Success',
|
|
171
186
|
data: [
|
|
172
|
-
[
|
|
173
|
-
[
|
|
174
|
-
[
|
|
187
|
+
[TEST_DATES.JULY_7_10AM, 30], // July 7th (latest)
|
|
188
|
+
[TEST_DATES.JULY_5_8AM, 10], // July 5th (earliest)
|
|
189
|
+
[TEST_DATES.JULY_6_2PM, 20], // July 6th (middle)
|
|
175
190
|
] as [Date, number][],
|
|
176
191
|
},
|
|
177
192
|
];
|
|
@@ -179,8 +194,8 @@ describe('transformTimeData', () => {
|
|
|
179
194
|
const type = {
|
|
180
195
|
type: 'time' as const,
|
|
181
196
|
timeRange: {
|
|
182
|
-
startDate:
|
|
183
|
-
endDate:
|
|
197
|
+
startDate: TEST_DATES.JULY_5_2024,
|
|
198
|
+
endDate: TEST_DATES.JULY_7_2024,
|
|
184
199
|
interval: 24 * 60 * 60 * 1000, // 1 day
|
|
185
200
|
},
|
|
186
201
|
};
|
|
@@ -191,9 +206,9 @@ describe('transformTimeData', () => {
|
|
|
191
206
|
|
|
192
207
|
// Should be in chronological order regardless of input order
|
|
193
208
|
expect(result).toEqual([
|
|
194
|
-
{ category:
|
|
195
|
-
{ category:
|
|
196
|
-
{ category:
|
|
209
|
+
{ category: TEST_DATES.JULY_5_2024.valueOf(), Success: 10 },
|
|
210
|
+
{ category: TEST_DATES.JULY_6_2024.valueOf(), Success: 20 },
|
|
211
|
+
{ category: TEST_DATES.JULY_7_2024.valueOf(), Success: 30 },
|
|
197
212
|
]);
|
|
198
213
|
});
|
|
199
214
|
|
|
@@ -201,19 +216,19 @@ describe('transformTimeData', () => {
|
|
|
201
216
|
const bars = [
|
|
202
217
|
{
|
|
203
218
|
label: 'Success',
|
|
204
|
-
data: [[
|
|
219
|
+
data: [[TEST_DATES.JULY_5_2024, 10]] as [Date, number][],
|
|
205
220
|
},
|
|
206
221
|
{
|
|
207
222
|
label: 'Failed',
|
|
208
|
-
data: [[
|
|
223
|
+
data: [[TEST_DATES.JULY_6_2024, 5]] as [Date, number][],
|
|
209
224
|
},
|
|
210
225
|
];
|
|
211
226
|
|
|
212
227
|
const type = {
|
|
213
228
|
type: 'time' as const,
|
|
214
229
|
timeRange: {
|
|
215
|
-
startDate:
|
|
216
|
-
endDate:
|
|
230
|
+
startDate: TEST_DATES.JULY_5_2024,
|
|
231
|
+
endDate: TEST_DATES.JULY_6_2024,
|
|
217
232
|
interval: 24 * 60 * 60 * 1000, // 1 day
|
|
218
233
|
},
|
|
219
234
|
};
|
|
@@ -223,8 +238,16 @@ describe('transformTimeData', () => {
|
|
|
223
238
|
const result = transformTimeData(bars, type, barDataKeys);
|
|
224
239
|
|
|
225
240
|
expect(result).toEqual([
|
|
226
|
-
{
|
|
227
|
-
|
|
241
|
+
{
|
|
242
|
+
category: TEST_DATES.JULY_5_2024.valueOf(),
|
|
243
|
+
Success: 10,
|
|
244
|
+
Failed: 0,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
category: TEST_DATES.JULY_6_2024.valueOf(),
|
|
248
|
+
Success: 0,
|
|
249
|
+
Failed: 5,
|
|
250
|
+
},
|
|
228
251
|
]);
|
|
229
252
|
});
|
|
230
253
|
});
|
|
@@ -570,11 +593,11 @@ describe('formatPrometheusDataToRechartsDataAndBars', () => {
|
|
|
570
593
|
const bars = [
|
|
571
594
|
{
|
|
572
595
|
label: 'Success Count',
|
|
573
|
-
data: [[
|
|
596
|
+
data: [[TEST_DATES.JULY_5_2024, 10]] as [Date, number][],
|
|
574
597
|
},
|
|
575
598
|
{
|
|
576
599
|
label: 'Failed Count',
|
|
577
|
-
data: [[
|
|
600
|
+
data: [[TEST_DATES.JULY_5_2024, 5]] as [Date, number][],
|
|
578
601
|
},
|
|
579
602
|
];
|
|
580
603
|
|
|
@@ -583,8 +606,8 @@ describe('formatPrometheusDataToRechartsDataAndBars', () => {
|
|
|
583
606
|
{
|
|
584
607
|
type: 'time',
|
|
585
608
|
timeRange: {
|
|
586
|
-
startDate:
|
|
587
|
-
endDate:
|
|
609
|
+
startDate: TEST_DATES.JULY_5_2024,
|
|
610
|
+
endDate: TEST_DATES.JULY_5_2024,
|
|
588
611
|
interval: 24 * 60 * 60 * 1000,
|
|
589
612
|
},
|
|
590
613
|
},
|
|
@@ -598,7 +621,11 @@ describe('formatPrometheusDataToRechartsDataAndBars', () => {
|
|
|
598
621
|
|
|
599
622
|
// Should integrate: time transformation + status color assignment
|
|
600
623
|
expect(result.data).toEqual([
|
|
601
|
-
{
|
|
624
|
+
{
|
|
625
|
+
category: TEST_DATES.JULY_5_2024.valueOf(),
|
|
626
|
+
'Success Count': 10,
|
|
627
|
+
'Failed Count': 5,
|
|
628
|
+
},
|
|
602
629
|
]);
|
|
603
630
|
expect(result.rechartsBars).toEqual([
|
|
604
631
|
{ dataKey: 'Success Count', fill: '#4BE4E2' }, // lineColor3
|
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
BarchartBars,
|
|
4
4
|
BarchartTooltipFn,
|
|
5
5
|
} from './Barchart.component';
|
|
6
|
-
import { DAY_MONTH_FORMATER, TIME_FORMATER } from '../date/FormattedDateTime';
|
|
7
6
|
import { TooltipContentProps } from 'recharts';
|
|
8
7
|
import { chartColors, ChartColors } from '../../style/theme';
|
|
9
8
|
import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
|
|
@@ -84,31 +83,6 @@ const generateTimeRanges = (
|
|
|
84
83
|
return ranges;
|
|
85
84
|
};
|
|
86
85
|
|
|
87
|
-
/**
|
|
88
|
-
* Formats a date based on the interval
|
|
89
|
-
* @param date - Date object
|
|
90
|
-
* @param interval - Interval in milliseconds
|
|
91
|
-
* @returns Formatted string
|
|
92
|
-
*/
|
|
93
|
-
const formatDate = (date: Date, interval: number): string => {
|
|
94
|
-
if (interval > 24 * 60 * 60 * 1000) {
|
|
95
|
-
return (
|
|
96
|
-
DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '') +
|
|
97
|
-
' ' +
|
|
98
|
-
TIME_FORMATER.format(date)
|
|
99
|
-
);
|
|
100
|
-
} else if (interval === 24 * 60 * 60 * 1000) {
|
|
101
|
-
// Daily or longer intervals - use day format
|
|
102
|
-
return DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '');
|
|
103
|
-
} else if (interval >= 60 * 1000) {
|
|
104
|
-
//Handle hourly and minute intervals - use minute format
|
|
105
|
-
return TIME_FORMATER.format(date);
|
|
106
|
-
} else {
|
|
107
|
-
// Second intervals or less - use full timestamp
|
|
108
|
-
return date.toISOString();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
86
|
/**
|
|
113
87
|
* Finds the time range that contains the given date
|
|
114
88
|
* @param date - Data point date
|
|
@@ -149,16 +123,12 @@ export const transformTimeData = <T extends BarchartBars>(
|
|
|
149
123
|
type.timeRange.interval,
|
|
150
124
|
);
|
|
151
125
|
|
|
152
|
-
const categoryMap = new Map<
|
|
153
|
-
string | number,
|
|
154
|
-
{ [key: string]: string | number }
|
|
155
|
-
>();
|
|
126
|
+
const categoryMap = new Map<number, { [key: string]: string | number }>();
|
|
156
127
|
|
|
157
128
|
// Initialize all ranges with zeros
|
|
158
129
|
timeRanges.forEach((range) => {
|
|
159
|
-
const categoryDisplay = formatDate(range.start, type.timeRange.interval);
|
|
160
130
|
const initialData: { [key: string]: string | number } = {
|
|
161
|
-
category:
|
|
131
|
+
category: range.start.getTime(),
|
|
162
132
|
};
|
|
163
133
|
barDataKeys.forEach((dataKey) => {
|
|
164
134
|
initialData[dataKey] = 0;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { ChartLegend } from './ChartLegend';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ChartLegendWrapper } from './ChartLegendWrapper';
|
|
5
|
+
import userEvent from '@testing-library/user-event';
|
|
6
|
+
|
|
7
|
+
const colorSet = {
|
|
8
|
+
CPU: 'red',
|
|
9
|
+
Memory: 'blue',
|
|
10
|
+
Disk: 'green',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe('ChartLegend', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
it('should render all legend items', () => {
|
|
18
|
+
render(
|
|
19
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
20
|
+
<ChartLegend shape="line" />
|
|
21
|
+
</ChartLegendWrapper>,
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText('Memory')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('Disk')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should have all items selected by default', () => {
|
|
29
|
+
render(
|
|
30
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
31
|
+
<ChartLegend shape="line" />
|
|
32
|
+
</ChartLegendWrapper>,
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
36
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
37
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
it('should not respond to clicks when disabled', () => {
|
|
40
|
+
render(
|
|
41
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
42
|
+
<ChartLegend shape="line" disabled />
|
|
43
|
+
</ChartLegendWrapper>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
userEvent.click(screen.getByText('CPU'));
|
|
47
|
+
// If disabled, should not select any items
|
|
48
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
51
|
+
|
|
52
|
+
userEvent.click(screen.getByText('Memory'), { metaKey: true });
|
|
53
|
+
// If disabled, should not select any items
|
|
54
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
describe('Normal Click Behavior', () => {
|
|
59
|
+
it('should select only clicked item when all are selected', () => {
|
|
60
|
+
render(
|
|
61
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
62
|
+
<ChartLegend shape="line" />
|
|
63
|
+
</ChartLegendWrapper>,
|
|
64
|
+
);
|
|
65
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
68
|
+
|
|
69
|
+
// Click on CPU should select only CPU
|
|
70
|
+
userEvent.click(screen.getByLabelText('CPU selected'));
|
|
71
|
+
|
|
72
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
73
|
+
expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
|
|
74
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should select all items when clicking on the only selected item', () => {
|
|
78
|
+
render(
|
|
79
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
80
|
+
<ChartLegend shape="line" />
|
|
81
|
+
</ChartLegendWrapper>,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// First click to select only CPU
|
|
85
|
+
userEvent.click(screen.getByLabelText('CPU selected'));
|
|
86
|
+
|
|
87
|
+
// Second click should select all
|
|
88
|
+
userEvent.click(screen.getByLabelText('CPU selected'));
|
|
89
|
+
|
|
90
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
91
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
92
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should select only clicked item when clicking unselected item', () => {
|
|
96
|
+
render(
|
|
97
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
98
|
+
<ChartLegend shape="line" />
|
|
99
|
+
</ChartLegendWrapper>,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Select only CPU first
|
|
103
|
+
userEvent.click(screen.getByLabelText('CPU selected'));
|
|
104
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
105
|
+
expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
|
|
106
|
+
|
|
107
|
+
// Click on unselected Memory should select only Memory
|
|
108
|
+
userEvent.click(screen.getByText('Memory'));
|
|
109
|
+
expect(screen.getByLabelText('CPU not selected')).toBeInTheDocument();
|
|
110
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
111
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('Cmd+Click Behavior', () => {
|
|
116
|
+
it('should add unselected item to selection when cmd+clicking', () => {
|
|
117
|
+
render(
|
|
118
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
119
|
+
<ChartLegend shape="line" />
|
|
120
|
+
</ChartLegendWrapper>,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Select only CPU first
|
|
124
|
+
userEvent.click(screen.getByText('CPU'));
|
|
125
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
126
|
+
expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
|
|
127
|
+
|
|
128
|
+
// Cmd+click Memory should add it to selection
|
|
129
|
+
userEvent.click(screen.getByText('Memory'), { metaKey: true });
|
|
130
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
131
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
132
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should remove selected item from selection when cmd+clicking with multiple selected', () => {
|
|
136
|
+
render(
|
|
137
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
138
|
+
<ChartLegend shape="line" />
|
|
139
|
+
</ChartLegendWrapper>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Start with all selected, select only CPU, then add Memory
|
|
143
|
+
userEvent.click(screen.getByText('CPU'));
|
|
144
|
+
userEvent.click(screen.getByText('Memory'), { metaKey: true });
|
|
145
|
+
|
|
146
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
147
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
148
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
149
|
+
|
|
150
|
+
// Cmd+click CPU should remove it
|
|
151
|
+
userEvent.click(screen.getByText('CPU'), { metaKey: true });
|
|
152
|
+
expect(screen.getByLabelText('CPU not selected')).toBeInTheDocument();
|
|
153
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
154
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should select all when cmd+clicking the only selected item', () => {
|
|
158
|
+
render(
|
|
159
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
160
|
+
<ChartLegend shape="line" />
|
|
161
|
+
</ChartLegendWrapper>,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Select only CPU
|
|
165
|
+
userEvent.click(screen.getByText('CPU'));
|
|
166
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
167
|
+
expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
|
|
168
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
169
|
+
|
|
170
|
+
// Cmd+click the only selected item should select all
|
|
171
|
+
userEvent.click(screen.getByText('CPU'), { metaKey: true });
|
|
172
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
173
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
174
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should work with ctrl+click on Windows/Linux', () => {
|
|
178
|
+
render(
|
|
179
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
180
|
+
<ChartLegend shape="line" />
|
|
181
|
+
</ChartLegendWrapper>,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Select only CPU first
|
|
185
|
+
userEvent.click(screen.getByText('CPU'));
|
|
186
|
+
|
|
187
|
+
// Ctrl+click Memory should add it to selection
|
|
188
|
+
userEvent.click(screen.getByText('Memory'), { ctrlKey: true });
|
|
189
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
190
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
191
|
+
expect(screen.getByLabelText('Disk not selected')).toBeInTheDocument();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should handle mixed normal and cmd+clicks correctly', () => {
|
|
195
|
+
render(
|
|
196
|
+
<ChartLegendWrapper colorSet={colorSet}>
|
|
197
|
+
<ChartLegend shape="line" />
|
|
198
|
+
</ChartLegendWrapper>,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Normal click to select CPU only
|
|
202
|
+
userEvent.click(screen.getByText('CPU'));
|
|
203
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
204
|
+
expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
|
|
205
|
+
|
|
206
|
+
// Cmd+click to add Memory
|
|
207
|
+
userEvent.click(screen.getByText('Memory'), { metaKey: true });
|
|
208
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
209
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
210
|
+
|
|
211
|
+
// Normal click on Disk should select only Disk
|
|
212
|
+
userEvent.click(screen.getByText('Disk'));
|
|
213
|
+
expect(screen.getByLabelText('CPU not selected')).toBeInTheDocument();
|
|
214
|
+
expect(screen.getByLabelText('Memory not selected')).toBeInTheDocument();
|
|
215
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -18,7 +18,10 @@ const Legend = styled.div<{ direction: 'horizontal' | 'vertical' }>`
|
|
|
18
18
|
flex-wrap: wrap;
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
|
-
const LegendItem = styled.div<{
|
|
21
|
+
export const LegendItem = styled.div<{
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
selected?: boolean;
|
|
24
|
+
}>`
|
|
22
25
|
display: flex;
|
|
23
26
|
align-items: center;
|
|
24
27
|
gap: 8px;
|
|
@@ -31,7 +34,7 @@ const LegendItem = styled.div<{ disabled?: boolean; selected?: boolean }>`
|
|
|
31
34
|
}
|
|
32
35
|
`;
|
|
33
36
|
|
|
34
|
-
const LegendShape = styled.div<{
|
|
37
|
+
export const LegendShape = styled.div<{
|
|
35
38
|
color?: string;
|
|
36
39
|
shape: 'line' | 'rectangle';
|
|
37
40
|
chartColors: Record<string, string>;
|
|
@@ -69,21 +72,51 @@ export const ChartLegend = ({
|
|
|
69
72
|
isSelected,
|
|
70
73
|
addSelectedResource,
|
|
71
74
|
removeSelectedResource,
|
|
75
|
+
selectAllResources,
|
|
76
|
+
getAllResourcesCount,
|
|
77
|
+
getSelectedCount,
|
|
78
|
+
selectOnlyResource,
|
|
72
79
|
} = useChartLegend();
|
|
73
80
|
|
|
74
81
|
const resources = listResources();
|
|
75
82
|
|
|
76
83
|
const handleLegendClick = useCallback(
|
|
77
|
-
(resource: string) => {
|
|
84
|
+
(resource: string, event: React.MouseEvent) => {
|
|
78
85
|
if (disabled) return;
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
87
|
+
const isModifierClick = event.metaKey || event.ctrlKey;
|
|
88
|
+
const itemIsSelected = isSelected(resource);
|
|
89
|
+
|
|
90
|
+
if (isModifierClick) {
|
|
91
|
+
if (itemIsSelected) {
|
|
92
|
+
if (getSelectedCount() === 1) {
|
|
93
|
+
selectAllResources();
|
|
94
|
+
} else {
|
|
95
|
+
removeSelectedResource(resource);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
addSelectedResource(resource);
|
|
99
|
+
}
|
|
82
100
|
} else {
|
|
83
|
-
|
|
101
|
+
if (getSelectedCount() === getAllResourcesCount()) {
|
|
102
|
+
selectOnlyResource(resource);
|
|
103
|
+
} else if (itemIsSelected) {
|
|
104
|
+
selectAllResources();
|
|
105
|
+
} else {
|
|
106
|
+
selectOnlyResource(resource);
|
|
107
|
+
}
|
|
84
108
|
}
|
|
85
109
|
},
|
|
86
|
-
[
|
|
110
|
+
[
|
|
111
|
+
disabled,
|
|
112
|
+
isSelected,
|
|
113
|
+
addSelectedResource,
|
|
114
|
+
removeSelectedResource,
|
|
115
|
+
selectAllResources,
|
|
116
|
+
selectOnlyResource,
|
|
117
|
+
getAllResourcesCount,
|
|
118
|
+
getSelectedCount,
|
|
119
|
+
],
|
|
87
120
|
);
|
|
88
121
|
|
|
89
122
|
return (
|
|
@@ -97,7 +130,8 @@ export const ChartLegend = ({
|
|
|
97
130
|
key={resource}
|
|
98
131
|
disabled={disabled}
|
|
99
132
|
selected={selected}
|
|
100
|
-
|
|
133
|
+
aria-label={`${resource} ${selected ? 'selected' : 'not selected'}`}
|
|
134
|
+
onClick={(event) => handleLegendClick(resource, event)}
|
|
101
135
|
>
|
|
102
136
|
<LegendShape
|
|
103
137
|
color={color}
|