@scality/core-ui 0.170.0 → 0.171.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/__mocks__/uuid.js +11 -0
- package/dist/components/barchartv2/Barchart.component.js +2 -2
- package/dist/components/chartlegend/ChartLegend.d.ts +3 -1
- package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -1
- package/dist/components/chartlegend/ChartLegend.js +2 -2
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts +3 -1
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -1
- package/dist/components/chartlegend/ChartLegendWrapper.js +43 -9
- package/dist/components/icon/Icon.component.d.ts +2 -0
- package/dist/components/icon/Icon.component.d.ts.map +1 -1
- package/dist/components/icon/Icon.component.js +2 -0
- package/dist/components/linetemporalchart/ChartUtil.d.ts.map +1 -1
- package/dist/components/linetemporalchart/ChartUtil.js +12 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +10 -5
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +84 -49
- package/dist/components/text/Text.component.d.ts +2 -1
- package/dist/components/text/Text.component.d.ts.map +1 -1
- package/dist/next.d.ts +1 -1
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +1 -1
- package/package.json +3 -1
- package/src/lib/components/barchartv2/Barchart.component.tsx +2 -2
- package/src/lib/components/chartlegend/ChartLegend.tsx +4 -2
- package/src/lib/components/chartlegend/ChartLegendWrapper.test.tsx +197 -0
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +65 -9
- package/src/lib/components/icon/Icon.component.tsx +2 -0
- package/src/lib/components/linetemporalchart/ChartUtil.ts +26 -0
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +227 -157
- package/src/lib/components/text/Text.component.tsx +8 -1
- package/src/lib/next.ts +4 -1
- package/stories/BarChart/barchart.stories.tsx +7 -1
- package/stories/linetimeseriechart.stories.tsx +217 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
2
|
import { useChartLegend } from './ChartLegendWrapper';
|
|
3
|
-
import { Text } from '../text/Text.component';
|
|
3
|
+
import { Text, TextVariant } from '../text/Text.component';
|
|
4
4
|
import { chartColors } from '../../style/theme';
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ type ChartLegendProps = {
|
|
|
8
8
|
shape: 'line' | 'rectangle';
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
direction?: 'horizontal' | 'vertical';
|
|
11
|
+
legendSize?: TextVariant;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
const Legend = styled.div<{ direction: 'horizontal' | 'vertical' }>`
|
|
@@ -65,6 +66,7 @@ export const ChartLegend = ({
|
|
|
65
66
|
shape,
|
|
66
67
|
disabled = false,
|
|
67
68
|
direction = 'horizontal',
|
|
69
|
+
legendSize = 'Basic',
|
|
68
70
|
}: ChartLegendProps) => {
|
|
69
71
|
const {
|
|
70
72
|
listResources,
|
|
@@ -132,7 +134,7 @@ export const ChartLegend = ({
|
|
|
132
134
|
shape={shape}
|
|
133
135
|
chartColors={chartColors}
|
|
134
136
|
/>
|
|
135
|
-
<Text variant=
|
|
137
|
+
<Text variant={legendSize}>{resource}</Text>
|
|
136
138
|
</LegendItem>
|
|
137
139
|
);
|
|
138
140
|
})}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import React, { useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
ChartLegendWrapper,
|
|
5
|
+
useChartId,
|
|
6
|
+
useChartLegend,
|
|
7
|
+
} from './ChartLegendWrapper';
|
|
8
|
+
import { ChartLegend } from './ChartLegend';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
|
|
11
|
+
describe('ChartLegendWrapper', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const TestChart = ({ seriesNames }: { seriesNames: string[] }) => {
|
|
17
|
+
const chartId = useChartId();
|
|
18
|
+
const { register } = useChartLegend();
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
register(chartId, seriesNames);
|
|
22
|
+
}, [chartId, register, seriesNames]);
|
|
23
|
+
|
|
24
|
+
return <div data-testid={`chart-${chartId}`}>Test Chart</div>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const generateColors = (seriesNames: string[]) => {
|
|
28
|
+
const colors: Record<string, string> = {};
|
|
29
|
+
const colorPalette = ['red', 'blue', 'green', 'yellow', 'purple'];
|
|
30
|
+
seriesNames.forEach((name, index) => {
|
|
31
|
+
colors[name] = colorPalette[index % colorPalette.length];
|
|
32
|
+
});
|
|
33
|
+
return colors;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Dynamic Color Generation', () => {
|
|
37
|
+
it('should generate colors dynamically based on registered series', () => {
|
|
38
|
+
render(
|
|
39
|
+
<ChartLegendWrapper colorSet={generateColors}>
|
|
40
|
+
<TestChart seriesNames={['CPU', 'Memory']} />
|
|
41
|
+
<ChartLegend shape="line" />
|
|
42
|
+
</ChartLegendWrapper>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
46
|
+
expect(screen.getByText('Memory')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
48
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should handle multiple charts with overlapping series', () => {
|
|
52
|
+
const TestChart1 = () => {
|
|
53
|
+
const chartId = useChartId();
|
|
54
|
+
const { register } = useChartLegend();
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
register(chartId, ['CPU', 'Memory']);
|
|
58
|
+
}, [chartId, register]);
|
|
59
|
+
|
|
60
|
+
return <div data-testid={`chart1-${chartId}`}>Test Chart 1</div>;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const TestChart2 = () => {
|
|
64
|
+
const chartId = useChartId();
|
|
65
|
+
const { register } = useChartLegend();
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
register(chartId, ['CPU', 'Disk']);
|
|
69
|
+
}, [chartId, register]);
|
|
70
|
+
|
|
71
|
+
return <div data-testid={`chart2-${chartId}`}>Test Chart 2</div>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
render(
|
|
75
|
+
<ChartLegendWrapper colorSet={generateColors}>
|
|
76
|
+
<TestChart1 />
|
|
77
|
+
<TestChart2 />
|
|
78
|
+
<ChartLegend shape="line" />
|
|
79
|
+
</ChartLegendWrapper>,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Should show unique series from both charts
|
|
83
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
84
|
+
expect(screen.getByText('Memory')).toBeInTheDocument();
|
|
85
|
+
expect(screen.getByText('Disk')).toBeInTheDocument();
|
|
86
|
+
|
|
87
|
+
// All should be selected by default
|
|
88
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
89
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
90
|
+
expect(screen.getByLabelText('Disk selected')).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle empty series registration', () => {
|
|
94
|
+
render(
|
|
95
|
+
<ChartLegendWrapper colorSet={generateColors}>
|
|
96
|
+
<TestChart seriesNames={[]} />
|
|
97
|
+
<ChartLegend shape="line" />
|
|
98
|
+
</ChartLegendWrapper>,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Should not crash and should render empty legend
|
|
102
|
+
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should maintain selection state when new series are added', () => {
|
|
106
|
+
const { rerender } = render(
|
|
107
|
+
<ChartLegendWrapper colorSet={generateColors}>
|
|
108
|
+
<TestChart seriesNames={['CPU']} />
|
|
109
|
+
<ChartLegend shape="line" />
|
|
110
|
+
</ChartLegendWrapper>,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Initially only CPU
|
|
114
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
115
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
116
|
+
|
|
117
|
+
// Select only CPU
|
|
118
|
+
userEvent.click(screen.getByText('CPU'));
|
|
119
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
120
|
+
|
|
121
|
+
// Add more series
|
|
122
|
+
rerender(
|
|
123
|
+
<ChartLegendWrapper colorSet={generateColors}>
|
|
124
|
+
<TestChart seriesNames={['CPU', 'Memory']} />
|
|
125
|
+
<ChartLegend shape="line" />
|
|
126
|
+
</ChartLegendWrapper>,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// New series should be added and all should be selected (reset behavior)
|
|
130
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
131
|
+
expect(screen.getByText('Memory')).toBeInTheDocument();
|
|
132
|
+
expect(screen.getByLabelText('CPU selected')).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByLabelText('Memory selected')).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should work with different chart configurations', () => {
|
|
137
|
+
render(
|
|
138
|
+
<ChartLegendWrapper colorSet={generateColors}>
|
|
139
|
+
<TestChart seriesNames={['Series1', 'Series2', 'Series3']} />
|
|
140
|
+
<ChartLegend shape="rectangle" direction="vertical" />
|
|
141
|
+
</ChartLegendWrapper>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(screen.getByText('Series1')).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByText('Series2')).toBeInTheDocument();
|
|
146
|
+
expect(screen.getByText('Series3')).toBeInTheDocument();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('Static Color Set', () => {
|
|
151
|
+
const staticColorSet = {
|
|
152
|
+
CPU: 'red',
|
|
153
|
+
Memory: 'blue',
|
|
154
|
+
Disk: 'green',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
it('should work with static color sets', () => {
|
|
158
|
+
render(
|
|
159
|
+
<ChartLegendWrapper colorSet={staticColorSet}>
|
|
160
|
+
<ChartLegend shape="line" />
|
|
161
|
+
</ChartLegendWrapper>,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
165
|
+
expect(screen.getByText('Memory')).toBeInTheDocument();
|
|
166
|
+
expect(screen.getByText('Disk')).toBeInTheDocument();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should ignore registration when using static color sets', () => {
|
|
170
|
+
render(
|
|
171
|
+
<ChartLegendWrapper colorSet={staticColorSet}>
|
|
172
|
+
<TestChart seriesNames={['DifferentSeries']} />
|
|
173
|
+
<ChartLegend shape="line" />
|
|
174
|
+
</ChartLegendWrapper>,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
// Should only show static color set items, not registered series
|
|
178
|
+
expect(screen.getByText('CPU')).toBeInTheDocument();
|
|
179
|
+
expect(screen.getByText('Memory')).toBeInTheDocument();
|
|
180
|
+
expect(screen.getByText('Disk')).toBeInTheDocument();
|
|
181
|
+
expect(screen.queryByText('DifferentSeries')).not.toBeInTheDocument();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('Error Handling', () => {
|
|
186
|
+
it('should throw error when useChartLegend is used outside wrapper', () => {
|
|
187
|
+
const TestComponent = () => {
|
|
188
|
+
useChartLegend();
|
|
189
|
+
return <div>Test</div>;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
expect(() => render(<TestComponent />)).toThrow(
|
|
193
|
+
'useChartLegend must be used within a ChartLegendWrapper',
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -5,9 +5,22 @@ import {
|
|
|
5
5
|
ReactNode,
|
|
6
6
|
useMemo,
|
|
7
7
|
useCallback,
|
|
8
|
+
useEffect,
|
|
9
|
+
useRef,
|
|
8
10
|
} from 'react';
|
|
11
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
9
12
|
import { ChartColors } from '../../style/theme';
|
|
10
13
|
|
|
14
|
+
export const useChartId = (): string => {
|
|
15
|
+
const idRef = useRef<string | null>(null);
|
|
16
|
+
|
|
17
|
+
if (idRef.current === null) {
|
|
18
|
+
idRef.current = uuidv4();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return idRef.current;
|
|
22
|
+
};
|
|
23
|
+
|
|
11
24
|
export type ChartLegendState = {
|
|
12
25
|
selectedResources: string[];
|
|
13
26
|
addSelectedResource: (resource: string) => void;
|
|
@@ -18,23 +31,65 @@ export type ChartLegendState = {
|
|
|
18
31
|
getColor: (resource: string) => string | undefined;
|
|
19
32
|
listResources: () => string[];
|
|
20
33
|
isOnlyOneSelected: () => boolean;
|
|
34
|
+
register: (chartId: string, seriesNames: string[]) => void;
|
|
21
35
|
};
|
|
22
36
|
|
|
23
37
|
const ChartLegendContext = createContext<ChartLegendState | null>(null);
|
|
24
38
|
|
|
25
39
|
export type ChartLegendWrapperProps = {
|
|
26
40
|
children: ReactNode;
|
|
27
|
-
colorSet:
|
|
41
|
+
colorSet:
|
|
42
|
+
| Record<string, ChartColors | string>
|
|
43
|
+
| ((seriesNames: string[]) => Record<string, ChartColors | string>);
|
|
28
44
|
};
|
|
29
45
|
|
|
30
46
|
export const ChartLegendWrapper = ({
|
|
31
47
|
children,
|
|
32
48
|
colorSet,
|
|
33
49
|
}: ChartLegendWrapperProps) => {
|
|
34
|
-
const
|
|
50
|
+
const [registeredColorSets, setRegisteredColorSets] = useState<
|
|
51
|
+
Record<string, string[]>
|
|
52
|
+
>({});
|
|
53
|
+
|
|
54
|
+
const [internalColorSet, setInternalColorSet] = useState<
|
|
55
|
+
Record<string, ChartColors | string>
|
|
56
|
+
>(() => {
|
|
57
|
+
return typeof colorSet === 'function' ? {} : colorSet;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (typeof colorSet === 'function') {
|
|
62
|
+
const allUniqueSeriesNames = Array.from(
|
|
63
|
+
new Set(Object.values(registeredColorSets).flat()),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (allUniqueSeriesNames.length > 0) {
|
|
67
|
+
const newColorSet = colorSet(allUniqueSeriesNames);
|
|
68
|
+
setInternalColorSet(newColorSet);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
setInternalColorSet(colorSet);
|
|
72
|
+
}
|
|
73
|
+
}, [registeredColorSets, colorSet]);
|
|
74
|
+
|
|
75
|
+
const allResources = useMemo(
|
|
76
|
+
() => Object.keys(internalColorSet),
|
|
77
|
+
[internalColorSet],
|
|
78
|
+
);
|
|
35
79
|
const [selectedResources, setSelectedResources] =
|
|
36
80
|
useState<string[]>(allResources);
|
|
37
81
|
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
setSelectedResources(allResources);
|
|
84
|
+
}, [allResources]);
|
|
85
|
+
|
|
86
|
+
const register = useCallback((chartId: string, seriesNames: string[]) => {
|
|
87
|
+
setRegisteredColorSets((prev) => ({
|
|
88
|
+
...prev,
|
|
89
|
+
[chartId]: seriesNames,
|
|
90
|
+
}));
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
38
93
|
const addSelectedResource = useCallback((resource: string) => {
|
|
39
94
|
setSelectedResources((prev) =>
|
|
40
95
|
prev.includes(resource) ? prev : [...prev, resource],
|
|
@@ -46,8 +101,8 @@ export const ChartLegendWrapper = ({
|
|
|
46
101
|
}, []);
|
|
47
102
|
|
|
48
103
|
const selectAllResources = useCallback(() => {
|
|
49
|
-
setSelectedResources(
|
|
50
|
-
}, [
|
|
104
|
+
setSelectedResources(Object.keys(internalColorSet));
|
|
105
|
+
}, [internalColorSet]);
|
|
51
106
|
|
|
52
107
|
const selectOnlyResource = useCallback((resource: string) => {
|
|
53
108
|
setSelectedResources([resource]);
|
|
@@ -65,7 +120,7 @@ export const ChartLegendWrapper = ({
|
|
|
65
120
|
|
|
66
121
|
const getColor = useCallback(
|
|
67
122
|
(resource: string) => {
|
|
68
|
-
const color =
|
|
123
|
+
const color = internalColorSet[resource];
|
|
69
124
|
if (!color) {
|
|
70
125
|
console.warn(
|
|
71
126
|
`ChartLegendWrapper: No color defined for resource "${resource}"`,
|
|
@@ -74,12 +129,12 @@ export const ChartLegendWrapper = ({
|
|
|
74
129
|
}
|
|
75
130
|
return color;
|
|
76
131
|
},
|
|
77
|
-
[
|
|
132
|
+
[internalColorSet],
|
|
78
133
|
);
|
|
79
134
|
|
|
80
135
|
const listResources = useCallback(() => {
|
|
81
|
-
return Object.keys(
|
|
82
|
-
}, [
|
|
136
|
+
return Object.keys(internalColorSet);
|
|
137
|
+
}, [internalColorSet]);
|
|
83
138
|
|
|
84
139
|
const chartLegendState = useMemo(
|
|
85
140
|
() => ({
|
|
@@ -92,6 +147,7 @@ export const ChartLegendWrapper = ({
|
|
|
92
147
|
getColor,
|
|
93
148
|
listResources,
|
|
94
149
|
isOnlyOneSelected,
|
|
150
|
+
register,
|
|
95
151
|
}),
|
|
96
152
|
[
|
|
97
153
|
selectedResources,
|
|
@@ -103,6 +159,7 @@ export const ChartLegendWrapper = ({
|
|
|
103
159
|
getColor,
|
|
104
160
|
listResources,
|
|
105
161
|
isOnlyOneSelected,
|
|
162
|
+
register,
|
|
106
163
|
],
|
|
107
164
|
);
|
|
108
165
|
|
|
@@ -113,7 +170,6 @@ export const ChartLegendWrapper = ({
|
|
|
113
170
|
);
|
|
114
171
|
};
|
|
115
172
|
|
|
116
|
-
// Hook for accessing legend state in custom components
|
|
117
173
|
export const useChartLegend = () => {
|
|
118
174
|
const context = useContext(ChartLegendContext);
|
|
119
175
|
if (!context) {
|
|
@@ -148,8 +148,24 @@ export function addMissingDataPoint(
|
|
|
148
148
|
|
|
149
149
|
const newValues: [number, number | string | null][] = [];
|
|
150
150
|
|
|
151
|
+
// add missing data points for the starting time
|
|
152
|
+
for (
|
|
153
|
+
let i = startingTimeStamp;
|
|
154
|
+
i < orginalValues[0][0];
|
|
155
|
+
i += sampleInterval
|
|
156
|
+
) {
|
|
157
|
+
newValues.push([i, NAN_STRING]);
|
|
158
|
+
}
|
|
159
|
+
|
|
151
160
|
// Process all but the last element
|
|
152
161
|
for (let i = 0; i < orginalValues.length - 1; i++) {
|
|
162
|
+
if (
|
|
163
|
+
orginalValues[i][0] < startingTimeStamp ||
|
|
164
|
+
orginalValues[i][0] > startingTimeStamp + sampleDuration
|
|
165
|
+
) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
153
169
|
// Always add the current data point
|
|
154
170
|
newValues.push(orginalValues[i]);
|
|
155
171
|
|
|
@@ -170,8 +186,18 @@ export function addMissingDataPoint(
|
|
|
170
186
|
// Add the last element
|
|
171
187
|
newValues.push(orginalValues[orginalValues.length - 1]);
|
|
172
188
|
|
|
189
|
+
// add missing data points for the ending time
|
|
190
|
+
for (
|
|
191
|
+
let i = orginalValues[orginalValues.length - 1][0] + sampleInterval;
|
|
192
|
+
i < startingTimeStamp + sampleDuration;
|
|
193
|
+
i += sampleInterval
|
|
194
|
+
) {
|
|
195
|
+
newValues.push([i, NAN_STRING]);
|
|
196
|
+
}
|
|
197
|
+
|
|
173
198
|
return newValues;
|
|
174
199
|
}
|
|
200
|
+
|
|
175
201
|
// get the value for the based value
|
|
176
202
|
// TODO: We need to handle the negative value in the future
|
|
177
203
|
export const getRelativeValue = (value: number, base: number) => {
|