@scality/core-ui 0.163.0 → 0.165.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.
Files changed (32) hide show
  1. package/dist/components/barchartv2/Barchart.component.d.ts +10 -2
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +20 -11
  4. package/dist/components/barchartv2/utils.d.ts +10 -3
  5. package/dist/components/barchartv2/utils.d.ts.map +1 -1
  6. package/dist/components/barchartv2/utils.js +45 -22
  7. package/dist/index.d.ts +0 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +0 -1
  10. package/dist/next.d.ts +2 -0
  11. package/dist/next.d.ts.map +1 -1
  12. package/dist/next.js +2 -0
  13. package/package.json +1 -1
  14. package/src/lib/components/accordion/Accordion.test.tsx +1 -1
  15. package/src/lib/components/barchartv2/Barchart.component.test.tsx +41 -18
  16. package/src/lib/components/barchartv2/Barchart.component.tsx +56 -14
  17. package/src/lib/components/barchartv2/utils.test.ts +82 -47
  18. package/src/lib/components/barchartv2/utils.ts +53 -16
  19. package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +7 -7
  20. package/src/lib/components/infomessage/InfoMessageUtils.test.tsx +0 -1
  21. package/src/lib/components/inlineinput/InlineInput.test.tsx +10 -7
  22. package/src/lib/components/inputlist/InputList.test.tsx +1 -2
  23. package/src/lib/components/linetemporalchart/ChartUtil.test.ts +5 -4
  24. package/src/lib/components/selectv2/selectv2.test.tsx +8 -4
  25. package/src/lib/components/tablev2/TableSync.test.tsx +2 -3
  26. package/src/lib/components/tablev2/TableUtils.test.ts +6 -3
  27. package/src/lib/components/tablev2/Tablev2.test.tsx +2 -3
  28. package/src/lib/components/toggle/Toggle.test.tsx +1 -1
  29. package/src/lib/index.ts +0 -1
  30. package/src/lib/next.ts +2 -0
  31. package/stories/BarChart/barchart.stories.tsx +96 -5
  32. package/tsconfig.json +0 -1
@@ -18,13 +18,18 @@ import { ConstrainedText } from '../constrainedtext/Constrainedtext.component';
18
18
  import { IconHelp } from '../iconhelper/IconHelper';
19
19
  import { Loader } from '../loader/Loader.component';
20
20
  import { Text } from '../text/Text.component';
21
- import { renderTooltipContent, UnitRange, useChartData } from './utils';
21
+ import {
22
+ formatDate,
23
+ renderTooltipContent,
24
+ UnitRange,
25
+ useChartData,
26
+ } from './utils';
22
27
  import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
23
28
 
