@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.
- package/README.md +15 -15
- package/dist/components/accordion/Accordion.component.d.ts +0 -1
- package/dist/components/accordion/Accordion.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.d.ts +53 -0
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -0
- package/dist/components/barchartv2/Barchart.component.js +86 -0
- package/dist/components/barchartv2/utils.d.ts +118 -0
- package/dist/components/barchartv2/utils.d.ts.map +1 -0
- package/dist/components/barchartv2/utils.js +337 -0
- package/dist/components/buttonv2/Buttonv2.component.d.ts +1 -1
- package/dist/components/buttonv2/Buttonv2.component.d.ts.map +1 -1
- 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/constrainedtext/Constrainedtext.component.d.ts +2 -1
- package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
- package/dist/components/constrainedtext/Constrainedtext.component.js +5 -4
- package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts +0 -1
- package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.d.ts +4 -1
- package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.js +24 -1
- package/dist/components/date/FormattedDateTime.spec.js +12 -0
- package/dist/components/emptytable/Emptytable.component.d.ts +0 -1
- package/dist/components/emptytable/Emptytable.component.d.ts.map +1 -1
- package/dist/components/emptytable/Emptytable.component.js +1 -0
- package/dist/components/error-pages/ErrorPage401.component.d.ts +0 -1
- package/dist/components/error-pages/ErrorPage401.component.d.ts.map +1 -1
- package/dist/components/error-pages/ErrorPage404.component.d.ts +0 -1
- package/dist/components/error-pages/ErrorPage404.component.d.ts.map +1 -1
- package/dist/components/error-pages/ErrorPage500.component.d.ts +0 -1
- package/dist/components/error-pages/ErrorPage500.component.d.ts.map +1 -1
- package/dist/components/error-pages/ErrorPageAuth.component.d.ts.map +1 -1
- package/dist/components/form/Form.component.d.ts +2 -2
- package/dist/components/form/Form.component.d.ts.map +1 -1
- 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/infomessage/InfoMessage.component.d.ts +0 -1
- package/dist/components/infomessage/InfoMessage.component.d.ts.map +1 -1
- package/dist/components/lateralnavbarlayout/LateralNavbarLayout.component.d.ts.map +1 -1
- package/dist/components/layout/Layout.component.d.ts.map +1 -1
- package/dist/components/layout/v2/panels.d.ts.map +1 -1
- 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/modal/Modal.component.js +2 -2
- package/dist/components/navbar/Navbar.component.js +2 -2
- package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts +0 -1
- package/dist/components/scrollbarwrapper/ScrollbarWrapper.component.d.ts.map +1 -1
- package/dist/components/searchinput/SearchInput.component.d.ts +1 -2
- package/dist/components/searchinput/SearchInput.component.d.ts.map +1 -1
- package/dist/components/selectv2/Selectv2.component.d.ts +5 -5
- package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
- package/dist/components/selectv2/Selectv2.component.js +11 -6
- package/dist/components/statuswrapper/Statuswrapper.component.d.ts +0 -1
- package/dist/components/statuswrapper/Statuswrapper.component.d.ts.map +1 -1
- package/dist/components/steppers/Stepper.component.d.ts.map +1 -1
- package/dist/components/steppers/Stepper.component.js +9 -8
- package/dist/components/tablev2/Search.js +2 -2
- package/dist/components/tablev2/SingleSelectableContent.d.ts +1 -2
- package/dist/components/tablev2/SingleSelectableContent.d.ts.map +1 -1
- package/dist/components/tablev2/TableCommon.d.ts +2 -2
- package/dist/components/tablev2/TableCommon.d.ts.map +1 -1
- package/dist/components/tablev2/TableSync.d.ts +8 -0
- package/dist/components/tablev2/TableSync.d.ts.map +1 -0
- package/dist/components/tablev2/TableSync.js +11 -0
- package/dist/components/tablev2/Tablev2.component.d.ts +2 -1
- package/dist/components/tablev2/Tablev2.component.d.ts.map +1 -1
- package/dist/components/tablev2/Tablev2.component.js +10 -9
- package/dist/components/tabsv2/ScrollButton.d.ts +1 -2
- package/dist/components/tabsv2/ScrollButton.d.ts.map +1 -1
- package/dist/components/tabsv2/ScrollButton.js +2 -2
- package/dist/components/tabsv2/Tabsv2.component.d.ts +2 -2
- package/dist/components/tabsv2/Tabsv2.component.d.ts.map +1 -1
- package/dist/components/tabsv2/Tabsv2.component.js +2 -2
- package/dist/components/text/Text.component.d.ts +0 -1
- package/dist/components/text/Text.component.d.ts.map +1 -1
- package/dist/components/textarea/TextArea.component.d.ts +3 -3
- package/dist/components/textarea/TextArea.component.d.ts.map +1 -1
- package/dist/components/textbadge/TextBadge.component.d.ts +0 -1
- package/dist/components/textbadge/TextBadge.component.d.ts.map +1 -1
- package/dist/components/toast/Toast.component.d.ts +1 -1
- package/dist/components/toast/Toast.component.d.ts.map +1 -1
- 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 +1 -2
- package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
- package/dist/components/vegachartv2/SyncedCursorCharts.js +3 -5
- package/dist/components/vegachartv2/VegaChartV2.component.d.ts +1 -2
- package/dist/components/vegachartv2/VegaChartV2.component.d.ts.map +1 -1
- package/dist/components/vegachartv2/VegaChartV2.component.js +2 -2
- package/dist/icons/branding.d.ts.map +1 -1
- package/dist/icons/scality-loading.d.ts.map +1 -1
- 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 +20 -0
- package/dist/style/theme.d.ts.map +1 -1
- package/dist/style/theme.js +46 -1
- package/package.json +7 -4
- package/setupTests.js +6 -0
- package/src/lib/components/accordion/Accordion.component.tsx +1 -1
- package/src/lib/components/accordion/Accordion.test.tsx +7 -15
- package/src/lib/components/barchartv2/Barchart.component.test.tsx +364 -0
- package/src/lib/components/barchartv2/Barchart.component.tsx +321 -0
- package/src/lib/components/barchartv2/utils.test.ts +899 -0
- package/src/lib/components/barchartv2/utils.ts +534 -0
- package/src/lib/components/buttonv2/Buttonv2.component.tsx +1 -1
- package/src/lib/components/chartlegend/ChartLegend.tsx +113 -0
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +85 -0
- package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +22 -3
- package/src/lib/components/coreuithemeprovider/CoreUiThemeProvider.tsx +0 -1
- package/src/lib/components/date/FormattedDateTime.spec.tsx +24 -0
- package/src/lib/components/date/FormattedDateTime.tsx +42 -2
- package/src/lib/components/emptytable/Emptytable.component.tsx +1 -1
- package/src/lib/components/error-pages/ErrorPage401.component.tsx +0 -1
- package/src/lib/components/error-pages/ErrorPage404.component.tsx +0 -1
- package/src/lib/components/error-pages/ErrorPage500.component.tsx +0 -1
- package/src/lib/components/error-pages/ErrorPageAuth.component.tsx +0 -1
- package/src/lib/components/form/Form.component.tsx +1 -1
- package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +3 -3
- package/src/lib/components/icon/Icon.component.tsx +48 -60
- package/src/lib/components/infomessage/InfoMessage.component.tsx +0 -1
- package/src/lib/components/inlineinput/InlineInput.test.tsx +22 -19
- package/src/lib/components/inputlist/InputList.test.tsx +21 -19
- package/src/lib/components/lateralnavbarlayout/LateralNavbarLayout.component.tsx +0 -1
- package/src/lib/components/layout/Layout.component.tsx +0 -1
- package/src/lib/components/layout/v2/panels.tsx +1 -1
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +502 -0
- package/src/lib/components/modal/Modal.component.tsx +2 -2
- package/src/lib/components/navbar/Navbar.component.tsx +2 -2
- package/src/lib/components/scrollbarwrapper/ScrollbarWrapper.component.tsx +0 -1
- package/src/lib/components/searchinput/SearchInput.component.tsx +0 -1
- package/src/lib/components/searchinput/SearchInput.test.tsx +3 -7
- package/src/lib/components/selectv2/Selectv2.component.tsx +24 -14
- package/src/lib/components/selectv2/selectv2.test.tsx +62 -57
- package/src/lib/components/sidebar/Sidebar.component.tsx +1 -1
- package/src/lib/components/statuswrapper/Statuswrapper.component.tsx +0 -1
- package/src/lib/components/steppers/Stepper.component.tsx +10 -8
- package/src/lib/components/tablev2/Search.tsx +2 -2
- package/src/lib/components/tablev2/SingleSelectableContent.tsx +2 -2
- package/src/lib/components/tablev2/TableCommon.tsx +1 -1
- package/src/lib/components/tablev2/TableSync.test.tsx +28 -0
- package/src/lib/components/tablev2/TableSync.tsx +36 -0
- package/src/lib/components/tablev2/Tablev2.component.tsx +11 -9
- package/src/lib/components/tablev2/Tablev2.test.tsx +36 -37
- package/src/lib/components/tabsv2/ScrollButton.tsx +2 -2
- package/src/lib/components/tabsv2/Tabsv2.component.tsx +6 -6
- package/src/lib/components/text/Text.component.tsx +4 -5
- package/src/lib/components/textarea/TextArea.component.tsx +3 -2
- package/src/lib/components/textbadge/TextBadge.component.tsx +0 -1
- package/src/lib/components/toast/Toast.component.tsx +1 -1
- package/src/lib/components/toast/ToastProvider.tsx +17 -7
- package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +5 -7
- package/src/lib/components/vegachartv2/VegaChartV2.component.tsx +2 -2
- package/src/lib/icons/branding.tsx +0 -2
- package/src/lib/icons/scality-loading.tsx +0 -2
- package/src/lib/index.ts +1 -0
- package/src/lib/next.ts +6 -0
- package/src/lib/style/theme.ts +53 -1
- package/stories/BarChart/barchart.stories.tsx +822 -0
- package/stories/areachart.stories.tsx +0 -1
- package/stories/format.mdx +4 -2
- package/stories/linetimeseriechart.stories.tsx +485 -0
- package/stories/tablev2.stories.tsx +41 -0
- 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
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
-
?
|
|
98
|
+
? createPortal(
|
|
99
99
|
<ModalContainer
|
|
100
100
|
className="sc-modal"
|
|
101
101
|
role={role}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
213
|
+
cloneElement(link, {
|
|
214
214
|
className: selected ? 'selected' : '',
|
|
215
215
|
'aria-selected': selected,
|
|
216
216
|
role: 'tab',
|