@scality/core-ui 0.161.0 → 0.163.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 (173) hide show
  1. package/README.md +15 -15
  2. package/dist/components/accordion/Accordion.component.d.ts +0 -1
  3. package/dist/components/accordion/Accordion.component.d.ts.map +1 -1
  4. package/dist/components/barchartv2/Barchart.component.d.ts +53 -0
  5. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -0
  6. package/dist/components/barchartv2/Barchart.component.js +86 -0
  7. package/dist/components/barchartv2/utils.d.ts +118 -0
  8. package/dist/components/barchartv2/utils.d.ts.map +1 -0
  9. package/dist/components/barchartv2/utils.js +337 -0
  10. package/dist/components/buttonv2/Buttonv2.component.d.ts +1 -1
  11. package/dist/components/buttonv2/Buttonv2.component.d.ts.map +1 -1
  12. package/dist/components/chartlegend/ChartLegend.d.ts +8 -0
  13. package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -0
  14. package/dist/components/chartlegend/ChartLegend.js +65 -0
  15. package/dist/components/chartlegend/ChartLegendWrapper.d.ts +17 -0
  16. package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -0
  17. package/dist/components/chartlegend/ChartLegendWrapper.js +50 -0
  18. package/dist/components/constrainedtext/Constrainedtext.component.d.ts +2 -1
  19. package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
  20. package/dist/components/constrainedtext/Constrainedtext.component.js +5 -4
  21. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts +0 -1
  22. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts.map +1 -1
  23. package/dist/components/date/FormattedDateTime.d.ts +4 -1
  24. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  25. package/dist/components/date/FormattedDateTime.js +24 -1
  26. package/dist/components/date/FormattedDateTime.spec.js +12 -0
  27. package/dist/components/emptytable/Emptytable.component.d.ts +0 -1
  28. package/dist/components/emptytable/Emptytable.component.d.ts.map +1 -1
  29. package/dist/components/emptytable/Emptytable.component.js +1 -0
  30. package/dist/components/error-pages/ErrorPage401.component.d.ts +0 -1
  31. package/dist/components/error-pages/ErrorPage401.component.d.ts.map +1 -1
  32. package/dist/components/error-pages/ErrorPage404.component.d.ts +0 -1
  33. package/dist/components/error-pages/ErrorPage404.component.d.ts.map +1 -1
  34. package/dist/components/error-pages/ErrorPage500.component.d.ts +0 -1
  35. package/dist/components/error-pages/ErrorPage500.component.d.ts.map +1 -1
  36. package/dist/components/error-pages/ErrorPageAuth.component.d.ts.map +1 -1
  37. package/dist/components/form/Form.component.d.ts +2 -2
  38. package/dist/components/form/Form.component.d.ts.map +1 -1
  39. package/dist/components/icon/Icon.component.d.ts +5 -5
  40. package/dist/components/icon/Icon.component.d.ts.map +1 -1
  41. package/dist/components/icon/Icon.component.js +33 -31
  42. package/dist/components/infomessage/InfoMessage.component.d.ts +0 -1
  43. package/dist/components/infomessage/InfoMessage.component.d.ts.map +1 -1
  44. package/dist/components/lateralnavbarlayout/LateralNavbarLayout.component.d.ts.map +1 -1
  45. package/dist/components/layout/Layout.component.d.ts.map +1 -1
  46. package/dist/components/layout/v2/panels.d.ts.map +1 -1
  47. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +33 -0
  48. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -0
  49. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +249 -0
  50. package/dist/components/modal/Modal.component.js +2 -2
  51. package/dist/components/navbar/Navbar.component.js +2 -2
  52. package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts +0 -1
  53. package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts.map +1 -1
  54. package/dist/components/searchinput/SearchInput.component.d.ts +1 -2
  55. package/dist/components/searchinput/SearchInput.component.d.ts.map +1 -1
  56. package/dist/components/selectv2/Selectv2.component.d.ts +5 -5
  57. package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
  58. package/dist/components/selectv2/Selectv2.component.js +11 -6
  59. package/dist/components/statuswrapper/Statuswrapper.component.d.ts +0 -1
  60. package/dist/components/statuswrapper/Statuswrapper.component.d.ts.map +1 -1
  61. package/dist/components/steppers/Stepper.component.d.ts.map +1 -1
  62. package/dist/components/steppers/Stepper.component.js +9 -8
  63. package/dist/components/tablev2/Search.js +2 -2
  64. package/dist/components/tablev2/SingleSelectableContent.d.ts +1 -2
  65. package/dist/components/tablev2/SingleSelectableContent.d.ts.map +1 -1
  66. package/dist/components/tablev2/TableCommon.d.ts +2 -2
  67. package/dist/components/tablev2/TableCommon.d.ts.map +1 -1
  68. package/dist/components/tablev2/TableSync.d.ts +8 -0
  69. package/dist/components/tablev2/TableSync.d.ts.map +1 -0
  70. package/dist/components/tablev2/TableSync.js +11 -0
  71. package/dist/components/tablev2/Tablev2.component.d.ts +2 -1
  72. package/dist/components/tablev2/Tablev2.component.d.ts.map +1 -1
  73. package/dist/components/tablev2/Tablev2.component.js +10 -9
  74. package/dist/components/tabsv2/ScrollButton.d.ts +1 -2
  75. package/dist/components/tabsv2/ScrollButton.d.ts.map +1 -1
  76. package/dist/components/tabsv2/ScrollButton.js +2 -2
  77. package/dist/components/tabsv2/Tabsv2.component.d.ts +2 -2
  78. package/dist/components/tabsv2/Tabsv2.component.d.ts.map +1 -1
  79. package/dist/components/tabsv2/Tabsv2.component.js +2 -2
  80. package/dist/components/text/Text.component.d.ts +0 -1
  81. package/dist/components/text/Text.component.d.ts.map +1 -1
  82. package/dist/components/textarea/TextArea.component.d.ts +3 -3
  83. package/dist/components/textarea/TextArea.component.d.ts.map +1 -1
  84. package/dist/components/textbadge/TextBadge.component.d.ts +0 -1
  85. package/dist/components/textbadge/TextBadge.component.d.ts.map +1 -1
  86. package/dist/components/toast/Toast.component.d.ts +1 -1
  87. package/dist/components/toast/Toast.component.d.ts.map +1 -1
  88. package/dist/components/toast/ToastProvider.d.ts.map +1 -1
  89. package/dist/components/toast/ToastProvider.js +4 -5
  90. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts +1 -2
  91. package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
  92. package/dist/components/vegachartv2/SyncedCursorCharts.js +3 -5
  93. package/dist/components/vegachartv2/VegaChartV2.component.d.ts +1 -2
  94. package/dist/components/vegachartv2/VegaChartV2.component.d.ts.map +1 -1
  95. package/dist/components/vegachartv2/VegaChartV2.component.js +2 -2
  96. package/dist/icons/branding.d.ts.map +1 -1
  97. package/dist/icons/scality-loading.d.ts.map +1 -1
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1 -0
  101. package/dist/next.d.ts +2 -0
  102. package/dist/next.d.ts.map +1 -1
  103. package/dist/next.js +2 -0
  104. package/dist/style/theme.d.ts +20 -0
  105. package/dist/style/theme.d.ts.map +1 -1
  106. package/dist/style/theme.js +46 -1
  107. package/package.json +7 -4
  108. package/setupTests.js +6 -0
  109. package/src/lib/components/accordion/Accordion.component.tsx +1 -1
  110. package/src/lib/components/accordion/Accordion.test.tsx +7 -15
  111. package/src/lib/components/barchartv2/Barchart.component.test.tsx +364 -0
  112. package/src/lib/components/barchartv2/Barchart.component.tsx +321 -0
  113. package/src/lib/components/barchartv2/utils.test.ts +899 -0
  114. package/src/lib/components/barchartv2/utils.ts +534 -0
  115. package/src/lib/components/buttonv2/Buttonv2.component.tsx +1 -1
  116. package/src/lib/components/chartlegend/ChartLegend.tsx +113 -0
  117. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +85 -0
  118. package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +22 -3
  119. package/src/lib/components/coreuithemeprovider/CoreUiThemeProvider.tsx +0 -1
  120. package/src/lib/components/date/FormattedDateTime.spec.tsx +24 -0
  121. package/src/lib/components/date/FormattedDateTime.tsx +42 -2
  122. package/src/lib/components/emptytable/Emptytable.component.tsx +1 -1
  123. package/src/lib/components/error-pages/ErrorPage401.component.tsx +0 -1
  124. package/src/lib/components/error-pages/ErrorPage404.component.tsx +0 -1
  125. package/src/lib/components/error-pages/ErrorPage500.component.tsx +0 -1
  126. package/src/lib/components/error-pages/ErrorPageAuth.component.tsx +0 -1
  127. package/src/lib/components/form/Form.component.tsx +1 -1
  128. package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +3 -3
  129. package/src/lib/components/icon/Icon.component.tsx +48 -60
  130. package/src/lib/components/infomessage/InfoMessage.component.tsx +0 -1
  131. package/src/lib/components/inlineinput/InlineInput.test.tsx +22 -19
  132. package/src/lib/components/inputlist/InputList.test.tsx +21 -19
  133. package/src/lib/components/lateralnavbarlayout/LateralNavbarLayout.component.tsx +0 -1
  134. package/src/lib/components/layout/Layout.component.tsx +0 -1
  135. package/src/lib/components/layout/v2/panels.tsx +1 -1
  136. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +502 -0
  137. package/src/lib/components/modal/Modal.component.tsx +2 -2
  138. package/src/lib/components/navbar/Navbar.component.tsx +2 -2
  139. package/src/lib/components/scrollbarwrapper/ScrollbarWrapper.component.tsx +0 -1
  140. package/src/lib/components/searchinput/SearchInput.component.tsx +0 -1
  141. package/src/lib/components/searchinput/SearchInput.test.tsx +3 -7
  142. package/src/lib/components/selectv2/Selectv2.component.tsx +24 -14
  143. package/src/lib/components/selectv2/selectv2.test.tsx +62 -57
  144. package/src/lib/components/sidebar/Sidebar.component.tsx +1 -1
  145. package/src/lib/components/statuswrapper/Statuswrapper.component.tsx +0 -1
  146. package/src/lib/components/steppers/Stepper.component.tsx +10 -8
  147. package/src/lib/components/tablev2/Search.tsx +2 -2
  148. package/src/lib/components/tablev2/SingleSelectableContent.tsx +2 -2
  149. package/src/lib/components/tablev2/TableCommon.tsx +1 -1
  150. package/src/lib/components/tablev2/TableSync.test.tsx +28 -0
  151. package/src/lib/components/tablev2/TableSync.tsx +36 -0
  152. package/src/lib/components/tablev2/Tablev2.component.tsx +11 -9
  153. package/src/lib/components/tablev2/Tablev2.test.tsx +36 -37
  154. package/src/lib/components/tabsv2/ScrollButton.tsx +2 -2
  155. package/src/lib/components/tabsv2/Tabsv2.component.tsx +6 -6
  156. package/src/lib/components/text/Text.component.tsx +4 -5
  157. package/src/lib/components/textarea/TextArea.component.tsx +3 -2
  158. package/src/lib/components/textbadge/TextBadge.component.tsx +0 -1
  159. package/src/lib/components/toast/Toast.component.tsx +1 -1
  160. package/src/lib/components/toast/ToastProvider.tsx +17 -7
  161. package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +5 -7
  162. package/src/lib/components/vegachartv2/VegaChartV2.component.tsx +2 -2
  163. package/src/lib/icons/branding.tsx +0 -2
  164. package/src/lib/icons/scality-loading.tsx +0 -2
  165. package/src/lib/index.ts +1 -0
  166. package/src/lib/next.ts +6 -0
  167. package/src/lib/style/theme.ts +53 -1
  168. package/stories/BarChart/barchart.stories.tsx +822 -0
  169. package/stories/areachart.stories.tsx +0 -1
  170. package/stories/format.mdx +4 -2
  171. package/stories/linetimeseriechart.stories.tsx +485 -0
  172. package/stories/tablev2.stories.tsx +41 -0
  173. package/tsconfig.json +5 -2
