@scality/core-ui 0.162.0 → 0.164.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 +9 -3
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +22 -5
- package/dist/components/barchartv2/utils.d.ts +26 -3
- package/dist/components/barchartv2/utils.d.ts.map +1 -1
- package/dist/components/barchartv2/utils.js +76 -22
- package/dist/components/chartlegend/ChartLegend.d.ts +8 -0
- package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -0
- package/dist/components/chartlegend/ChartLegend.js +65 -0
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts +17 -0
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -0
- package/dist/components/chartlegend/ChartLegendWrapper.js +50 -0
- package/dist/components/date/FormattedDateTime.d.ts +3 -1
- package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.js +19 -1
- package/dist/components/date/FormattedDateTime.spec.js +12 -0
- package/dist/components/icon/Icon.component.d.ts +5 -5
- package/dist/components/icon/Icon.component.d.ts.map +1 -1
- package/dist/components/icon/Icon.component.js +33 -31
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +33 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +249 -0
- package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
- package/dist/components/selectv2/Selectv2.component.js +11 -6
- package/dist/components/steppers/Stepper.component.d.ts.map +1 -1
- package/dist/components/steppers/Stepper.component.js +9 -8
- package/dist/components/toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/toast/ToastProvider.js +4 -5
- package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
- package/dist/components/vegachartv2/SyncedCursorCharts.js +3 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/next.d.ts +2 -0
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +2 -0
- package/dist/style/theme.d.ts +1 -0
- package/dist/style/theme.d.ts.map +1 -1
- package/dist/style/theme.js +28 -0
- package/package.json +2 -2
- package/src/lib/components/accordion/Accordion.test.tsx +8 -16
- package/src/lib/components/barchartv2/Barchart.component.test.tsx +117 -111
- package/src/lib/components/barchartv2/Barchart.component.tsx +54 -7
- package/src/lib/components/barchartv2/utils.test.ts +127 -2
- package/src/lib/components/barchartv2/utils.ts +103 -19
- package/src/lib/components/chartlegend/ChartLegend.tsx +113 -0
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +85 -0
- package/src/lib/components/date/FormattedDateTime.spec.tsx +24 -0
- package/src/lib/components/date/FormattedDateTime.tsx +36 -2
- package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +10 -10
- package/src/lib/components/icon/Icon.component.tsx +48 -60
- package/src/lib/components/infomessage/InfoMessageUtils.test.tsx +0 -1
- package/src/lib/components/inlineinput/InlineInput.test.tsx +28 -22
- package/src/lib/components/inputlist/InputList.test.tsx +22 -21
- package/src/lib/components/linetemporalchart/ChartUtil.test.ts +5 -4
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +502 -0
- package/src/lib/components/searchinput/SearchInput.test.tsx +3 -7
- package/src/lib/components/selectv2/Selectv2.component.tsx +13 -5
- package/src/lib/components/selectv2/selectv2.test.tsx +70 -61
- package/src/lib/components/steppers/Stepper.component.tsx +10 -8
- package/src/lib/components/tablev2/TableSync.test.tsx +8 -12
- package/src/lib/components/tablev2/TableUtils.test.ts +6 -3
- package/src/lib/components/tablev2/Tablev2.test.tsx +38 -40
- package/src/lib/components/toast/ToastProvider.tsx +14 -6
- package/src/lib/components/toggle/Toggle.test.tsx +1 -1
- package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +5 -7
- package/src/lib/index.ts +1 -0
- package/src/lib/next.ts +2 -0
- package/src/lib/style/theme.ts +29 -0
- package/stories/BarChart/barchart.stories.tsx +387 -129
- package/stories/format.mdx +4 -2
- package/stories/linetimeseriechart.stories.tsx +485 -0
- package/tsconfig.json +0 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import { DAY_MONTH_FORMATER, TIME_FORMATER } from '../date/FormattedDateTime';
|
|
7
7
|
import { TooltipContentProps } from 'recharts';
|
|
8
8
|
import { chartColors, ChartColors } from '../../style/theme';
|
|
9
|
+
import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
|
|
9
10
|
|
|
10
11
|
export const getRoundReferenceValue = (value: number): number => {
|
|
11
12
|
if (value <= 0) return 10; // Default for zero or negative values
|
|
@@ -17,10 +18,14 @@ export const getRoundReferenceValue = (value: number): number => {
|
|
|
17
18
|
const normalized = value / magnitude;
|
|
18
19
|
|
|
19
20
|
// Round to nice numbers based on normalized value
|
|
20
|
-
|
|
21
|
-
if (normalized <=
|
|
22
|
-
if (normalized <= 5)
|
|
23
|
-
|
|
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);
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
export const getMaxBarValue = (
|
|
@@ -45,9 +50,9 @@ export const getMaxBarValue = (
|
|
|
45
50
|
.filter((key) => key !== 'category')
|
|
46
51
|
.map((key) => Number(item[key]));
|
|
47
52
|
// Get the max value among the values in the object (corresponding to one bar)
|
|
48
|
-
return Math.max(...numberValues);
|
|
53
|
+
return Math.max(...numberValues, 0); // Ensure we don't get -Infinity
|
|
49
54
|
});
|
|
50
|
-
return Math.max(...values);
|
|
55
|
+
return Math.max(...values, 0);
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
/**
|
|
@@ -63,9 +68,6 @@ const generateTimeRanges = (
|
|
|
63
68
|
interval: number,
|
|
64
69
|
): { start: Date; end: Date }[] => {
|
|
65
70
|
const ranges: { start: Date; end: Date }[] = [];
|
|
66
|
-
if (!startDate || !endDate || !interval) {
|
|
67
|
-
return ranges;
|
|
68
|
-
}
|
|
69
71
|
|
|
70
72
|
let currentDate = new Date(startDate.getTime());
|
|
71
73
|
while (currentDate.getTime() <= endDate.getTime()) {
|
|
@@ -248,7 +250,7 @@ export const applySortingToData = <T extends BarchartBars>(
|
|
|
248
250
|
|
|
249
251
|
const getRechartsBarsAndBarDataKeys = (
|
|
250
252
|
bars: BarchartBars,
|
|
251
|
-
colorSet: Record<
|
|
253
|
+
colorSet: Record<string, ChartColors | string>,
|
|
252
254
|
stacked?: boolean,
|
|
253
255
|
) => {
|
|
254
256
|
const rechartsBars: { dataKey: string; fill: string; stackId?: string }[] =
|
|
@@ -284,9 +286,10 @@ export const formatPrometheusDataToRechartsDataAndBars = <
|
|
|
284
286
|
>(
|
|
285
287
|
bars: T,
|
|
286
288
|
type: BarchartProps<T>['type'],
|
|
287
|
-
colorSet: Record<
|
|
289
|
+
colorSet: Record<string, ChartColors | string>,
|
|
288
290
|
stacked?: boolean,
|
|
289
291
|
defaultSort?: BarchartProps<T>['defaultSort'],
|
|
292
|
+
legendOrder?: string[],
|
|
290
293
|
): {
|
|
291
294
|
data: { [key: string]: string | number }[];
|
|
292
295
|
rechartsBars: { dataKey: string; fill: string; stackId?: string }[];
|
|
@@ -306,7 +309,12 @@ export const formatPrometheusDataToRechartsDataAndBars = <
|
|
|
306
309
|
data = applySortingToData(data, barDataKeys, defaultSort);
|
|
307
310
|
}
|
|
308
311
|
|
|
309
|
-
const sortedRechartsBars = sortStackedBars(
|
|
312
|
+
const sortedRechartsBars = sortStackedBars(
|
|
313
|
+
rechartsBars,
|
|
314
|
+
data,
|
|
315
|
+
stacked,
|
|
316
|
+
legendOrder,
|
|
317
|
+
);
|
|
310
318
|
|
|
311
319
|
return {
|
|
312
320
|
rechartsBars: sortedRechartsBars,
|
|
@@ -329,7 +337,7 @@ export const computeUnitLabelAndRoundReferenceValue = (
|
|
|
329
337
|
return { unitLabel: '', roundReferenceValue, rechartsData: data };
|
|
330
338
|
}
|
|
331
339
|
|
|
332
|
-
const { valueBase, unitLabel } = getUnitLabel(unitRange
|
|
340
|
+
const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue);
|
|
333
341
|
const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
|
|
334
342
|
const roundReferenceValue = getRoundReferenceValue(topValue);
|
|
335
343
|
const rechartsData = data.map((dataPoint) => {
|
|
@@ -396,8 +404,8 @@ export function getUnitLabel(
|
|
|
396
404
|
};
|
|
397
405
|
}
|
|
398
406
|
|
|
399
|
-
// Sort stacked bars by their average values in descending order
|
|
400
|
-
// This ensures the largest bars appear at the bottom of the stack
|
|
407
|
+
// Sort stacked bars by their average values in descending order or by legend order
|
|
408
|
+
// This ensures the largest bars appear at the bottom of the stack (default) or follow legend order
|
|
401
409
|
export const sortStackedBars = (
|
|
402
410
|
rechartsBars: {
|
|
403
411
|
dataKey: string;
|
|
@@ -408,10 +416,33 @@ export const sortStackedBars = (
|
|
|
408
416
|
[key: string]: string | number;
|
|
409
417
|
}[],
|
|
410
418
|
stacked?: boolean,
|
|
419
|
+
legendOrder?: string[],
|
|
411
420
|
) => {
|
|
412
421
|
if (!stacked) {
|
|
413
422
|
return rechartsBars;
|
|
414
423
|
}
|
|
424
|
+
|
|
425
|
+
// If legend order is provided, sort by legend order
|
|
426
|
+
if (legendOrder && legendOrder.length > 0) {
|
|
427
|
+
return [...rechartsBars].sort((a, b) => {
|
|
428
|
+
const indexA = legendOrder.indexOf(a.dataKey);
|
|
429
|
+
const indexB = legendOrder.indexOf(b.dataKey);
|
|
430
|
+
|
|
431
|
+
// If both items are in legend order, sort by their position
|
|
432
|
+
if (indexA !== -1 && indexB !== -1) {
|
|
433
|
+
return indexA - indexB;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// If only one item is in legend order, prioritize it
|
|
437
|
+
if (indexA !== -1) return -1;
|
|
438
|
+
if (indexB !== -1) return 1;
|
|
439
|
+
|
|
440
|
+
// If neither is in legend order, maintain original order
|
|
441
|
+
return 0;
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Default behavior: sort by average values
|
|
415
446
|
const barAverages = rechartsBars.map((bar) => {
|
|
416
447
|
const values = data
|
|
417
448
|
.map((item) => Number(item[bar.dataKey]) || 0)
|
|
@@ -456,29 +487,82 @@ export const renderTooltipContent = <T extends BarchartBars>(
|
|
|
456
487
|
return tooltip(currentPoint);
|
|
457
488
|
};
|
|
458
489
|
|
|
490
|
+
/**
|
|
491
|
+
* Filters both chart data and recharts bars to only include selected resources from legend
|
|
492
|
+
* @param data - Array of chart data objects with category and resource values
|
|
493
|
+
* @param rechartsBars - Array of recharts bar configurations
|
|
494
|
+
* @param selectedResources - Array of selected resource names
|
|
495
|
+
* @returns Object containing filtered data and recharts bars
|
|
496
|
+
*/
|
|
497
|
+
export const filterChartDataAndBarsByLegendSelection = (
|
|
498
|
+
data: { [key: string]: string | number }[],
|
|
499
|
+
rechartsBars: { dataKey: string; fill: string; stackId?: string }[],
|
|
500
|
+
selectedResources: string[],
|
|
501
|
+
) => {
|
|
502
|
+
// If no resources are selected, show all data and bars (default behavior)
|
|
503
|
+
if (selectedResources.length === 0) {
|
|
504
|
+
return { filteredData: data, filteredRechartsBars: rechartsBars };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Filter recharts bars
|
|
508
|
+
const filteredRechartsBars = rechartsBars.filter((bar) =>
|
|
509
|
+
selectedResources.includes(bar.dataKey),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Filter data to only include selected resources
|
|
513
|
+
const filteredData = data.map((item) => {
|
|
514
|
+
const filteredItem: { [key: string]: string | number } = {
|
|
515
|
+
category: item.category,
|
|
516
|
+
};
|
|
517
|
+
selectedResources.forEach((resource) => {
|
|
518
|
+
if (resource in item) {
|
|
519
|
+
filteredItem[resource] = item[resource];
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
return filteredItem;
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
return { filteredData, filteredRechartsBars };
|
|
526
|
+
};
|
|
527
|
+
|
|
459
528
|
export const useChartData = <T extends BarchartBars>(
|
|
460
529
|
bars: T,
|
|
461
530
|
type: BarchartProps<T>['type'],
|
|
462
|
-
colorSet: Record<
|
|
531
|
+
colorSet: Record<string, ChartColors | string>,
|
|
463
532
|
stacked?: boolean,
|
|
464
533
|
defaultSort?: BarchartProps<T>['defaultSort'],
|
|
465
534
|
unitRange?: UnitRange,
|
|
535
|
+
stackedBarSort?: 'default' | 'legend',
|
|
466
536
|
) => {
|
|
537
|
+
const { selectedResources, listResources } = useChartLegend();
|
|
538
|
+
|
|
539
|
+
// Get legend order when stackedBarSort is 'legend'
|
|
540
|
+
const legendOrder = stackedBarSort === 'legend' ? listResources() : undefined;
|
|
541
|
+
|
|
467
542
|
const { data, rechartsBars } = formatPrometheusDataToRechartsDataAndBars(
|
|
468
543
|
bars,
|
|
469
544
|
type,
|
|
470
545
|
colorSet,
|
|
471
546
|
stacked,
|
|
472
547
|
defaultSort,
|
|
548
|
+
legendOrder,
|
|
473
549
|
);
|
|
474
550
|
|
|
475
|
-
|
|
551
|
+
// Filter both data and bars to only include selected resources for accurate maxValue calculation
|
|
552
|
+
const { filteredData, filteredRechartsBars } =
|
|
553
|
+
filterChartDataAndBarsByLegendSelection(
|
|
554
|
+
data,
|
|
555
|
+
rechartsBars,
|
|
556
|
+
selectedResources,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const maxValue = getMaxBarValue(filteredData, stacked);
|
|
476
560
|
|
|
477
561
|
const { unitLabel, roundReferenceValue, rechartsData } =
|
|
478
|
-
computeUnitLabelAndRoundReferenceValue(
|
|
562
|
+
computeUnitLabelAndRoundReferenceValue(filteredData, maxValue, unitRange);
|
|
479
563
|
|
|
480
564
|
return {
|
|
481
|
-
rechartsBars,
|
|
565
|
+
rechartsBars: filteredRechartsBars,
|
|
482
566
|
unitLabel,
|
|
483
567
|
roundReferenceValue,
|
|
484
568
|
rechartsData,
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
import { useChartLegend } from './ChartLegendWrapper';
|
|
3
|
+
import { Text } from '../text/Text.component';
|
|
4
|
+
import { chartColors } from '../../style/theme';
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
type ChartLegendProps = {
|
|
8
|
+
shape: 'line' | 'rectangle';
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
direction?: 'horizontal' | 'vertical';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const Legend = styled.div<{ direction: 'horizontal' | 'vertical' }>`
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: ${({ direction }) =>
|
|
16
|
+
direction === 'horizontal' ? 'row' : 'column'};
|
|
17
|
+
gap: ${({ direction }) => (direction === 'horizontal' ? '16px' : '8px')};
|
|
18
|
+
flex-wrap: wrap;
|
|
19
|
+
`;
|
|
20
|
+
|
|
21
|
+
const LegendItem = styled.div<{ disabled?: boolean; selected?: boolean }>`
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: 8px;
|
|
25
|
+
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
|
|
26
|
+
opacity: ${({ selected, disabled }) => (disabled ? 0.5 : selected ? 1 : 0.7)};
|
|
27
|
+
transition: opacity 0.2s ease;
|
|
28
|
+
|
|
29
|
+
&:hover {
|
|
30
|
+
opacity: ${({ disabled }) => (disabled ? 0.5 : 1)};
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const LegendShape = styled.div<{
|
|
35
|
+
color?: string;
|
|
36
|
+
shape: 'line' | 'rectangle';
|
|
37
|
+
chartColors: Record<string, string>;
|
|
38
|
+
}>`
|
|
39
|
+
${({ shape, color, chartColors }) => {
|
|
40
|
+
if (shape === 'line') {
|
|
41
|
+
return `
|
|
42
|
+
width: 20px;
|
|
43
|
+
height: 2px;
|
|
44
|
+
background-color: ${chartColors[color as keyof typeof chartColors] || color};
|
|
45
|
+
`;
|
|
46
|
+
} else if (shape === 'rectangle') {
|
|
47
|
+
return `
|
|
48
|
+
width: 12px;
|
|
49
|
+
height: 12px;
|
|
50
|
+
background-color: ${chartColors[color as keyof typeof chartColors] || color};
|
|
51
|
+
border-radius: 2px;
|
|
52
|
+
`;
|
|
53
|
+
} else {
|
|
54
|
+
console.error(
|
|
55
|
+
'The shape is not valid. Please use "line" or "rectangle".',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}}
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
export const ChartLegend = ({
|
|
62
|
+
shape,
|
|
63
|
+
disabled = false,
|
|
64
|
+
direction = 'horizontal',
|
|
65
|
+
}: ChartLegendProps) => {
|
|
66
|
+
const {
|
|
67
|
+
listResources,
|
|
68
|
+
getColor,
|
|
69
|
+
isSelected,
|
|
70
|
+
addSelectedResource,
|
|
71
|
+
removeSelectedResource,
|
|
72
|
+
} = useChartLegend();
|
|
73
|
+
|
|
74
|
+
const resources = listResources();
|
|
75
|
+
|
|
76
|
+
const handleLegendClick = useCallback(
|
|
77
|
+
(resource: string) => {
|
|
78
|
+
if (disabled) return;
|
|
79
|
+
|
|
80
|
+
if (isSelected(resource)) {
|
|
81
|
+
removeSelectedResource(resource);
|
|
82
|
+
} else {
|
|
83
|
+
addSelectedResource(resource);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
[disabled, isSelected, addSelectedResource, removeSelectedResource],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Legend direction={direction}>
|
|
91
|
+
{resources.map((resource) => {
|
|
92
|
+
const color = getColor(resource);
|
|
93
|
+
const selected = isSelected(resource);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<LegendItem
|
|
97
|
+
key={resource}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
selected={selected}
|
|
100
|
+
onClick={() => handleLegendClick(resource)}
|
|
101
|
+
>
|
|
102
|
+
<LegendShape
|
|
103
|
+
color={color}
|
|
104
|
+
shape={shape}
|
|
105
|
+
chartColors={chartColors}
|
|
106
|
+
/>
|
|
107
|
+
<Text variant="Basic">{resource}</Text>
|
|
108
|
+
</LegendItem>
|
|
109
|
+
);
|
|
110
|
+
})}
|
|
111
|
+
</Legend>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createContext, useContext, useState, ReactNode, useMemo, useCallback } from 'react';
|
|
2
|
+
import { ChartColors } from '../../style/theme';
|
|
3
|
+
|
|
4
|
+
export type ChartLegendState = {
|
|
5
|
+
selectedResources: string[];
|
|
6
|
+
addSelectedResource: (resource: string) => void;
|
|
7
|
+
removeSelectedResource: (resource: string) => void;
|
|
8
|
+
isSelected: (resource: string) => boolean;
|
|
9
|
+
getColor: (resource: string) => string | undefined;
|
|
10
|
+
listResources: () => string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const ChartLegendContext = createContext<ChartLegendState | null>(null);
|
|
14
|
+
|
|
15
|
+
export type ChartLegendWrapperProps = {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
colorSet: Record<string, ChartColors | string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const ChartLegendWrapper = ({
|
|
21
|
+
children,
|
|
22
|
+
colorSet,
|
|
23
|
+
}: ChartLegendWrapperProps) => {
|
|
24
|
+
const [selectedResources, setSelectedResources] = useState<string[]>([]);
|
|
25
|
+
|
|
26
|
+
const addSelectedResource = useCallback((resource: string) => {
|
|
27
|
+
setSelectedResources((prev) =>
|
|
28
|
+
prev.includes(resource) ? prev : [...prev, resource],
|
|
29
|
+
);
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const removeSelectedResource = useCallback((resource: string) => {
|
|
33
|
+
setSelectedResources((prev) => prev.filter((r) => r !== resource));
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const isSelected = useCallback((resource: string) => {
|
|
37
|
+
return selectedResources.includes(resource);
|
|
38
|
+
}, [selectedResources]);
|
|
39
|
+
|
|
40
|
+
const getColor = useCallback((resource: string) => {
|
|
41
|
+
const color = colorSet[resource];
|
|
42
|
+
if (!color) {
|
|
43
|
+
console.warn(
|
|
44
|
+
`ChartLegendWrapper: No color defined for resource "${resource}"`,
|
|
45
|
+
);
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return color;
|
|
49
|
+
}, [colorSet]);
|
|
50
|
+
|
|
51
|
+
const listResources = useCallback(() => {
|
|
52
|
+
return Object.keys(colorSet);
|
|
53
|
+
}, [colorSet]);
|
|
54
|
+
|
|
55
|
+
const chartLegendState = useMemo(() => ({
|
|
56
|
+
selectedResources,
|
|
57
|
+
addSelectedResource,
|
|
58
|
+
removeSelectedResource,
|
|
59
|
+
isSelected,
|
|
60
|
+
getColor,
|
|
61
|
+
listResources,
|
|
62
|
+
}), [
|
|
63
|
+
selectedResources,
|
|
64
|
+
addSelectedResource,
|
|
65
|
+
removeSelectedResource,
|
|
66
|
+
isSelected,
|
|
67
|
+
getColor,
|
|
68
|
+
listResources,
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<ChartLegendContext.Provider value={chartLegendState}>
|
|
73
|
+
{children}
|
|
74
|
+
</ChartLegendContext.Provider>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Hook for accessing legend state in custom components
|
|
79
|
+
export const useChartLegend = () => {
|
|
80
|
+
const context = useContext(ChartLegendContext);
|
|
81
|
+
if (!context) {
|
|
82
|
+
throw new Error('useChartLegend must be used within a ChartLegendWrapper');
|
|
83
|
+
}
|
|
84
|
+
return context;
|
|
85
|
+
};
|
|
@@ -214,4 +214,28 @@ describe('FormatttedDateTime', () => {
|
|
|
214
214
|
//V
|
|
215
215
|
expect(screen.getByText('2022-12-12 11:57:26')).toBeInTheDocument();
|
|
216
216
|
});
|
|
217
|
+
|
|
218
|
+
it('should display the date in the expected format of the xaxis tick in the chart', () => {
|
|
219
|
+
//S
|
|
220
|
+
render(
|
|
221
|
+
<FormattedDateTime
|
|
222
|
+
format="day-month-abbreviated-hour-minute"
|
|
223
|
+
value={new Date('2022-10-06T18:33:00Z')}
|
|
224
|
+
/>,
|
|
225
|
+
);
|
|
226
|
+
//V
|
|
227
|
+
expect(screen.getByText('6 Oct 18:33')).toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should display the date in the expected format of date in the chart', () => {
|
|
231
|
+
//S
|
|
232
|
+
render(
|
|
233
|
+
<FormattedDateTime
|
|
234
|
+
format="day-month-abbreviated-hour-minute-second"
|
|
235
|
+
value={new Date('2022-10-06T18:33:00Z')}
|
|
236
|
+
/>,
|
|
237
|
+
);
|
|
238
|
+
//V
|
|
239
|
+
expect(screen.getByText('6 Oct 18:33:00')).toBeInTheDocument();
|
|
240
|
+
});
|
|
217
241
|
});
|
|
@@ -27,6 +27,26 @@ export const TIME_FORMATER = Intl.DateTimeFormat('en-GB', {
|
|
|
27
27
|
minute: '2-digit',
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
export const DAY_MONTH_ABBREVIATED_HOUR_MINUTE_SECOND = Intl.DateTimeFormat(
|
|
31
|
+
'en-GB',
|
|
32
|
+
{
|
|
33
|
+
day: 'numeric',
|
|
34
|
+
month: 'short',
|
|
35
|
+
hour: '2-digit',
|
|
36
|
+
minute: '2-digit',
|
|
37
|
+
second: '2-digit',
|
|
38
|
+
hour12: false,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
export const DAY_MONTH_ABBREVIATED_HOUR_MINUTE = Intl.DateTimeFormat('en-GB', {
|
|
43
|
+
day: 'numeric',
|
|
44
|
+
month: 'short',
|
|
45
|
+
hour: '2-digit',
|
|
46
|
+
minute: '2-digit',
|
|
47
|
+
hour12: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
30
50
|
type FormattedDateTimeProps = {
|
|
31
51
|
format:
|
|
32
52
|
| 'date'
|
|
@@ -34,7 +54,9 @@ type FormattedDateTimeProps = {
|
|
|
34
54
|
| 'date-time-second'
|
|
35
55
|
| 'time'
|
|
36
56
|
| 'time-second'
|
|
37
|
-
| 'relative'
|
|
57
|
+
| 'relative'
|
|
58
|
+
| 'day-month-abbreviated-hour-minute'
|
|
59
|
+
| 'day-month-abbreviated-hour-minute-second';
|
|
38
60
|
value: Date;
|
|
39
61
|
};
|
|
40
62
|
|
|
@@ -149,7 +171,19 @@ export const FormattedDateTime = ({
|
|
|
149
171
|
few seconds ago
|
|
150
172
|
</Tooltip>
|
|
151
173
|
);
|
|
152
|
-
|
|
174
|
+
case 'day-month-abbreviated-hour-minute':
|
|
175
|
+
return (
|
|
176
|
+
<>{DAY_MONTH_ABBREVIATED_HOUR_MINUTE.format(value).replace(',', '')}</>
|
|
177
|
+
);
|
|
178
|
+
case 'day-month-abbreviated-hour-minute-second':
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
{DAY_MONTH_ABBREVIATED_HOUR_MINUTE_SECOND.format(value).replace(
|
|
182
|
+
',',
|
|
183
|
+
'',
|
|
184
|
+
)}
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
153
187
|
default:
|
|
154
188
|
return <></>;
|
|
155
189
|
}
|
|
@@ -2,27 +2,27 @@ import {
|
|
|
2
2
|
HealthSelector,
|
|
3
3
|
optionsDefaultConfiguration,
|
|
4
4
|
} from './HealthSelector.component';
|
|
5
|
-
import
|
|
6
|
-
import { render, screen } from '@testing-library/react';
|
|
5
|
+
import { act } from 'react';
|
|
6
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
7
7
|
import userEvent from '@testing-library/user-event';
|
|
8
|
-
import { QueryClient, QueryClientProvider } from 'react-query';
|
|
9
8
|
import { getWrapper } from '../../testUtils';
|
|
10
9
|
describe('HealthSelector', () => {
|
|
11
|
-
it('should display correctly without any props and select first option', () => {
|
|
10
|
+
it('should display correctly without any props and select first option', async () => {
|
|
12
11
|
const { Wrapper } = getWrapper();
|
|
13
12
|
const { getByText } = render(
|
|
14
13
|
<Wrapper>
|
|
15
14
|
<HealthSelector id="health" onChange={() => {}} />
|
|
16
15
|
</Wrapper>,
|
|
17
16
|
);
|
|
17
|
+
await waitFor(() => screen.findByRole('img', { hidden: true }));
|
|
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
|
});
|