@centreon/ui 23.10.61 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@centreon/ui",
3
- "version": "23.10.61",
3
+ "version": "23.10.62",
4
4
  "description": "Centreon UI Components",
5
5
  "scripts": {
6
6
  "eslint": "eslint ./src --ext .js,.jsx,.ts,.tsx --max-warnings 0",
@@ -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 || filteredLines.length <= 0) {
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
 
@@ -44,6 +44,7 @@ export const changeMousePositionAndTimeValueDerivedAtom = atom(
44
44
 
45
45
  return;
46
46
  }
47
+
47
48
  set(mousePositionAtom, position);
48
49
 
49
50
  set(timeValueAtom, timeValue);
@@ -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 { timeValueAtom } from '../InteractiveComponents/interactionWithGraphAtoms';
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 timeValue = useAtomValue(timeValueAtom);
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
- setLines([
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
- setLines(sortedData);
56
+ setLinesGraph(sortedData);
60
57
  };
61
58
 
62
59
  const clearHighlight = (): void => {
63
- setLines(map((line) => ({ ...line, highlight: undefined }), lines));
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
- setLines(
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
- setLines(
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
- setLines(newLines);
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
  )}
@@ -1,6 +1,3 @@
1
1
  import { atom } from 'jotai';
2
2
 
3
- import { Line } from '../common/timeSeries/models';
4
-
5
- export const linesGraphAtom = atom<Array<Line> | null>(null);
6
3
  export const timeTickGraphAtom = atom<Date | null>(null);
@@ -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 dataLastDay from './mockedData/lastDayThreshold.json';
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={dataLastDay}
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(dataLastDay as unknown as LineChartData);
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(dataLastDay as unknown as LineChartData);
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={dataLastDay as unknown as LineChartData}
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={dataLastDay as unknown as LineChartData}
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 || !adjustedData) {
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(getMin(graphValues), getMin(stackedValues));
332
- const maxValue = max(getMax(graphValues), getMax(stackedValues));
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({ graphValues, height: valueGraphHeight, stackedValues });
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({ graphValues, height: valueGraphHeight, stackedValues });
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: number;
122
+ x?: number;
121
123
  xScale: ScaleLinear<number, number>;
122
124
  }
@@ -1,15 +1,24 @@
1
- import { T, always, cond, flatten, isEmpty, join, pipe, pluck } from 'ramda';
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?: 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: TimePeriod
33
+ timePeriod: number
29
34
  ): { end: string; start: string } => {
30
- return cond([
31
- [
32
- T,
33
- always({
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 = TimePeriod.lastDay,
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 { end, start } = getStartEndFromTimePeriod(timePeriod);
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
- `${baseEndpoint}?metricIds=[${metricIds}]&start=${start}&end=${end}`,
67
- getQueryKey: () => ['graph', metricIds],
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,
@@ -61,8 +61,7 @@ const useStyles = makeStyles<{
61
61
  color: theme.palette.text.secondary,
62
62
  margin: theme.spacing(1, 0, 1, 0),
63
63
  width: '90%'
64
- },
65
- height: '100%'
64
+ }
66
65
  },
67
66
  modalCloseButton: {
68
67
  position: 'absolute',
@@ -18,3 +18,4 @@ export * from './sanitizedHTML';
18
18
  export * from './docsURL';
19
19
  export * from './useInfiniteScrollListing';
20
20
  export * from './useLicenseExpirationWarning';
21
+ export * from './useRefreshInterval';
@@ -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
+ };