@centreon/ui 23.10.60 → 23.10.62
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/package.json +1 -1
- package/src/Graph/LineChart/BasicComponents/Thresholds.tsx +119 -0
- package/src/Graph/LineChart/BasicComponents/useFilterLines.ts +9 -7
- package/src/Graph/LineChart/InteractiveComponents/AnchorPoint/index.tsx +2 -4
- package/src/Graph/LineChart/InteractiveComponents/interactionWithGraphAtoms.ts +1 -0
- package/src/Graph/LineChart/Legend/index.tsx +9 -4
- package/src/Graph/LineChart/Legend/useInteractiveValues.ts +13 -4
- package/src/Graph/LineChart/Legend/useLegend.ts +10 -13
- package/src/Graph/LineChart/LineChart.tsx +70 -6
- package/src/Graph/LineChart/graphAtoms.ts +0 -3
- package/src/Graph/LineChart/index.stories.tsx +37 -6
- package/src/Graph/LineChart/index.tsx +18 -2
- package/src/Graph/common/timeSeries/index.ts +41 -8
- package/src/Graph/common/timeSeries/models.ts +3 -1
- package/src/api/useGraphQuery/index.ts +52 -21
- package/src/components/Modal/Modal.styles.ts +1 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/useRefreshInterval.ts +21 -0
package/package.json
CHANGED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { always, cond, equals, isNil } from 'ramda';
|
|
2
|
+
|
|
3
|
+
import { Theme, useTheme } from '@mui/material';
|
|
4
|
+
|
|
5
|
+
import { getUnits, getYScale } from '../../common/timeSeries';
|
|
6
|
+
import { Line } from '../../common/timeSeries/models';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
displayedLines: Array<Line>;
|
|
10
|
+
hideTooltip: () => void;
|
|
11
|
+
leftScale: (value: number) => number;
|
|
12
|
+
rightScale: (value: number) => number;
|
|
13
|
+
showTooltip: (props) => void;
|
|
14
|
+
thresholdLabels?: Array<string>;
|
|
15
|
+
thresholdUnit?: string;
|
|
16
|
+
thresholds?: Array<number>;
|
|
17
|
+
width: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface GetColorProps {
|
|
21
|
+
index: number;
|
|
22
|
+
theme: Theme;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getColor = ({ index, theme }: GetColorProps): string => {
|
|
26
|
+
return cond([
|
|
27
|
+
[equals(0), always(theme.palette.warning.main)],
|
|
28
|
+
[equals(1), always(theme.palette.error.main)]
|
|
29
|
+
])(index);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const Thresholds = ({
|
|
33
|
+
thresholds,
|
|
34
|
+
leftScale,
|
|
35
|
+
rightScale,
|
|
36
|
+
width,
|
|
37
|
+
displayedLines,
|
|
38
|
+
thresholdUnit,
|
|
39
|
+
showTooltip,
|
|
40
|
+
hideTooltip,
|
|
41
|
+
thresholdLabels
|
|
42
|
+
}: Props): JSX.Element | null => {
|
|
43
|
+
const theme = useTheme();
|
|
44
|
+
|
|
45
|
+
if (!thresholds) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [firstUnit, secondUnit, thirdUnit] = getUnits(
|
|
50
|
+
displayedLines as Array<Line>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const shouldUseRightScale = equals(thresholdUnit, secondUnit);
|
|
54
|
+
|
|
55
|
+
const yScale = shouldUseRightScale
|
|
56
|
+
? rightScale
|
|
57
|
+
: getYScale({
|
|
58
|
+
hasMoreThanTwoUnits: !isNil(thirdUnit),
|
|
59
|
+
invert: null,
|
|
60
|
+
leftScale,
|
|
61
|
+
rightScale,
|
|
62
|
+
secondUnit,
|
|
63
|
+
unit: firstUnit
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const thresholdScaledValues = thresholds
|
|
67
|
+
.sort()
|
|
68
|
+
.map((threshold) => yScale(threshold));
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
{thresholdScaledValues.map((threshold, index) => {
|
|
73
|
+
return (
|
|
74
|
+
<line
|
|
75
|
+
data-testid={`threshold-${threshold}`}
|
|
76
|
+
key={`threshold-${thresholdLabels?.[index]}-${threshold}`}
|
|
77
|
+
stroke={getColor({
|
|
78
|
+
index,
|
|
79
|
+
theme
|
|
80
|
+
})}
|
|
81
|
+
strokeDasharray="5,5"
|
|
82
|
+
strokeWidth={2}
|
|
83
|
+
x1={0}
|
|
84
|
+
x2={width}
|
|
85
|
+
y1={threshold}
|
|
86
|
+
y2={threshold}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
{thresholdScaledValues.map((threshold, index) => {
|
|
91
|
+
return (
|
|
92
|
+
<line
|
|
93
|
+
data-testid={`threshold-${threshold}-tooltip`}
|
|
94
|
+
key={`threshold-${thresholdLabels?.[index]}-${threshold}-tooltip`}
|
|
95
|
+
stroke="transparent"
|
|
96
|
+
strokeWidth={4}
|
|
97
|
+
x1={0}
|
|
98
|
+
x2={width}
|
|
99
|
+
y1={threshold}
|
|
100
|
+
y2={threshold}
|
|
101
|
+
onMouseEnter={(): void => {
|
|
102
|
+
if (!thresholdLabels?.[index]) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
showTooltip({
|
|
106
|
+
tooltipData: thresholdLabels?.[index],
|
|
107
|
+
tooltipLeft: 0,
|
|
108
|
+
tooltipTop: threshold
|
|
109
|
+
});
|
|
110
|
+
}}
|
|
111
|
+
onMouseLeave={hideTooltip}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default Thresholds;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { Dispatch, SetStateAction, useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
import { useAtom } from 'jotai';
|
|
4
3
|
import { compose, equals, prop, propEq, reject, sortBy, toLower } from 'ramda';
|
|
5
4
|
|
|
6
|
-
import { linesGraphAtom } from '../graphAtoms';
|
|
7
5
|
import { Line } from '../../common/timeSeries/models';
|
|
8
6
|
|
|
9
7
|
import {
|
|
@@ -15,6 +13,8 @@ import {
|
|
|
15
13
|
interface UseFilterLines {
|
|
16
14
|
displayThreshold?: boolean;
|
|
17
15
|
lines: Array<Line>;
|
|
16
|
+
linesGraph: Array<Line> | null;
|
|
17
|
+
setLinesGraph: Dispatch<SetStateAction<Array<Line> | null>>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
interface Result {
|
|
@@ -24,10 +24,10 @@ interface Result {
|
|
|
24
24
|
|
|
25
25
|
const useFilterLines = ({
|
|
26
26
|
displayThreshold = false,
|
|
27
|
-
lines
|
|
27
|
+
lines,
|
|
28
|
+
linesGraph,
|
|
29
|
+
setLinesGraph
|
|
28
30
|
}: UseFilterLines): Result => {
|
|
29
|
-
const [linesGraph, setLinesGraph] = useAtom(linesGraphAtom);
|
|
30
|
-
|
|
31
31
|
const displayedLines = reject(propEq('display', false), linesGraph ?? lines);
|
|
32
32
|
const filterLines = (): Array<Line> => {
|
|
33
33
|
const lineOriginMetric = findLineOfOriginMetricThreshold(lines);
|
|
@@ -45,7 +45,9 @@ const useFilterLines = ({
|
|
|
45
45
|
|
|
46
46
|
useEffect(() => {
|
|
47
47
|
const filteredLines = filterLines();
|
|
48
|
-
if (!lines || !displayThreshold
|
|
48
|
+
if (!lines || !displayThreshold) {
|
|
49
|
+
setLinesGraph(lines);
|
|
50
|
+
|
|
49
51
|
return;
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -8,14 +8,12 @@ interface Props {
|
|
|
8
8
|
|
|
9
9
|
const Point = ({ lineColor, x, y }: Props): JSX.Element => (
|
|
10
10
|
<circle
|
|
11
|
+
cx={x}
|
|
12
|
+
cy={y}
|
|
11
13
|
fill={alpha(lineColor, 0.5)}
|
|
12
14
|
r={4}
|
|
13
15
|
stroke={lineColor}
|
|
14
16
|
strokeWidth={2}
|
|
15
|
-
style={{
|
|
16
|
-
transform: `translate(${x}px, ${y}px)`,
|
|
17
|
-
transition: 'transform 0.08s ease-out'
|
|
18
|
-
}}
|
|
19
17
|
/>
|
|
20
18
|
);
|
|
21
19
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReactNode } from 'react';
|
|
1
|
+
import { Dispatch, ReactNode, SetStateAction } from 'react';
|
|
2
2
|
|
|
3
3
|
import { slice } from 'ramda';
|
|
4
4
|
import { useAtomValue } from 'jotai';
|
|
@@ -28,8 +28,10 @@ interface Props {
|
|
|
28
28
|
limitLegendRows?: boolean;
|
|
29
29
|
lines: Array<Line>;
|
|
30
30
|
renderExtraComponent?: ReactNode;
|
|
31
|
+
setLinesGraph: Dispatch<SetStateAction<Array<Line> | null>>;
|
|
31
32
|
timeSeries: Array<TimeValue>;
|
|
32
33
|
toggable?: boolean;
|
|
34
|
+
xScale;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
const MainLegend = ({
|
|
@@ -39,18 +41,21 @@ const MainLegend = ({
|
|
|
39
41
|
toggable = true,
|
|
40
42
|
limitLegendRows = true,
|
|
41
43
|
renderExtraComponent,
|
|
42
|
-
displayAnchor = true
|
|
44
|
+
displayAnchor = true,
|
|
45
|
+
setLinesGraph,
|
|
46
|
+
xScale
|
|
43
47
|
}: Props): JSX.Element => {
|
|
44
48
|
const { classes, cx } = useStyles({ limitLegendRows });
|
|
45
49
|
const theme = useTheme();
|
|
46
50
|
|
|
47
51
|
const { selectMetricLine, clearHighlight, highlightLine, toggleMetricLine } =
|
|
48
|
-
useLegend({ lines });
|
|
52
|
+
useLegend({ lines, setLinesGraph });
|
|
49
53
|
|
|
50
54
|
const { getFormattedValue } = useInteractiveValues({
|
|
51
55
|
base,
|
|
52
56
|
lines,
|
|
53
|
-
timeSeries
|
|
57
|
+
timeSeries,
|
|
58
|
+
xScale
|
|
54
59
|
});
|
|
55
60
|
|
|
56
61
|
const displayedLines = limitLegendRows
|
|
@@ -3,11 +3,12 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { useAtomValue } from 'jotai';
|
|
4
4
|
import { equals, find, isNil } from 'ramda';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { mousePositionAtom } from '../InteractiveComponents/interactionWithGraphAtoms';
|
|
7
7
|
import {
|
|
8
8
|
formatMetricValue,
|
|
9
9
|
getLineForMetric,
|
|
10
|
-
getMetrics
|
|
10
|
+
getMetrics,
|
|
11
|
+
getTimeValue
|
|
11
12
|
} from '../../common/timeSeries';
|
|
12
13
|
import { Line, TimeValue } from '../../common/timeSeries/models';
|
|
13
14
|
|
|
@@ -21,14 +22,22 @@ interface Props {
|
|
|
21
22
|
base: number;
|
|
22
23
|
lines: Array<Line>;
|
|
23
24
|
timeSeries: Array<TimeValue>;
|
|
25
|
+
xScale;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
const useInteractiveValues = ({
|
|
27
29
|
timeSeries,
|
|
28
30
|
lines,
|
|
29
|
-
base
|
|
31
|
+
base,
|
|
32
|
+
xScale
|
|
30
33
|
}: Props): InteractiveValues => {
|
|
31
|
-
const
|
|
34
|
+
const mousePosition = useAtomValue(mousePositionAtom);
|
|
35
|
+
|
|
36
|
+
const timeValue = getTimeValue({
|
|
37
|
+
timeSeries,
|
|
38
|
+
x: mousePosition?.[0],
|
|
39
|
+
xScale
|
|
40
|
+
});
|
|
32
41
|
|
|
33
42
|
const graphTimeValue = timeSeries?.find((item) =>
|
|
34
43
|
equals(item.timeTick, timeValue?.timeTick)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { Dispatch, SetStateAction, useEffect } from 'react';
|
|
2
2
|
|
|
3
|
-
import { useSetAtom } from 'jotai';
|
|
4
3
|
import {
|
|
5
4
|
equals,
|
|
6
5
|
find,
|
|
@@ -17,7 +16,6 @@ import {
|
|
|
17
16
|
toLower
|
|
18
17
|
} from 'ramda';
|
|
19
18
|
|
|
20
|
-
import { linesGraphAtom } from '../graphAtoms';
|
|
21
19
|
import { Line } from '../../common/timeSeries/models';
|
|
22
20
|
|
|
23
21
|
interface LegendActions {
|
|
@@ -29,11 +27,10 @@ interface LegendActions {
|
|
|
29
27
|
|
|
30
28
|
interface Props {
|
|
31
29
|
lines: Array<Line>;
|
|
30
|
+
setLinesGraph: Dispatch<SetStateAction<Array<Line> | null>>;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
const useLegend = ({ lines }: Props): LegendActions => {
|
|
35
|
-
const setLines = useSetAtom(linesGraphAtom);
|
|
36
|
-
|
|
33
|
+
const useLegend = ({ lines, setLinesGraph }: Props): LegendActions => {
|
|
37
34
|
const displayedLines = reject(propEq('display', false), lines);
|
|
38
35
|
const getLineByMetric = (metric: string): Line =>
|
|
39
36
|
find(propEq('metric', metric), lines) as Line;
|
|
@@ -41,7 +38,7 @@ const useLegend = ({ lines }: Props): LegendActions => {
|
|
|
41
38
|
const toggleMetricLine = (metric): void => {
|
|
42
39
|
const line = getLineByMetric(metric);
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
setLinesGraph([
|
|
45
42
|
...reject(propEq('metric', metric), lines),
|
|
46
43
|
{ ...line, display: !line.display }
|
|
47
44
|
]);
|
|
@@ -56,11 +53,11 @@ const useLegend = ({ lines }: Props): LegendActions => {
|
|
|
56
53
|
|
|
57
54
|
const sortedData = sortBy(compose(toLower, prop('name')), data);
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
setLinesGraph(sortedData);
|
|
60
57
|
};
|
|
61
58
|
|
|
62
59
|
const clearHighlight = (): void => {
|
|
63
|
-
|
|
60
|
+
setLinesGraph(map((line) => ({ ...line, highlight: undefined }), lines));
|
|
64
61
|
};
|
|
65
62
|
|
|
66
63
|
const selectMetricLine = (metric: string): void => {
|
|
@@ -71,7 +68,7 @@ const useLegend = ({ lines }: Props): LegendActions => {
|
|
|
71
68
|
equals(displayedLines.length, 1) && isLineDisplayed;
|
|
72
69
|
|
|
73
70
|
if (isOnlyLineDisplayed || isEmpty(displayedLines)) {
|
|
74
|
-
|
|
71
|
+
setLinesGraph(
|
|
75
72
|
map(
|
|
76
73
|
(line) => ({
|
|
77
74
|
...line,
|
|
@@ -84,7 +81,7 @@ const useLegend = ({ lines }: Props): LegendActions => {
|
|
|
84
81
|
return;
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
|
|
84
|
+
setLinesGraph(
|
|
88
85
|
map(
|
|
89
86
|
(line) => ({
|
|
90
87
|
...line,
|
|
@@ -105,8 +102,8 @@ const useLegend = ({ lines }: Props): LegendActions => {
|
|
|
105
102
|
display: find(propEq('name', line.name), lines)?.display ?? true
|
|
106
103
|
}));
|
|
107
104
|
|
|
108
|
-
|
|
109
|
-
}, []);
|
|
105
|
+
setLinesGraph(newLines);
|
|
106
|
+
}, [lines]);
|
|
110
107
|
|
|
111
108
|
return { clearHighlight, highlightLine, selectMetricLine, toggleMetricLine };
|
|
112
109
|
};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { MutableRefObject, useMemo, useRef } from 'react';
|
|
1
|
+
import { MutableRefObject, useMemo, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import { Group } from '@visx/visx';
|
|
3
|
+
import { Group, Tooltip } from '@visx/visx';
|
|
4
4
|
import { isNil } from 'ramda';
|
|
5
5
|
|
|
6
|
-
import { ClickAwayListener, Skeleton } from '@mui/material';
|
|
6
|
+
import { ClickAwayListener, Fade, Skeleton, useTheme } from '@mui/material';
|
|
7
7
|
|
|
8
8
|
import { getLeftScale, getRightScale, getXScale } from '../common/timeSeries';
|
|
9
|
+
import { Line } from '../common/timeSeries/models';
|
|
9
10
|
|
|
10
11
|
import Axes from './BasicComponents/Axes';
|
|
11
12
|
import Grids from './BasicComponents/Grids';
|
|
@@ -30,17 +31,27 @@ import {
|
|
|
30
31
|
} from './models';
|
|
31
32
|
import { useIntersection } from './useLineChartIntersection';
|
|
32
33
|
import { CurveType } from './BasicComponents/Lines/models';
|
|
34
|
+
import Thresholds from './BasicComponents/Thresholds';
|
|
33
35
|
|
|
34
36
|
interface Props extends LineChartProps {
|
|
35
37
|
curve: CurveType;
|
|
38
|
+
disabledThresholds?: boolean;
|
|
36
39
|
graphData: Data;
|
|
37
40
|
graphInterval: GraphInterval;
|
|
38
41
|
graphRef: MutableRefObject<HTMLDivElement | null>;
|
|
39
42
|
legend?: LegendModel;
|
|
40
43
|
marginBottom: number;
|
|
41
44
|
shapeLines?: GlobalAreaLines;
|
|
45
|
+
thresholdLabels?: Array<string>;
|
|
46
|
+
thresholdUnit?: string;
|
|
47
|
+
thresholds?: Array<number>;
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
const baseStyles = {
|
|
51
|
+
...Tooltip.defaultStyles,
|
|
52
|
+
textAlign: 'center'
|
|
53
|
+
};
|
|
54
|
+
|
|
44
55
|
const LineChart = ({
|
|
45
56
|
graphData,
|
|
46
57
|
height = 500,
|
|
@@ -58,15 +69,32 @@ const LineChart = ({
|
|
|
58
69
|
graphRef,
|
|
59
70
|
header,
|
|
60
71
|
curve,
|
|
61
|
-
marginBottom
|
|
72
|
+
marginBottom,
|
|
73
|
+
thresholds,
|
|
74
|
+
thresholdUnit,
|
|
75
|
+
thresholdLabels,
|
|
76
|
+
disabledThresholds
|
|
62
77
|
}: Props): JSX.Element => {
|
|
78
|
+
const { classes } = useStyles();
|
|
79
|
+
|
|
80
|
+
const theme = useTheme();
|
|
81
|
+
|
|
82
|
+
const [linesGraph, setLinesGraph] = useState<Array<Line> | null>(null);
|
|
63
83
|
const graphSvgRef = useRef<SVGSVGElement | null>(null);
|
|
64
84
|
|
|
65
|
-
const { classes } = useStyles();
|
|
66
85
|
const { isInViewport } = useIntersection({ element: graphRef?.current });
|
|
67
86
|
|
|
68
87
|
const legendRef = useRef<HTMLDivElement | null>(null);
|
|
69
88
|
|
|
89
|
+
const {
|
|
90
|
+
tooltipOpen: thresholdTooltipOpen,
|
|
91
|
+
tooltipLeft: thresholdTooltipLeft,
|
|
92
|
+
tooltipTop: thresholdTooltipTop,
|
|
93
|
+
tooltipData: thresholdTooltipData,
|
|
94
|
+
hideTooltip: hideThresholdTooltip,
|
|
95
|
+
showTooltip: showThresholdTooltip
|
|
96
|
+
} = Tooltip.useTooltip();
|
|
97
|
+
|
|
70
98
|
const graphWidth = width > 0 ? width - margin.left - margin.right : 0;
|
|
71
99
|
const graphHeight =
|
|
72
100
|
(height || 0) > 0
|
|
@@ -81,7 +109,9 @@ const LineChart = ({
|
|
|
81
109
|
|
|
82
110
|
const { displayedLines, newLines } = useFilterLines({
|
|
83
111
|
displayThreshold: canDisplayThreshold(shapeLines?.areaThresholdLines),
|
|
84
|
-
lines
|
|
112
|
+
lines,
|
|
113
|
+
linesGraph,
|
|
114
|
+
setLinesGraph
|
|
85
115
|
});
|
|
86
116
|
|
|
87
117
|
const xScale = useMemo(
|
|
@@ -98,6 +128,8 @@ const LineChart = ({
|
|
|
98
128
|
getLeftScale({
|
|
99
129
|
dataLines: displayedLines,
|
|
100
130
|
dataTimeSeries: timeSeries,
|
|
131
|
+
thresholdUnit,
|
|
132
|
+
thresholds: (!disabledThresholds && thresholds) || [],
|
|
101
133
|
valueGraphHeight: graphHeight - 35
|
|
102
134
|
}),
|
|
103
135
|
[displayedLines, timeSeries, graphHeight]
|
|
@@ -108,6 +140,8 @@ const LineChart = ({
|
|
|
108
140
|
getRightScale({
|
|
109
141
|
dataLines: displayedLines,
|
|
110
142
|
dataTimeSeries: timeSeries,
|
|
143
|
+
thresholdUnit,
|
|
144
|
+
thresholds: (!disabledThresholds && thresholds) || [],
|
|
111
145
|
valueGraphHeight: graphHeight - 35
|
|
112
146
|
}),
|
|
113
147
|
[timeSeries, displayedLines, graphHeight]
|
|
@@ -201,6 +235,20 @@ const LineChart = ({
|
|
|
201
235
|
}}
|
|
202
236
|
zoomData={{ ...zoomPreview }}
|
|
203
237
|
/>
|
|
238
|
+
|
|
239
|
+
{!disabledThresholds && (
|
|
240
|
+
<Thresholds
|
|
241
|
+
displayedLines={displayedLines}
|
|
242
|
+
hideTooltip={hideThresholdTooltip}
|
|
243
|
+
leftScale={leftScale}
|
|
244
|
+
rightScale={rightScale}
|
|
245
|
+
showTooltip={showThresholdTooltip}
|
|
246
|
+
thresholdLabels={thresholdLabels}
|
|
247
|
+
thresholdUnit={thresholdUnit}
|
|
248
|
+
thresholds={thresholds}
|
|
249
|
+
width={graphWidth}
|
|
250
|
+
/>
|
|
251
|
+
)}
|
|
204
252
|
</Group.Group>
|
|
205
253
|
</svg>
|
|
206
254
|
{displayTooltip && (
|
|
@@ -218,6 +266,20 @@ const LineChart = ({
|
|
|
218
266
|
xScale={xScale}
|
|
219
267
|
/>
|
|
220
268
|
)}
|
|
269
|
+
<Fade in={thresholdTooltipOpen}>
|
|
270
|
+
<Tooltip.Tooltip
|
|
271
|
+
left={thresholdTooltipLeft}
|
|
272
|
+
style={{
|
|
273
|
+
...baseStyles,
|
|
274
|
+
backgroundColor: theme.palette.background.paper,
|
|
275
|
+
color: theme.palette.text.primary,
|
|
276
|
+
transform: `translate(${graphWidth / 2}px, -10px)`
|
|
277
|
+
}}
|
|
278
|
+
top={thresholdTooltipTop}
|
|
279
|
+
>
|
|
280
|
+
{thresholdTooltipData}
|
|
281
|
+
</Tooltip.Tooltip>
|
|
282
|
+
</Fade>
|
|
221
283
|
</div>
|
|
222
284
|
</ClickAwayListener>
|
|
223
285
|
{displayLegend && (
|
|
@@ -227,7 +289,9 @@ const LineChart = ({
|
|
|
227
289
|
displayAnchor={displayAnchor?.displayGuidingLines ?? true}
|
|
228
290
|
lines={newLines}
|
|
229
291
|
renderExtraComponent={legend?.renderExtraComponent}
|
|
292
|
+
setLinesGraph={setLinesGraph}
|
|
230
293
|
timeSeries={timeSeries}
|
|
294
|
+
xScale={xScale}
|
|
231
295
|
/>
|
|
232
296
|
</div>
|
|
233
297
|
)}
|
|
@@ -29,10 +29,11 @@ import exclusionPeriodFirstPeriod from './mockedData/exclusionPeriodFirstPeriod.
|
|
|
29
29
|
import exclusionPeriodSecondPeriod from './mockedData/exclusionPeriodSecondPeriod.json';
|
|
30
30
|
import exclusionPeriodThirdPeriod from './mockedData/exclusionPeriodThirdPeriod.json';
|
|
31
31
|
import dataLastDayForword from './mockedData/lastDayForward.json';
|
|
32
|
-
import
|
|
32
|
+
import dataLastDayThreshold from './mockedData/lastDayThreshold.json';
|
|
33
33
|
import dataLastMonth from './mockedData/lastMonth.json';
|
|
34
34
|
import dataLastWeek from './mockedData/lastWeek.json';
|
|
35
35
|
import dataZoomPreview from './mockedData/zoomPreview.json';
|
|
36
|
+
import dataLastDay from './mockedData/lastDay.json';
|
|
36
37
|
import { Interval, ThresholdType, TooltipData } from './models';
|
|
37
38
|
|
|
38
39
|
import WrapperLineChart from './index';
|
|
@@ -75,7 +76,7 @@ const Threshold = (args): JSX.Element => {
|
|
|
75
76
|
|
|
76
77
|
<WrapperLineChart
|
|
77
78
|
{...args}
|
|
78
|
-
data={
|
|
79
|
+
data={dataLastDayThreshold}
|
|
79
80
|
shapeLines={{
|
|
80
81
|
areaThresholdLines: [
|
|
81
82
|
{ type: ThresholdType.basic },
|
|
@@ -123,7 +124,7 @@ const LineChartAndCLS = (args): JSX.Element => {
|
|
|
123
124
|
useEffect(() => {
|
|
124
125
|
setTimeout(() => {
|
|
125
126
|
setLoading(false);
|
|
126
|
-
setData(
|
|
127
|
+
setData(dataLastDayThreshold as unknown as LineChartData);
|
|
127
128
|
}, 100000);
|
|
128
129
|
}, []);
|
|
129
130
|
|
|
@@ -187,7 +188,7 @@ const LineChartAndTimePeriod = (args): JSX.Element => {
|
|
|
187
188
|
}
|
|
188
189
|
|
|
189
190
|
if (start.includes(`${defaultStart.split('T')[0]}`)) {
|
|
190
|
-
setCurrentData(
|
|
191
|
+
setCurrentData(dataLastDayThreshold as unknown as LineChartData);
|
|
191
192
|
|
|
192
193
|
return;
|
|
193
194
|
}
|
|
@@ -281,7 +282,7 @@ const LineChartAndExclusionPeriod = (args): JSX.Element => {
|
|
|
281
282
|
</ButtonGroup>
|
|
282
283
|
<WrapperLineChart
|
|
283
284
|
{...args}
|
|
284
|
-
data={
|
|
285
|
+
data={dataLastDayThreshold as unknown as LineChartData}
|
|
285
286
|
shapeLines={{
|
|
286
287
|
areaThresholdLines: [
|
|
287
288
|
{
|
|
@@ -303,7 +304,7 @@ const Template: Story = {
|
|
|
303
304
|
render: (args) => (
|
|
304
305
|
<WrapperLineChart
|
|
305
306
|
{...args}
|
|
306
|
-
data={
|
|
307
|
+
data={dataLastDayThreshold as unknown as LineChartData}
|
|
307
308
|
shapeLines={{
|
|
308
309
|
areaThresholdLines: [
|
|
309
310
|
{
|
|
@@ -381,3 +382,33 @@ export const withCLS: Story = {
|
|
|
381
382
|
start: defaultStart
|
|
382
383
|
}
|
|
383
384
|
};
|
|
385
|
+
|
|
386
|
+
export const withThresholds: Story = {
|
|
387
|
+
argTypes,
|
|
388
|
+
args: {
|
|
389
|
+
...argumentsData,
|
|
390
|
+
thresholdLabels: ['threshold 1', 'threshold 2'],
|
|
391
|
+
thresholds: [300, 350]
|
|
392
|
+
},
|
|
393
|
+
render: (args) => (
|
|
394
|
+
<WrapperLineChart
|
|
395
|
+
{...args}
|
|
396
|
+
data={dataLastDay as unknown as LineChartData}
|
|
397
|
+
/>
|
|
398
|
+
)
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
export const withThresholdsAndUnit: Story = {
|
|
402
|
+
argTypes,
|
|
403
|
+
args: {
|
|
404
|
+
...argumentsData,
|
|
405
|
+
thresholdUnit: '%',
|
|
406
|
+
thresholds: [65, 79]
|
|
407
|
+
},
|
|
408
|
+
render: (args) => (
|
|
409
|
+
<WrapperLineChart
|
|
410
|
+
{...args}
|
|
411
|
+
data={dataLastDay as unknown as LineChartData}
|
|
412
|
+
/>
|
|
413
|
+
)
|
|
414
|
+
};
|
|
@@ -25,12 +25,16 @@ dayjs.extend(timezonePlugin);
|
|
|
25
25
|
interface Props extends Partial<LineChartProps> {
|
|
26
26
|
curve?: CurveType;
|
|
27
27
|
data?: LineChartData;
|
|
28
|
+
disabledThresholds?: boolean;
|
|
28
29
|
end: string;
|
|
29
30
|
legend: LegendModel;
|
|
30
31
|
loading: boolean;
|
|
31
32
|
marginBottom?: number;
|
|
32
33
|
shapeLines?: GlobalAreaLines;
|
|
33
34
|
start: string;
|
|
35
|
+
thresholdLabels?: Array<string>;
|
|
36
|
+
thresholdUnit?: string;
|
|
37
|
+
thresholds?: Array<number>;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
const WrapperLineChart = ({
|
|
@@ -50,12 +54,16 @@ const WrapperLineChart = ({
|
|
|
50
54
|
legend,
|
|
51
55
|
header,
|
|
52
56
|
curve = Curve.curveLinear,
|
|
53
|
-
marginBottom = 0
|
|
57
|
+
marginBottom = 0,
|
|
58
|
+
thresholds,
|
|
59
|
+
thresholdUnit,
|
|
60
|
+
thresholdLabels,
|
|
61
|
+
disabledThresholds
|
|
54
62
|
}: Props): JSX.Element | null => {
|
|
55
63
|
const { adjustedData } = useLineChartData({ data, end, start });
|
|
56
64
|
const lineChartRef = useRef<HTMLDivElement | null>(null);
|
|
57
65
|
|
|
58
|
-
if (loading
|
|
66
|
+
if (loading && !adjustedData) {
|
|
59
67
|
return (
|
|
60
68
|
<LoadingSkeleton
|
|
61
69
|
displayTitleSkeleton={header?.displayTitle ?? true}
|
|
@@ -64,6 +72,10 @@ const WrapperLineChart = ({
|
|
|
64
72
|
);
|
|
65
73
|
}
|
|
66
74
|
|
|
75
|
+
if (!adjustedData) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
return (
|
|
68
80
|
<div
|
|
69
81
|
ref={lineChartRef as MutableRefObject<HTMLDivElement>}
|
|
@@ -79,6 +91,7 @@ const WrapperLineChart = ({
|
|
|
79
91
|
annotationEvent={annotationEvent}
|
|
80
92
|
axis={axis}
|
|
81
93
|
curve={curve}
|
|
94
|
+
disabledThresholds={disabledThresholds}
|
|
82
95
|
displayAnchor={displayAnchor}
|
|
83
96
|
graphData={adjustedData}
|
|
84
97
|
graphInterval={{ end, start }}
|
|
@@ -89,6 +102,9 @@ const WrapperLineChart = ({
|
|
|
89
102
|
loading={loading}
|
|
90
103
|
marginBottom={marginBottom}
|
|
91
104
|
shapeLines={shapeLines}
|
|
105
|
+
thresholdLabels={thresholdLabels}
|
|
106
|
+
thresholdUnit={thresholdUnit}
|
|
107
|
+
thresholds={thresholds}
|
|
92
108
|
timeShiftZones={timeShiftZones}
|
|
93
109
|
tooltip={tooltip}
|
|
94
110
|
width={width ?? responsiveWidth}
|
|
@@ -326,10 +326,19 @@ const getYScale = ({
|
|
|
326
326
|
const getScale = ({
|
|
327
327
|
graphValues,
|
|
328
328
|
height,
|
|
329
|
-
stackedValues
|
|
329
|
+
stackedValues,
|
|
330
|
+
thresholds
|
|
330
331
|
}): ScaleLinear<number, number> => {
|
|
331
|
-
const minValue = min(
|
|
332
|
-
|
|
332
|
+
const minValue = Math.min(
|
|
333
|
+
getMin(graphValues),
|
|
334
|
+
getMin(stackedValues),
|
|
335
|
+
Math.min(...thresholds)
|
|
336
|
+
);
|
|
337
|
+
const maxValue = Math.max(
|
|
338
|
+
getMax(graphValues),
|
|
339
|
+
getMax(stackedValues),
|
|
340
|
+
Math.max(...thresholds)
|
|
341
|
+
);
|
|
333
342
|
|
|
334
343
|
const upperRangeValue = minValue === maxValue && maxValue === 0 ? height : 0;
|
|
335
344
|
|
|
@@ -343,10 +352,17 @@ const getScale = ({
|
|
|
343
352
|
const getLeftScale = ({
|
|
344
353
|
dataLines,
|
|
345
354
|
dataTimeSeries,
|
|
346
|
-
valueGraphHeight
|
|
355
|
+
valueGraphHeight,
|
|
356
|
+
thresholds,
|
|
357
|
+
thresholdUnit
|
|
347
358
|
}: AxeScale): ScaleLinear<number, number> => {
|
|
348
359
|
const [firstUnit, , thirdUnit] = getUnits(dataLines);
|
|
349
360
|
|
|
361
|
+
const shouldApplyThresholds =
|
|
362
|
+
equals(thresholdUnit, firstUnit) ||
|
|
363
|
+
equals(thresholdUnit, thirdUnit) ||
|
|
364
|
+
!thresholdUnit;
|
|
365
|
+
|
|
350
366
|
const graphValues = isNil(thirdUnit)
|
|
351
367
|
? getMetricValuesForUnit({
|
|
352
368
|
lines: dataLines,
|
|
@@ -370,7 +386,12 @@ const getLeftScale = ({
|
|
|
370
386
|
})
|
|
371
387
|
: [0];
|
|
372
388
|
|
|
373
|
-
return getScale({
|
|
389
|
+
return getScale({
|
|
390
|
+
graphValues,
|
|
391
|
+
height: valueGraphHeight,
|
|
392
|
+
stackedValues,
|
|
393
|
+
thresholds: shouldApplyThresholds ? thresholds : []
|
|
394
|
+
});
|
|
374
395
|
};
|
|
375
396
|
|
|
376
397
|
const getXScale = ({
|
|
@@ -386,7 +407,9 @@ const getXScale = ({
|
|
|
386
407
|
const getRightScale = ({
|
|
387
408
|
dataLines,
|
|
388
409
|
dataTimeSeries,
|
|
389
|
-
valueGraphHeight
|
|
410
|
+
valueGraphHeight,
|
|
411
|
+
thresholds,
|
|
412
|
+
thresholdUnit
|
|
390
413
|
}: AxeScale): ScaleLinear<number, number> => {
|
|
391
414
|
const [, secondUnit] = getUnits(dataLines);
|
|
392
415
|
|
|
@@ -396,6 +419,8 @@ const getRightScale = ({
|
|
|
396
419
|
unit: secondUnit
|
|
397
420
|
});
|
|
398
421
|
|
|
422
|
+
const shouldApplyThresholds = equals(thresholdUnit, secondUnit);
|
|
423
|
+
|
|
399
424
|
const secondUnitHasStackedLines = isNil(secondUnit)
|
|
400
425
|
? false
|
|
401
426
|
: hasUnitStackedLines({ lines: dataLines, unit: secondUnit });
|
|
@@ -407,7 +432,12 @@ const getRightScale = ({
|
|
|
407
432
|
})
|
|
408
433
|
: [0];
|
|
409
434
|
|
|
410
|
-
return getScale({
|
|
435
|
+
return getScale({
|
|
436
|
+
graphValues,
|
|
437
|
+
height: valueGraphHeight,
|
|
438
|
+
stackedValues,
|
|
439
|
+
thresholds: shouldApplyThresholds ? thresholds : []
|
|
440
|
+
});
|
|
411
441
|
};
|
|
412
442
|
|
|
413
443
|
const formatMetricValue = ({
|
|
@@ -476,7 +506,10 @@ const getTimeValue = ({
|
|
|
476
506
|
xScale,
|
|
477
507
|
timeSeries,
|
|
478
508
|
marginLeft = margin.left
|
|
479
|
-
}: TimeValueProps): TimeValue => {
|
|
509
|
+
}: TimeValueProps): TimeValue | null => {
|
|
510
|
+
if (isNil(x)) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
480
513
|
const date = xScale.invert(x - marginLeft);
|
|
481
514
|
const index = bisectDate(getDates(timeSeries), date);
|
|
482
515
|
|
|
@@ -65,6 +65,8 @@ export interface Xscale {
|
|
|
65
65
|
export interface AxeScale {
|
|
66
66
|
dataLines: Array<Line>;
|
|
67
67
|
dataTimeSeries: Array<TimeValue>;
|
|
68
|
+
thresholdUnit?: string;
|
|
69
|
+
thresholds: Array<number>;
|
|
68
70
|
valueGraphHeight: number;
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -117,6 +119,6 @@ export interface YScales {
|
|
|
117
119
|
export interface TimeValueProps {
|
|
118
120
|
marginLeft?: number;
|
|
119
121
|
timeSeries: Array<TimeValue>;
|
|
120
|
-
x
|
|
122
|
+
x?: number;
|
|
121
123
|
xScale: ScaleLinear<number, number>;
|
|
122
124
|
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { equals, flatten, has, isEmpty, join, pipe, pluck } from 'ramda';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
|
|
4
4
|
import { LineChartData, useFetchQuery } from '../..';
|
|
5
5
|
|
|
6
6
|
import { ServiceMetric } from './models';
|
|
7
7
|
|
|
8
|
+
interface CustomTimePeriod {
|
|
9
|
+
end: string;
|
|
10
|
+
start: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
interface UseMetricsQueryProps {
|
|
9
14
|
baseEndpoint: string;
|
|
10
15
|
metrics: Array<ServiceMetric>;
|
|
11
16
|
refreshInterval?: number | false;
|
|
12
|
-
timePeriod?:
|
|
17
|
+
timePeriod?: {
|
|
18
|
+
end?: string | null;
|
|
19
|
+
start?: string | null;
|
|
20
|
+
timePeriodType: number;
|
|
21
|
+
};
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
interface UseMetricsQueryState {
|
|
@@ -20,24 +29,24 @@ interface UseMetricsQueryState {
|
|
|
20
29
|
start: string;
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
enum TimePeriod {
|
|
24
|
-
lastDay = 'last_day'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
32
|
const getStartEndFromTimePeriod = (
|
|
28
|
-
timePeriod:
|
|
33
|
+
timePeriod: number
|
|
29
34
|
): { end: string; start: string } => {
|
|
30
|
-
return
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end: dayjs().toISOString(),
|
|
35
|
-
start: dayjs().subtract(1, 'day').toISOString()
|
|
36
|
-
})
|
|
37
|
-
]
|
|
38
|
-
])(timePeriod);
|
|
35
|
+
return {
|
|
36
|
+
end: dayjs().toISOString(),
|
|
37
|
+
start: dayjs().subtract(timePeriod, 'hour').toISOString()
|
|
38
|
+
};
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
+
const isCustomTimePeriod = (
|
|
42
|
+
timePeriod:
|
|
43
|
+
| number
|
|
44
|
+
| {
|
|
45
|
+
end?: string | null;
|
|
46
|
+
start?: string | null;
|
|
47
|
+
}
|
|
48
|
+
): boolean => has('end', timePeriod) && has('start', timePeriod);
|
|
49
|
+
|
|
41
50
|
interface PerformanceGraphData extends Omit<LineChartData, 'global'> {
|
|
42
51
|
base: number;
|
|
43
52
|
}
|
|
@@ -45,7 +54,9 @@ interface PerformanceGraphData extends Omit<LineChartData, 'global'> {
|
|
|
45
54
|
const useGraphQuery = ({
|
|
46
55
|
metrics,
|
|
47
56
|
baseEndpoint,
|
|
48
|
-
timePeriod =
|
|
57
|
+
timePeriod = {
|
|
58
|
+
timePeriodType: 1
|
|
59
|
+
},
|
|
49
60
|
refreshInterval = false
|
|
50
61
|
}: UseMetricsQueryProps): UseMetricsQueryState => {
|
|
51
62
|
const metricIds = pipe(
|
|
@@ -55,16 +66,32 @@ const useGraphQuery = ({
|
|
|
55
66
|
join(',')
|
|
56
67
|
)(metrics);
|
|
57
68
|
|
|
58
|
-
const
|
|
69
|
+
const timePeriodToUse = equals(timePeriod?.timePeriodType, -1)
|
|
70
|
+
? {
|
|
71
|
+
end: timePeriod.end,
|
|
72
|
+
start: timePeriod.start
|
|
73
|
+
}
|
|
74
|
+
: timePeriod?.timePeriodType;
|
|
59
75
|
|
|
60
76
|
const {
|
|
61
77
|
data: graphData,
|
|
62
78
|
isFetching,
|
|
63
79
|
isLoading
|
|
64
80
|
} = useFetchQuery<PerformanceGraphData>({
|
|
65
|
-
getEndpoint: () =>
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
getEndpoint: () => {
|
|
82
|
+
if (isCustomTimePeriod(timePeriodToUse)) {
|
|
83
|
+
return `${baseEndpoint}?metricIds=[${metricIds}]&start=${
|
|
84
|
+
(timePeriodToUse as CustomTimePeriod).start
|
|
85
|
+
}&end=${(timePeriodToUse as CustomTimePeriod).end}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const { end, start } = getStartEndFromTimePeriod(
|
|
89
|
+
timePeriodToUse as number
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return `${baseEndpoint}?metricIds=[${metricIds}]&start=${start}&end=${end}`;
|
|
93
|
+
},
|
|
94
|
+
getQueryKey: () => ['graph', metricIds, timePeriod],
|
|
68
95
|
queryOptions: {
|
|
69
96
|
enabled: !isEmpty(metricIds),
|
|
70
97
|
refetchInterval: refreshInterval,
|
|
@@ -83,6 +110,10 @@ const useGraphQuery = ({
|
|
|
83
110
|
}
|
|
84
111
|
: undefined;
|
|
85
112
|
|
|
113
|
+
const { end, start } = isCustomTimePeriod(timePeriodToUse)
|
|
114
|
+
? (timePeriodToUse as CustomTimePeriod)
|
|
115
|
+
: getStartEndFromTimePeriod(timePeriodToUse as number);
|
|
116
|
+
|
|
86
117
|
return {
|
|
87
118
|
end,
|
|
88
119
|
graphData: formattedGraphData,
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { always, cond, equals } from 'ramda';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
globalRefreshInterval?: number;
|
|
5
|
+
refreshInterval: 'default' | 'custom' | 'manual';
|
|
6
|
+
refreshIntervalCustom?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useRefreshInterval = ({
|
|
10
|
+
refreshInterval,
|
|
11
|
+
refreshIntervalCustom,
|
|
12
|
+
globalRefreshInterval
|
|
13
|
+
}: Props): number | false => {
|
|
14
|
+
const refreshIntervalToUse = cond([
|
|
15
|
+
[equals('default'), always(globalRefreshInterval)],
|
|
16
|
+
[equals('custom'), always(refreshIntervalCustom)],
|
|
17
|
+
[equals('manual'), always(0)]
|
|
18
|
+
])(refreshInterval);
|
|
19
|
+
|
|
20
|
+
return refreshIntervalToUse ? refreshIntervalToUse * 1000 : false;
|
|
21
|
+
};
|