@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.
Files changed (33) hide show
  1. package/__mocks__/uuid.js +11 -0
  2. package/dist/components/barchartv2/Barchart.component.js +2 -2
  3. package/dist/components/chartlegend/ChartLegend.d.ts +3 -1
  4. package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -1
  5. package/dist/components/chartlegend/ChartLegend.js +2 -2
  6. package/dist/components/chartlegend/ChartLegendWrapper.d.ts +3 -1
  7. package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -1
  8. package/dist/components/chartlegend/ChartLegendWrapper.js +43 -9
  9. package/dist/components/icon/Icon.component.d.ts +2 -0
  10. package/dist/components/icon/Icon.component.d.ts.map +1 -1
  11. package/dist/components/icon/Icon.component.js +2 -0
  12. package/dist/components/linetemporalchart/ChartUtil.d.ts.map +1 -1
  13. package/dist/components/linetemporalchart/ChartUtil.js +12 -0
  14. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +10 -5
  15. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
  16. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +84 -49
  17. package/dist/components/text/Text.component.d.ts +2 -1
  18. package/dist/components/text/Text.component.d.ts.map +1 -1
  19. package/dist/next.d.ts +1 -1
  20. package/dist/next.d.ts.map +1 -1
  21. package/dist/next.js +1 -1
  22. package/package.json +3 -1
  23. package/src/lib/components/barchartv2/Barchart.component.tsx +2 -2
  24. package/src/lib/components/chartlegend/ChartLegend.tsx +4 -2
  25. package/src/lib/components/chartlegend/ChartLegendWrapper.test.tsx +197 -0
  26. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +65 -9
  27. package/src/lib/components/icon/Icon.component.tsx +2 -0
  28. package/src/lib/components/linetemporalchart/ChartUtil.ts +26 -0
  29. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +227 -157
  30. package/src/lib/components/text/Text.component.tsx +8 -1
  31. package/src/lib/next.ts +4 -1
  32. package/stories/BarChart/barchart.stories.tsx +7 -1
  33. 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="Basic">{resource}</Text>
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: Record<string, ChartColors | string>;
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 allResources = Object.keys(colorSet);
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(allResources);
50
- }, [allResources]);
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 = colorSet[resource];
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
- [colorSet],
132
+ [internalColorSet],
78
133
  );
79
134
 
80
135
  const listResources = useCallback(() => {
81
- return Object.keys(colorSet);
82
- }, [colorSet]);
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) {
@@ -140,6 +140,8 @@ export const iconTable = {
140
140
  Stop: 'fas faStop',
141
141
  Play: 'fas faPlay',
142
142
  Mail: 'fas faEnvelope',
143
+ ThumbsUp: 'fas faThumbsUp',
144
+ ThumbsDown: 'fas faThumbsDown',
143
145
  };
144
146
 
145
147
  type IconProps = {
@@ -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) => {