24
29
  const CHART_CONSTANTS = {
25
30
  TICK_WIDTH_OFFSET: 5,
26
31
  BAR_SIZE: 12,
27
- MIN_POINT_SIZE: 1,
32
+ MIN_POINT_SIZE: 3,
28
33
  DEFAULT_HEIGHT: 200,
29
34
  CHART_MARGIN: {
30
35
  left: 0,
@@ -70,17 +75,25 @@ export type BarchartSortFn<T extends BarchartBars> = (
70
75
 
71
76
  export type BarchartProps<T extends BarchartBars> = {
72
77
  type: 'category' | TimeType;
73
- bars: T;
78
+ bars?: T;
74
79
  tooltip?: BarchartTooltipFn<T>;
75
80
  defaultSort?: BarchartSortFn<T>;
76
81
  unitRange?: UnitRange;
77
- helpTooltip?: string;
82
+ helpTooltip?: React.ReactNode;
78
83
  stacked?: boolean;
84
+ /**
85
+ * Sort the bars by default or by legend order
86
+ * legend will sort the bars by the order of the colorSet property of the ChartLegendWrapper component
87
+ * default will sort the bars by average values in descending order (biggest values will be at bottom)
88
+ * @default 'default'
89
+ */
90
+ stackedBarSort?: 'default' | 'legend';
79
91
  title?: string;
80
92
  secondaryTitle?: string;
81
93
  rightTitle?: React.ReactNode;
82
94
  height?: number;
83
95
  isLoading?: boolean;
96
+ isError?: boolean;
84
97
  };
85
98
 
86
99
  interface CustomTickProps {
@@ -91,6 +104,7 @@ interface CustomTickProps {
91
104
  };
92
105
  visibleTicksCount: number;
93
106
  width: number;
107
+ type: TimeType;
94
108
  }
95
109
 
96
110
  /* ---------------------------------- COMPONENTS ---------------------------------- */
@@ -101,6 +115,7 @@ const CustomTick = ({
101
115
  payload,
102
116
  visibleTicksCount,
103
117
  width,
118
+ type,
104
119
  }: CustomTickProps) => {
105
120
  const theme = useTheme();
106
121
  const tickWidth =
@@ -118,7 +133,9 @@ const CustomTick = ({
118
133
  <ConstrainedText
119
134
  text={
120
135
  <Text variant="Smaller" color="textSecondary">
121
- {String(payload.value)}
136
+ {type.type === 'time'
137
+ ? formatDate(new Date(payload.value), type.timeRange.interval)
138
+ : String(payload.value)}
122
139
  </Text>
123
140
  }
124
141
  centered
@@ -149,16 +166,14 @@ const ChartHeader = ({
149
166
  }: {
150
167
  title?: string;
151
168
  secondaryTitle?: string;
152
- helpTooltip?: string;
169
+ helpTooltip?: React.ReactNode;
153
170
  rightTitle?: React.ReactNode;
154
171
  }) => {
155
172
  return (
156
173
  <Wrap>
157
174
  <Stack gap="r4">
158
175
  <Text variant="ChartTitle">{title}</Text>
159
- {helpTooltip && (
160
- <IconHelp tooltipMessage={helpTooltip} title={helpTooltip} />
161
- )}
176
+ {helpTooltip && <IconHelp tooltipMessage={helpTooltip} />}
162
177
 
163
178
  {secondaryTitle && (
164
179
  <Text
@@ -177,6 +192,21 @@ const ChartHeader = ({
177
192
  );
178
193
  };
179
194
 
195
+ const Error = ({ height }: { height: number }) => {
196
+ return (
197
+ <Box
198
+ height={height}
199
+ style={{
200
+ alignItems: 'center',
201
+ justifyContent: 'center',
202
+ display: 'flex',
203
+ }}
204
+ >
205
+ <Text>Chart data is not available</Text>
206
+ </Box>
207
+ );
208
+ };
209
+
180
210
  const Loading = ({ height }: { height: number }) => {
181
211
  return (
182
212
  <Box
@@ -205,6 +235,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
205
235
  type = 'category',
206
236
  unitRange,
207
237
  stacked,
238
+ stackedBarSort = 'default',
208
239
  defaultSort,
209
240
  tooltip,
210
241
  title,
@@ -212,10 +243,11 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
212
243
  helpTooltip,
213
244
  rightTitle,
214
245
  isLoading,
246
+ isError,
215
247
  } = props;
216
248
 
217
249
  // Create colorSet from ChartLegendWrapper
218
- const colorSet = bars.reduce(
250
+ const colorSet = bars?.reduce(
219
251
  (acc, bar) => {
220
252
  const color = getColor(bar.label);
221
253
  if (color) {
@@ -227,7 +259,15 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
227
259
  );
228
260
 
229
261
  const { rechartsBars, unitLabel, roundReferenceValue, rechartsData } =
230
- useChartData(bars, type, colorSet, stacked, defaultSort, unitRange);
262
+ useChartData(
263
+ bars || [],
264
+ type,
265
+ colorSet || {},
266
+ stacked,
267
+ defaultSort,
268
+ unitRange,
269
+ stackedBarSort,
270
+ );
231
271
 
232
272
  return (
233
273
  <Stack direction="vertical" gap="r8">
@@ -237,7 +277,9 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
237
277
  helpTooltip={helpTooltip}
238
278
  rightTitle={rightTitle}
239
279
  />
240
- {isLoading ? (
280
+ {isError || (!bars && !isLoading) ? (
281
+ <Error height={height} />
282
+ ) : isLoading ? (
241
283
  <Loading height={height} />
242
284
  ) : (
243
285
  <StyledResponsiveContainer width="100%" height={height}>
@@ -260,7 +302,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
260
302
  key={dataKey}
261
303
  dataKey={dataKey}
262
304
  fill={chartColors[fill] || fill}
263
- minPointSize={CHART_CONSTANTS.MIN_POINT_SIZE}
305
+ minPointSize={stacked ? 0 : CHART_CONSTANTS.MIN_POINT_SIZE}
264
306
  stackId={stackId}
265
307
  onMouseOver={() => setHoveredValue(dataKey)}
266
308
  onMouseLeave={() => setHoveredValue(undefined)}
@@ -295,7 +337,7 @@ export const Barchart = <T extends BarchartBars>(props: BarchartProps<T>) => {
295
337
  />
296
338
  <XAxis
297
339
  dataKey="category"
298
- tick={(props) => <CustomTick {...props} />}
340
+ tick={(props) => <CustomTick {...props} type={type} />}
299
341
  type="category"
300
342
  interval={0}
301
343
  allowDataOverflow={true}
@@ -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
- [new Date('2024-07-05T00:00:00'), 10],
50
- [new Date('2024-07-06T00:00:00'), 20],
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: new Date('2024-07-05T00:00:00'),
59
- endDate: new Date('2024-07-06T00:00:00'),
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: 'Fri05Jul', Success: 10 },
70
- { category: 'Sat06Jul', Success: 20 },
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
- [new Date('2024-07-05T00:00:00'), 10],
94
+ [TEST_DATES.JULY_5_2024, 10],
80
95
  // Missing July 6th
81
- [new Date('2024-07-07T00:00:00'), 30],
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: new Date('2024-07-05T00:00:00'),
90
- endDate: new Date('2024-07-07T00:00:00'),
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: 'Fri05Jul', Success: 10 },
101
- { category: 'Sat06Jul', Success: 0 }, // Missing data filled with 0
102
- { category: 'Sun07Jul', Success: 30 },
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
- [new Date('2024-07-05T10:00:00'), 10],
112
- [new Date('2024-07-05T11:00:00'), 20],
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: new Date('2024-07-05T10:00:00'),
121
- endDate: new Date('2024-07-05T11:00:00'),
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: '10:00', Success: 10 },
132
- { category: '11:00', Success: 20 },
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
- [new Date('2024-07-05T08:30:00'), 10], // 8:30 AM
142
- [new Date('2024-07-05T14:45:00'), 25], // 2:45 PM (should overwrite 8:30 AM)
143
- [new Date('2024-07-06T09:15:00'), 15], // Next day
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: new Date('2024-07-05T00:00:00'),
152
- endDate: new Date('2024-07-06T00:00:00'),
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: 'Fri05Jul', Success: 25 }, // Last value for July 5th
163
- { category: 'Sat06Jul', Success: 15 }, // July 6th value
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
- [new Date('2024-07-07T10:00:00'), 30], // July 7th (latest)
173
- [new Date('2024-07-05T08:00:00'), 10], // July 5th (earliest)
174
- [new Date('2024-07-06T14:00:00'), 20], // July 6th (middle)
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: new Date('2024-07-05T00:00:00'),
183
- endDate: new Date('2024-07-07T00:00:00'),
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: 'Fri05Jul', Success: 10 },
195
- { category: 'Sat06Jul', Success: 20 },
196
- { category: 'Sun07Jul', Success: 30 },
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: [[new Date('2024-07-05T00:00:00'), 10]] as [Date, number][],
219
+ data: [[TEST_DATES.JULY_5_2024, 10]] as [Date, number][],
205
220
  },
206
221
  {
207
222
  label: 'Failed',
208
- data: [[new Date('2024-07-06T00:00:00'), 5]] as [Date, number][],
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: new Date('2024-07-05T00:00:00'),
216
- endDate: new Date('2024-07-06T00:00:00'),
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
- { category: 'Fri05Jul', Success: 10, Failed: 0 },
227
- { category: 'Sat06Jul', Success: 0, Failed: 5 },
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
  });
@@ -482,8 +505,8 @@ describe('applySortingToData', () => {
482
505
 
483
506
  describe('getRoundReferenceValue', () => {
484
507
  it('should return appropriate rounded values', () => {
485
- expect(getRoundReferenceValue(1)).toBe(1);
486
- expect(getRoundReferenceValue(2)).toBe(2.5);
508
+ expect(getRoundReferenceValue(1)).toBe(5);
509
+ expect(getRoundReferenceValue(2)).toBe(5);
487
510
  expect(getRoundReferenceValue(3)).toBe(5);
488
511
  expect(getRoundReferenceValue(7)).toBe(10);
489
512
  expect(getRoundReferenceValue(15)).toBe(25);
@@ -570,11 +593,11 @@ describe('formatPrometheusDataToRechartsDataAndBars', () => {
570
593
  const bars = [
571
594
  {
572
595
  label: 'Success Count',
573
- data: [[new Date('2024-07-05T00:00:00'), 10]] as [Date, number][],
596
+ data: [[TEST_DATES.JULY_5_2024, 10]] as [Date, number][],
574
597
  },
575
598
  {
576
599
  label: 'Failed Count',
577
- data: [[new Date('2024-07-05T00:00:00'), 5]] as [Date, number][],
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: new Date('2024-07-05T00:00:00'),
587
- endDate: new Date('2024-07-05T00:00:00'),
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
- { category: 'Fri05Jul', 'Success Count': 10, 'Failed Count': 5 },
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
@@ -717,6 +744,14 @@ describe('sortStackedBars', () => {
717
744
  { dataKey: 'bar1', fill: 'blue' },
718
745
  ]);
719
746
  });
747
+ it('should sort bars by legend order when stacked is true and legendOrder is provided', () => {
748
+ const result = sortStackedBars(bars, data, true, ['bar3', 'bar2', 'bar1']);
749
+ expect(result).toEqual([
750
+ { dataKey: 'bar3', fill: 'green' },
751
+ { dataKey: 'bar2', fill: 'red' },
752
+ { dataKey: 'bar1', fill: 'blue' },
753
+ ]);
754
+ });
720
755
  it('should not sort bars when stacked is false', () => {
721
756
  const result = sortStackedBars(bars, data, false);
722
757
  expect(result).toEqual([
@@ -18,10 +18,14 @@ export const getRoundReferenceValue = (value: number): number => {
18
18
  const normalized = value / magnitude;
19
19
 
20
20
  // Round to nice numbers based on normalized value
21
- if (normalized <= 1) return magnitude;
22
- if (normalized <= 2.5) return 2.5 * magnitude;
23
- if (normalized <= 5) return 5 * magnitude;
24
- return 10 * magnitude;
21
+ let result: number;
22
+ if (normalized <= 1) result = magnitude;
23
+ else if (normalized <= 2.5) result = 2.5 * magnitude;
24
+ else if (normalized <= 5) result = 5 * magnitude;
25
+ else result = 10 * magnitude;
26
+
27
+ // Ensure minimum value of 5 for better chart appearance
28
+ return Math.max(result, 5);
25
29
  };
26
30
 
27
31
  export const getMaxBarValue = (
@@ -46,9 +50,9 @@ export const getMaxBarValue = (
46
50
  .filter((key) => key !== 'category')
47
51
  .map((key) => Number(item[key]));
48
52
  // Get the max value among the values in the object (corresponding to one bar)
49
- return Math.max(...numberValues);
53
+ return Math.max(...numberValues, 0); // Ensure we don't get -Infinity
50
54
  });
51
- return Math.max(...values);
55
+ return Math.max(...values, 0);
52
56
  };
53
57
 
54
58
  /**
@@ -64,9 +68,6 @@ const generateTimeRanges = (
64
68
  interval: number,
65
69
  ): { start: Date; end: Date }[] => {
66
70
  const ranges: { start: Date; end: Date }[] = [];
67
- if (!startDate || !endDate || !interval) {
68
- return ranges;
69
- }
70
71
 
71
72
  let currentDate = new Date(startDate.getTime());
72
73
  while (currentDate.getTime() <= endDate.getTime()) {
@@ -89,7 +90,7 @@ const generateTimeRanges = (
89
90
  * @param interval - Interval in milliseconds
90
91
  * @returns Formatted string
91
92
  */
92
- const formatDate = (date: Date, interval: number): string => {
93
+ export const formatDate = (date: Date, interval: number): string => {
93
94
  if (interval > 24 * 60 * 60 * 1000) {
94
95
  return (
95
96
  DAY_MONTH_FORMATER.format(date).replace(/[ ,]/g, '') +
@@ -155,7 +156,8 @@ export const transformTimeData = <T extends BarchartBars>(
155
156
 
156
157
  // Initialize all ranges with zeros
157
158
  timeRanges.forEach((range) => {
158
- const categoryDisplay = formatDate(range.start, type.timeRange.interval);
159
+ // const categoryDisplay = formatDate(range.start, type.timeRange.interval);
160
+ const categoryDisplay = range.start.getTime();
159
161
  const initialData: { [key: string]: string | number } = {
160
162
  category: categoryDisplay,
161
163
  };
@@ -288,6 +290,7 @@ export const formatPrometheusDataToRechartsDataAndBars = <
288
290
  colorSet: Record<string, ChartColors | string>,
289
291
  stacked?: boolean,
290
292
  defaultSort?: BarchartProps<T>['defaultSort'],
293
+ legendOrder?: string[],
291
294
  ): {
292
295
  data: { [key: string]: string | number }[];
293
296
  rechartsBars: { dataKey: string; fill: string; stackId?: string }[];
@@ -307,7 +310,12 @@ export const formatPrometheusDataToRechartsDataAndBars = <
307
310
  data = applySortingToData(data, barDataKeys, defaultSort);
308
311
  }
309
312
 
310
- const sortedRechartsBars = sortStackedBars(rechartsBars, data, stacked);
313
+ const sortedRechartsBars = sortStackedBars(
314
+ rechartsBars,
315
+ data,
316
+ stacked,
317
+ legendOrder,
318
+ );
311
319
 
312
320
  return {
313
321
  rechartsBars: sortedRechartsBars,
@@ -330,7 +338,7 @@ export const computeUnitLabelAndRoundReferenceValue = (
330
338
  return { unitLabel: '', roundReferenceValue, rechartsData: data };
331
339
  }
332
340
 
333
- const { valueBase, unitLabel } = getUnitLabel(unitRange ?? [], maxValue);
341
+ const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue);
334
342
  const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
335
343
  const roundReferenceValue = getRoundReferenceValue(topValue);
336
344
  const rechartsData = data.map((dataPoint) => {
@@ -397,8 +405,8 @@ export function getUnitLabel(
397
405
  };
398
406
  }
399
407
 
400
- // Sort stacked bars by their average values in descending order
401
- // This ensures the largest bars appear at the bottom of the stack
408
+ // Sort stacked bars by their average values in descending order or by legend order
409
+ // This ensures the largest bars appear at the bottom of the stack (default) or follow legend order
402
410
  export const sortStackedBars = (
403
411
  rechartsBars: {
404
412
  dataKey: string;
@@ -409,10 +417,33 @@ export const sortStackedBars = (
409
417
  [key: string]: string | number;
410
418
  }[],
411
419
  stacked?: boolean,
420
+ legendOrder?: string[],
412
421
  ) => {
413
422
  if (!stacked) {
414
423
  return rechartsBars;
415
424
  }
425
+
426
+ // If legend order is provided, sort by legend order
427
+ if (legendOrder && legendOrder.length > 0) {
428
+ return [...rechartsBars].sort((a, b) => {
429
+ const indexA = legendOrder.indexOf(a.dataKey);
430
+ const indexB = legendOrder.indexOf(b.dataKey);
431
+
432
+ // If both items are in legend order, sort by their position
433
+ if (indexA !== -1 && indexB !== -1) {
434
+ return indexA - indexB;
435
+ }
436
+
437
+ // If only one item is in legend order, prioritize it
438
+ if (indexA !== -1) return -1;
439
+ if (indexB !== -1) return 1;
440
+
441
+ // If neither is in legend order, maintain original order
442
+ return 0;
443
+ });
444
+ }
445
+
446
+ // Default behavior: sort by average values
416
447
  const barAverages = rechartsBars.map((bar) => {
417
448
  const values = data
418
449
  .map((item) => Number(item[bar.dataKey]) || 0)
@@ -502,14 +533,20 @@ export const useChartData = <T extends BarchartBars>(
502
533
  stacked?: boolean,
503
534
  defaultSort?: BarchartProps<T>['defaultSort'],
504
535
  unitRange?: UnitRange,
536
+ stackedBarSort?: 'default' | 'legend',
505
537
  ) => {
506
- const { selectedResources } = useChartLegend();
538
+ const { selectedResources, listResources } = useChartLegend();
539
+
540
+ // Get legend order when stackedBarSort is 'legend'
541
+ const legendOrder = stackedBarSort === 'legend' ? listResources() : undefined;
542
+
507
543
  const { data, rechartsBars } = formatPrometheusDataToRechartsDataAndBars(
508
544
  bars,
509
545
  type,
510
546
  colorSet,
511
547
  stacked,
512
548
  defaultSort,
549
+ legendOrder,
513
550
  );
514
551
 
515
552
  // Filter both data and bars to only include selected resources for accurate maxValue calculation
@@ -2,7 +2,7 @@ import {
2
2
  HealthSelector,
3
3
  optionsDefaultConfiguration,
4
4
  } from './HealthSelector.component';
5
- import React from 'react';
5
+ import { act } from 'react';
6
6
  import { render, screen, waitFor } from '@testing-library/react';
7
7
  import userEvent from '@testing-library/user-event';
8
8
  import { getWrapper } from '../../testUtils';
@@ -18,11 +18,11 @@ describe('HealthSelector', () => {
18
18
  const input = screen.getByRole('textbox');
19
19
 
20
20
  // open the menu
21
- userEvent.click(input);
21
+ await act(() => userEvent.click(input));
22
22
  const healthyOption = getByText(/healthy/i);
23
23
  expect(healthyOption).toBeInTheDocument();
24
24
  });
25
- it('should call the onChange function when it change', () => {
25
+ it('should call the onChange function when it change', async () => {
26
26
  const { Wrapper } = getWrapper();
27
27
  const onChange = jest.fn();
28
28
  const { getByText } = render(
@@ -31,12 +31,12 @@ describe('HealthSelector', () => {
31
31
  </Wrapper>,
32
32
  );
33
33
  const input = screen.getByRole('textbox');
34
- userEvent.click(input);
34
+ await act(() => userEvent.click(input));
35
35
  const warningOption = getByText(/warning/i);
36
- userEvent.click(warningOption);
36
+ await act(() => userEvent.click(warningOption));
37
37
  expect(onChange).toHaveBeenCalledWith('warning');
38
38
  });
39
- it('should not display hidden options', () => {
39
+ it('should not display hidden options', async () => {
40
40
  const { Wrapper } = getWrapper();
41
41
  const { queryByText } = render(
42
42
  <Wrapper>
@@ -55,7 +55,7 @@ describe('HealthSelector', () => {
55
55
 
56
56
  // open the menu
57
57
  const input = screen.getByRole('textbox');
58
- userEvent.click(input);
58
+ await act(() => userEvent.click(input));
59
59
  const healthyOption = queryByText(/healthy/i);
60
60
  expect(healthyOption).not.toBeInTheDocument();
61
61
  });
@@ -1,6 +1,5 @@
1
1
  import '@testing-library/jest-dom';
2
2
  import { render } from '@testing-library/react';
3
- import React from 'react';
4
3
  import { coreUIAvailableThemes } from '../../style/theme';
5
4
  import { CoreUiThemeProvider } from '../coreuithemeprovider/CoreUiThemeProvider';
6
5
  import { useComputeBackgroundColor } from './InfoMessageUtils';