@@ -1,24 +1,21 @@
1
1
  import React from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
3
3
  import { InputList, InputListProps } from './InputList.component';
4
4
  import { FormSection } from '../form/Form.component';
5
- import { QueryClient, QueryClientProvider } from 'react-query';
6
5
 
7
6
  describe('InputList', () => {
8
7
  const onChangeMock = jest.fn();
9
8
 
10
9
  const renderInputList = (props: InputListProps<string[]>) => {
11
10
  render(
12
- <QueryClientProvider client={new QueryClient()}>
13
- <FormSection>
14
- <InputList
15
- placeholder="Input list Test"
16
- onChange={onChangeMock}
17
- value={props.value}
18
- name="inputListTest"
19
- />
20
- </FormSection>
21
- </QueryClientProvider>,
11
+ <FormSection>
12
+ <InputList
13
+ placeholder="Input list Test"
14
+ onChange={onChangeMock}
15
+ value={props.value}
16
+ name="inputListTest"
17
+ />
18
+ </FormSection>
22
19
  );
23
20
  };
24
21
 
@@ -26,20 +23,22 @@ describe('InputList', () => {
26
23
  onChangeMock.mockClear();
27
24
  });
28
25
 
29
- it('should render an empty input list', () => {
26
+ it('should render an empty input list', async () => {
30
27
  renderInputList({
31
28
  value: [''],
32
29
  });
30
+ await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
33
31
 
34
32
  expect(screen.getByLabelText('inputListTest0')).toHaveValue('');
35
33
  });
36
34
 
37
- it('should render an input list with initial values', () => {
35
+ it('should render an input list with initial values', async () => {
38
36
  const initialValues = ['Value 1', 'Value 2', 'Value 3'];
39
37
 
40
38
  renderInputList({
41
39
  value: initialValues,
42
40
  });
41
+ await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
43
42
 
44
43
  const inputElements = screen.getAllByRole('textbox');
45
44
 
@@ -50,44 +49,47 @@ describe('InputList', () => {
50
49
  });
51
50
  });
52
51
 
53
- it('should add a new input when clicking the add button', () => {
52
+ it('should add a new input when clicking the add button', async () => {
54
53
  const initialValues = ['Value 1', 'Value 2'];
55
54
 
56
55
  renderInputList({
57
56
  value: initialValues,
58
57
  });
58
+ await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
59
59
 
60
60
  const addButton = screen.getByLabelText('Add1');
61
61
 
62
- fireEvent.click(addButton);
62
+ await act(() => fireEvent.click(addButton));
63
63
 
64
64
  expect(onChangeMock).toHaveBeenCalledWith({
65
65
  target: { value: [...initialValues, ''] },
66
66
  });
67
67
  });
68
68
 
69
- it('should delete an input when clicking the delete button', () => {
69
+ it('should delete an input when clicking the delete button', async () => {
70
70
  const initialValues = ['Value 1', 'Value 2', 'Value 3'];
71
71
 
72
72
  renderInputList({
73
73
  value: initialValues,
74
74
  });
75
+ await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
75
76
 
76
77
  const deleteButton = screen.getByLabelText('Remove1');
77
78
 
78
- fireEvent.click(deleteButton);
79
+ await act(() => fireEvent.click(deleteButton));
79
80
 
80
81
  expect(onChangeMock).toHaveBeenCalledWith({
81
82
  target: { value: ['Value 1', 'Value 3'] },
82
83
  });
83
84
  });
84
85
 
85
- it('should update the value of an input', () => {
86
+ it('should update the value of an input', async () => {
86
87
  const initialValues = ['Value 1', 'Value 2', 'Value 3'];
87
88
 
88
89
  renderInputList({
89
90
  value: initialValues,
90
91
  });
92
+ await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
91
93
 
92
94
  const inputElements = screen.getAllByRole('textbox');
93
95
 
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import styled from 'styled-components';
3
2
 
4
3
  import { Props as SidebarProps } from '../sidebar/Sidebar.component';
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import styled from 'styled-components';
3
2
 
4
3
  import { Navbar } from '../navbar/Navbar.component';
@@ -1,5 +1,5 @@
1
- import { ReactElement } from 'react';
2
1
  import styled from 'styled-components';
2
+ import { ReactElement } from 'react';
3
3
  import { ThemeColors } from '../../../style/theme';
4
4
  import { AppContainer } from './AppContainer';
5
5
 
@@ -0,0 +1,502 @@
1
+ import {
2
+ Line,
3
+ LineChart,
4
+ ReferenceLine,
5
+ ResponsiveContainer,
6
+ Tooltip,
7
+ XAxis,
8
+ YAxis,
9
+ CartesianGrid,
10
+ } from 'recharts';
11
+ import { useMemo, useRef } from 'react';
12
+ import { useTheme } from 'styled-components';
13
+ import { useMetricsTimeSpan } from '../linetemporalchart/MetricTimespanProvider';
14
+ import { addMissingDataPoint } from '../linetemporalchart/ChartUtil';
15
+ import styled from 'styled-components';
16
+ import { fontSize, fontWeight } from '../../style/theme';
17
+ import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
18
+ import { ChartTitleText, SmallerText } from '../text/Text.component';
19
+ import { Loader } from '../loader/Loader.component';
20
+ import { spacing } from '../../spacing';
21
+ import { getUnitLabel } from '../linetemporalchart/ChartUtil';
22
+ import { Icon } from '../icon/Icon.component';
23
+ import { Tooltip as TooltipComponent } from '../tooltip/Tooltip.component';
24
+ import {
25
+ DAY_MONTH_ABBREVIATED_HOUR_MINUTE,
26
+ FormattedDateTime,
27
+ } from '../date/FormattedDateTime';
28
+
29
+ const LineTemporalChartWrapper = styled.div`
30
+ display: flex;
31
+ flex-direction: column;
32
+ justify-content: flex-start;
33
+ flex: 1;
34
+ `;
35
+
36
+ const ChartHeader = styled.div`
37
+ display: flex;
38
+ align-items: center;
39
+ `;
40
+
41
+ const TooltipContainer = styled.div`
42
+ background-color: ${(props) => props.theme.backgroundLevel1};
43
+ padding: ${spacing.r8};
44
+ border: 1px solid ${(props) => props.theme.border};
45
+ border-radius: 4px;
46
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
47
+ max-width: 250px;
48
+ `;
49
+
50
+ const TooltipTime = styled.div`
51
+ margin-bottom: ${spacing.r8};
52
+ color: ${(props) => props.theme.textPrimary};
53
+ font-size: ${fontSize.smaller};
54
+ font-weight: ${fontWeight.bold};
55
+ text-align: center;
56
+ `;
57
+
58
+ const TooltipValue = styled.div`
59
+ font-size: ${fontSize.smaller};
60
+ margin-top: 4px;
61
+ color: ${(props) => props.theme.textSecondary};
62
+ display: flex;
63
+ align-items: flex-start;
64
+ `;
65
+
66
+ const TooltipLegend = styled.div<{ color: string }>`
67
+ width: 12px;
68
+ height: 3px;
69
+ background-color: ${(props) => props.color};
70
+ margin-right: 8px;
71
+ flex-shrink: 0;
72
+ margin-top: 8px;
73
+ `;
74
+
75
+ const TooltipContent = styled.div`
76
+ display: flex;
77
+ min-width: 0;
78
+ flex: 1;
79
+ `;
80
+
81
+ const TooltipName = styled.div`
82
+ margin-right: 4px;
83
+ word-wrap: break-word;
84
+ word-break: break-word;
85
+ justify-content: flex-start;
86
+ `;
87
+
88
+ const TooltipInstanceValue = styled.div`
89
+ justify-content: flex-end;
90
+ `;
91
+
92
+ export type Serie = {
93
+ // the name of the resource
94
+ resource: string;
95
+ // the original data format from prometheus
96
+ data: [number, string | null][];
97
+ // it's mandatory to display tooltip label in the tooltip
98
+ getTooltipLabel: (metricPrefix?: string, resource?: string) => string;
99
+ // the name of the metric prefix with read, write, in, out
100
+ metricPrefix?: string;
101
+ // to specify if the line is dash
102
+ isLineDashed?: boolean;
103
+ };
104
+
105
+ type NonSymmetricalChartSerie = {
106
+ yAxisType?: 'default' | 'percentage';
107
+ series: Serie[];
108
+ };
109
+
110
+ // The symmetrical chart props are used to display two series on the same chart, such as in/out, write/read
111
+ type SymmetricalChartSerie = {
112
+ yAxisType: 'symmetrical';
113
+ series: {
114
+ above: Serie[];
115
+ below: Serie[];
116
+ };
117
+ };
118
+
119
+ export type LineChartProps = (
120
+ | NonSymmetricalChartSerie
121
+ | SymmetricalChartSerie
122
+ ) & {
123
+ title: string;
124
+ height: number;
125
+ startingTimeStamp: number;
126
+ unitRange?: {
127
+ threshold: number;
128
+ label: string;
129
+ }[];
130
+ isLoading?: boolean;
131
+ yAxisTitle?: string;
132
+ helpText?: string;
133
+ };
134
+
135
+ const CustomTooltip = ({
136
+ active,
137
+ payload,
138
+ label,
139
+ unitLabel,
140
+ }: {
141
+ active?: boolean;
142
+ payload?: Array<{
143
+ value: number;
144
+ name: string;
145
+ color: string;
146
+ dataKey: string;
147
+ }>;
148
+ label?: string;
149
+ unitLabel?: string;
150
+ }) => {
151
+ if (!active || !payload || !payload.length || !label) return null;
152
+ // We can't use the default itemSorter method because it's a custom tooltip.
153
+ // Sort the payload here instead
154
+ const sortedPayload = [...payload].sort((a, b) => {
155
+ const aValue = Number(a.value);
156
+ const bValue = Number(b.value);
157
+
158
+ if (aValue >= 0 && bValue >= 0) {
159
+ return bValue - aValue; // Higher positive values first
160
+ }
161
+ if (aValue < 0 && bValue < 0) {
162
+ return bValue - aValue; // Lower negative values first
163
+ }
164
+ return bValue - aValue; // Positives before negatives
165
+ });
166
+
167
+ return (
168
+ <TooltipContainer>
169
+ <TooltipTime>
170
+ <FormattedDateTime
171
+ format="day-month-abbreviated-hour-minute-second"
172
+ value={new Date(label)}
173
+ />
174
+ </TooltipTime>
175
+ {sortedPayload.map((entry, index) => (
176
+ <TooltipValue key={index}>
177
+ <TooltipLegend color={entry.color} />
178
+ <TooltipContent>
179
+ <TooltipName>{entry.name}</TooltipName>
180
+ <TooltipInstanceValue>
181
+ {isNaN(Number(entry.value))
182
+ ? '-'
183
+ : `${Number(entry.value).toFixed(2)}${unitLabel}`}
184
+ </TooltipInstanceValue>
185
+ </TooltipContent>
186
+ </TooltipValue>
187
+ ))}
188
+ </TooltipContainer>
189
+ );
190
+ };
191
+
192
+ const isSymmetricalSeries = (
193
+ series: Serie[] | { above: Serie[]; below: Serie[] },
194
+ ): series is { above: Serie[]; below: Serie[] } => {
195
+ return 'above' in series && 'below' in series;
196
+ };
197
+
198
+ export function LineTimeSerieChart({
199
+ series,
200
+ title,
201
+ height,
202
+ startingTimeStamp,
203
+ unitRange,
204
+ isLoading = false,
205
+ yAxisType = 'default',
206
+ yAxisTitle,
207
+ helpText,
208
+ ...rest
209
+ }: LineChartProps) {
210
+ const theme = useTheme();
211
+ const { frequency, duration } = useMetricsTimeSpan();
212
+ const { getColor } = useChartLegend();
213
+ const chartRef = useRef(null);
214
+
215
+ const chartData = useMemo(() => {
216
+ // 1. Add missing data points
217
+ const normalizedSeries =
218
+ yAxisType === 'symmetrical' && isSymmetricalSeries(series)
219
+ ? {
220
+ above: series.above.map((line) => ({
221
+ ...line,
222
+ data: addMissingDataPoint(
223
+ line.data,
224
+ startingTimeStamp,
225
+ duration,
226
+ frequency,
227
+ ),
228
+ })),
229
+ // Convert positive values to negative values
230
+ below: series.below.map((line) => ({
231
+ ...line,
232
+ data: addMissingDataPoint(
233
+ line.data,
234
+ startingTimeStamp,
235
+ duration,
236
+ frequency,
237
+ ).map(
238
+ ([timestamp, value]) =>
239
+ [timestamp, value === null ? null : `-${Number(value)}`] as [
240
+ number,
241
+ string | null,
242
+ ],
243
+ ),
244
+ })),
245
+ }
246
+ : (series as Serie[]).map((line) => ({
247
+ ...line,
248
+ data: addMissingDataPoint(
249
+ line.data,
250
+ startingTimeStamp,
251
+ duration,
252
+ frequency,
253
+ ),
254
+ }));
255
+
256
+ // 2. Convert directly to Recharts format
257
+ // Initialize an object to hold data points by timestamp
258
+ const dataPointsByTime: Record<
259
+ number,
260
+ { timestamp: number } & Record<string, string | number | null>
261
+ > = {};
262
+ const seriesToProcess =
263
+ yAxisType === 'symmetrical' && isSymmetricalSeries(normalizedSeries)
264
+ ? [...normalizedSeries.above, ...normalizedSeries.below]
265
+ : (normalizedSeries as Serie[]);
266
+
267
+ seriesToProcess.forEach((serie) => {
268
+ const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
269
+
270
+ serie.data.forEach((point) => {
271
+ const timestamp =
272
+ typeof point[0] === 'number' ? point[0] * 1000 : Number(point[0]);
273
+ const value = point[1];
274
+ // Initialize this timestamp if it doesn't exist
275
+ if (!dataPointsByTime[timestamp]) {
276
+ dataPointsByTime[timestamp] = { timestamp };
277
+ }
278
+ // Add this metric's value to the data point, and convert the value to a number if it's a string
279
+ dataPointsByTime[timestamp][label] =
280
+ typeof value === 'string' ? Number(value) : value;
281
+ });
282
+ });
283
+ // Convert object to array for Recharts
284
+ return Object.values(dataPointsByTime).sort(
285
+ (
286
+ a: { timestamp: number } & Record<string, string | number | null>,
287
+ b: { timestamp: number } & Record<string, string | number | null>,
288
+ ) => (a.timestamp as number) - (b.timestamp as number),
289
+ );
290
+ }, [series, startingTimeStamp, duration, frequency, yAxisType]);
291
+
292
+ // Calculate 5 perfectly evenly spaced ticks
293
+ const xAxisTicks = useMemo(() => {
294
+ if (!chartData || chartData.length === 0) return [];
295
+
296
+ const timestamps: number[] = chartData.map((d) => d.timestamp);
297
+ const minTimestamp = Math.min(...timestamps);
298
+ const maxTimestamp = Math.max(...timestamps);
299
+
300
+ // Calculate 5 perfectly evenly spaced ticks
301
+ const timeRange = maxTimestamp - minTimestamp;
302
+ const interval = timeRange / 4; // 4 intervals create 5 points
303
+
304
+ const exactEvenTicks = [
305
+ minTimestamp,
306
+ minTimestamp + interval,
307
+ minTimestamp + interval * 2,
308
+ minTimestamp + interval * 3,
309
+ maxTimestamp,
310
+ ];
311
+
312
+ // Return perfectly even ticks (guaranteed to be evenly divided)
313
+ return exactEvenTicks;
314
+ }, [chartData]);
315
+
316
+ // 3. Transform the data base on the valuebase
317
+ const { topValue, unitLabel, rechartsData } = useMemo(() => {
318
+ if (yAxisType === 'percentage')
319
+ return {
320
+ topValue: 100,
321
+ unitLabel: '%',
322
+ rechartsData: chartData,
323
+ };
324
+
325
+ const values = chartData.flatMap((dataPoint) =>
326
+ Object.entries(dataPoint)
327
+ .filter(([key]) => key !== 'timestamp')
328
+ .map(([_, value]) => {
329
+ const num =
330
+ typeof value === 'string' ? Number(value) : (value ?? Infinity);
331
+ return !isNaN(num) && num !== null ? num : null;
332
+ })
333
+ .filter((value): value is number => value !== null),
334
+ );
335
+
336
+ const top = Math.abs(Math.max(...values));
337
+ const bottom = Math.abs(Math.min(...values));
338
+ const maxValue = Math.max(top, bottom);
339
+
340
+ const { valueBase, unitLabel } = getUnitLabel(unitRange ?? [], maxValue);
341
+
342
+ const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
343
+
344
+ const rechartsData = chartData.map((dataPoint) => {
345
+ const normalizedDataPoint = { ...dataPoint };
346
+ Object.entries(dataPoint).forEach(([key, value]) => {
347
+ if (key !== 'timestamp' && typeof value === 'number') {
348
+ normalizedDataPoint[key] = value / valueBase;
349
+ }
350
+ });
351
+ return normalizedDataPoint;
352
+ });
353
+
354
+ return { topValue, unitLabel, rechartsData };
355
+ }, [chartData, yAxisType, unitRange]);
356
+
357
+ // Group series by resource and create color mapping
358
+ const { colorMapping, groupedSeries } = useMemo(() => {
359
+ const mapping: Record<string, string> = {};
360
+ const allSeries = isSymmetricalSeries(series)
361
+ ? [...series.above, ...series.below]
362
+ : (series as Serie[]);
363
+
364
+ // Group series by resource
365
+ const groups = allSeries.reduce(
366
+ (acc, serie) => {
367
+ const key = serie.resource;
368
+ if (!acc[key]) {
369
+ acc[key] = [];
370
+ }
371
+ acc[key].push(serie);
372
+ return acc;
373
+ },
374
+ {} as Record<string, Serie[]>,
375
+ );
376
+
377
+ // Get colors from the ChartLegend context
378
+ Object.keys(groups).forEach((resource) => {
379
+ const color = getColor(resource);
380
+ if (color) {
381
+ mapping[resource] = color;
382
+ } else {
383
+ console.warn(`Color not defined for resource: ${resource}`);
384
+ }
385
+ });
386
+
387
+ return {
388
+ colorMapping: mapping,
389
+ groupedSeries: groups,
390
+ };
391
+ }, [series, getColor]);
392
+
393
+ // Format time for display the tick in the x axis
394
+ const formatTime = useMemo(
395
+ () => (timestamp: number) => {
396
+ const date = new Date(timestamp);
397
+ return DAY_MONTH_ABBREVIATED_HOUR_MINUTE.format(date).replace(',', '');
398
+ },
399
+ [],
400
+ );
401
+
402
+ return (
403
+ <LineTemporalChartWrapper>
404
+ <ChartHeader>
405
+ <ChartTitleText>
406
+ {title} {unitLabel && `(${unitLabel})`}
407
+ </ChartTitleText>
408
+ {helpText && (
409
+ <TooltipComponent
410
+ placement={'right'}
411
+ overlay={<SmallerText>{helpText}</SmallerText>}
412
+ >
413
+ <Icon name="Info" color={theme.buttonSecondary} />
414
+ </TooltipComponent>
415
+ )}
416
+ {isLoading && <Loader />}
417
+ </ChartHeader>
418
+ <ResponsiveContainer width="100%" height={height}>
419
+ <LineChart
420
+ data={rechartsData}
421
+ ref={chartRef}
422
+ margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
423
+ aria-label={`Time series chart for ${title}`}
424
+ >
425
+ <CartesianGrid
426
+ vertical={true}
427
+ horizontal={true}
428
+ verticalPoints={[0]}
429
+ horizontalPoints={[0]}
430
+ stroke={theme.border}
431
+ fill={theme.backgroundLevel4}
432
+ strokeWidth={1}
433
+ />
434
+ <XAxis
435
+ dataKey="timestamp"
436
+ type="number"
437
+ domain={['dataMin', 'dataMax']}
438
+ ticks={xAxisTicks}
439
+ tickFormatter={formatTime}
440
+ tickCount={5}
441
+ tick={{
442
+ fill: theme.textSecondary,
443
+ fontSize: fontSize.smaller,
444
+ }}
445
+ axisLine={{ stroke: theme.border }}
446
+ />
447
+ <YAxis
448
+ orientation="right"
449
+ allowDataOverflow={false}
450
+ label={{
451
+ value: yAxisTitle,
452
+ angle: 90,
453
+ position: 'insideRight',
454
+ style: {
455
+ textAnchor: 'middle',
456
+ fill: theme.textSecondary,
457
+ fontSize: fontSize.smaller,
458
+ },
459
+ }}
460
+ domain={
461
+ yAxisType === 'percentage'
462
+ ? [0, 100]
463
+ : yAxisType === 'symmetrical'
464
+ ? [-topValue, topValue]
465
+ : [0, topValue]
466
+ }
467
+ axisLine={{ stroke: theme.border }}
468
+ tick={{
469
+ fill: theme.textSecondary,
470
+ fontSize: fontSize.smaller,
471
+ }}
472
+ tickFormatter={(value) => Math.round(value).toString()}
473
+ />
474
+ <Tooltip content={<CustomTooltip unitLabel={unitLabel} />} />
475
+ {/* Add horizontal line at y=0 for symmetrical charts */}
476
+ {yAxisType === 'symmetrical' && (
477
+ <ReferenceLine y={0} stroke={theme.border} />
478
+ )}
479
+
480
+ {/* Chart lines */}
481
+ {Object.entries(groupedSeries).map(([resource, resourceSeries]) =>
482
+ resourceSeries.map((serie, serieIndex) => {
483
+ const label = serie.getTooltipLabel(
484
+ serie.metricPrefix,
485
+ serie.resource,
486
+ );
487
+ return (
488
+ <Line
489
+ key={`${title}-${resource}-${serieIndex}`}
490
+ type="monotone"
491
+ dataKey={label}
492
+ stroke={colorMapping[resource]}
493
+ dot={false}
494
+ />
495
+ );
496
+ }),
497
+ )}
498
+ </LineChart>
499
+ </ResponsiveContainer>
500
+ </LineTemporalChartWrapper>
501
+ );
502
+ }
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, useEffect, useLayoutEffect, useRef } from 'react';
2
- import ReactDom from 'react-dom';
2
+ import { createPortal } from 'react-dom';
3
3
  import styled from 'styled-components';
4
4
  import { Wrap, spacing } from '../../spacing';
5
5
  import { zIndex } from '../../style/theme';
@@ -95,7 +95,7 @@ const Modal = ({
95
95
  }
96
96
  }, [isOpen]);
97
97
  return isOpen
98
- ? ReactDom.createPortal(
98
+ ? createPortal(
99
99
  <ModalContainer
100
100
  className="sc-modal"
101
101
  role={role}
@@ -1,4 +1,4 @@
1
- import React, { Fragment } from 'react';
1
+ import { cloneElement, Fragment } from 'react';
2
2
  import styled, { css } from 'styled-components';
3
3
  import { Logo } from '../../icons/branding';
4
4
  import { spacing } from '../../spacing';
@@ -210,7 +210,7 @@ function NavBar({
210
210
  );
211
211
  }
212
212
  return link ? (
213
- React.cloneElement(link, {
213
+ cloneElement(link, {
214
214
  className: selected ? 'selected' : '',
215
215
  'aria-selected': selected,
216
216
  role: 'tab',
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import styled, { createGlobalStyle, css } from 'styled-components';
3
2
 
4
3
  type Props = {
@@ -1,4 +1,3 @@
1
- import React from 'react';
2
1
  import { ChangeEvent, forwardRef, useEffect, useRef, useState } from 'react';
3
2
  import styled, { css } from 'styled-components';
4
3
  import { Icon } from '../icon/Icon.component';