@centreon/ui 24.10.12 → 24.10.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/package.json +3 -2
  2. package/public/mockServiceWorker.js +1 -1
  3. package/src/Button/Icon/index.tsx +3 -1
  4. package/src/Dashboard/Dashboard.styles.ts +3 -4
  5. package/src/Dashboard/DashboardLayout.stories.tsx +1 -1
  6. package/src/Dashboard/Grid.tsx +11 -17
  7. package/src/Dashboard/Layout.tsx +27 -56
  8. package/src/Dialog/UnsavedChanges/index.tsx +15 -13
  9. package/src/Dialog/UnsavedChanges/translatedLabels.ts +15 -13
  10. package/src/Form/Form.tsx +0 -1
  11. package/src/Form/Inputs/Autocomplete.tsx +1 -1
  12. package/src/Form/Inputs/ConnectedAutocomplete.tsx +5 -2
  13. package/src/Form/Inputs/Grid.tsx +7 -1
  14. package/src/Form/Inputs/Radio.tsx +1 -1
  15. package/src/Form/Inputs/Switch.tsx +1 -1
  16. package/src/Form/Inputs/Text.tsx +1 -1
  17. package/src/Form/Inputs/index.tsx +25 -24
  18. package/src/Form/Inputs/models.ts +2 -0
  19. package/src/Graph/BarChart/BarChart.cypress.spec.tsx +3 -3
  20. package/src/Graph/BarChart/BarChart.tsx +24 -31
  21. package/src/Graph/BarChart/BarGroup.tsx +32 -59
  22. package/src/Graph/BarChart/BarStack.tsx +64 -13
  23. package/src/Graph/BarChart/MemoizedGroup.tsx +123 -0
  24. package/src/Graph/BarChart/ResponsiveBarChart.tsx +21 -7
  25. package/src/Graph/BarStack/BarStack.cypress.spec.tsx +87 -9
  26. package/src/Graph/BarStack/BarStack.stories.tsx +13 -4
  27. package/src/Graph/BarStack/BarStack.styles.ts +57 -33
  28. package/src/Graph/BarStack/Graph.tsx +173 -0
  29. package/src/Graph/BarStack/GraphAndLegend.tsx +117 -0
  30. package/src/Graph/BarStack/ResponsiveBarStack.tsx +61 -168
  31. package/src/Graph/BarStack/constants.ts +5 -0
  32. package/src/Graph/BarStack/models.ts +0 -1
  33. package/src/Graph/BarStack/useGraphAndLegend.ts +84 -0
  34. package/src/Graph/BarStack/useResponsiveBarStack.ts +73 -97
  35. package/src/Graph/Chart/Chart.cypress.spec.tsx +14 -26
  36. package/src/Graph/Chart/Chart.stories.tsx +1 -1
  37. package/src/Graph/Chart/Chart.tsx +53 -37
  38. package/src/Graph/Chart/InteractiveComponents/AnchorPoint/GuidingLines.tsx +3 -3
  39. package/src/Graph/Chart/InteractiveComponents/AnchorPoint/useTickGraph.ts +19 -6
  40. package/src/Graph/Chart/Legend/Legend.styles.ts +25 -11
  41. package/src/Graph/Chart/Legend/index.tsx +6 -24
  42. package/src/Graph/Chart/index.tsx +34 -43
  43. package/src/Graph/Chart/models.ts +0 -1
  44. package/src/Graph/Chart/useChartData.ts +19 -1
  45. package/src/Graph/HeatMap/ResponsiveHeatMap.tsx +20 -2
  46. package/src/Graph/HeatMap/model.ts +6 -2
  47. package/src/Graph/Legend/Legend.styles.ts +10 -0
  48. package/src/Graph/Legend/Legend.tsx +6 -1
  49. package/src/Graph/SingleBar/ResponsiveSingleBar.tsx +9 -10
  50. package/src/Graph/SingleBar/ThresholdLine.tsx +6 -6
  51. package/src/Graph/Text/Text.styles.ts +2 -2
  52. package/src/Graph/Text/Text.tsx +23 -10
  53. package/src/Graph/Timeline/ResponsiveTimeline.tsx +152 -0
  54. package/src/Graph/Timeline/Timeline.cypress.spec.tsx +148 -0
  55. package/src/Graph/Timeline/Timeline.stories.tsx +91 -0
  56. package/src/Graph/Timeline/Timeline.tsx +28 -0
  57. package/src/Graph/Timeline/index.ts +1 -0
  58. package/src/Graph/Timeline/models.ts +20 -0
  59. package/src/Graph/Timeline/timeline.styles.ts +11 -0
  60. package/src/Graph/Timeline/translatedLabel.ts +6 -0
  61. package/src/Graph/Timeline/useTimeline.ts +90 -0
  62. package/src/Graph/Tree/Links.tsx +2 -2
  63. package/src/Graph/Tree/Tree.tsx +2 -2
  64. package/src/Graph/Tree/constants.ts +1 -1
  65. package/src/Graph/common/Axes/index.tsx +1 -1
  66. package/src/Graph/common/Axes/useAxisY.ts +8 -4
  67. package/src/Graph/common/BaseChart/BaseChart.tsx +3 -12
  68. package/src/Graph/common/BaseChart/ChartSvgWrapper.tsx +12 -4
  69. package/src/Graph/common/BaseChart/Header/index.tsx +3 -1
  70. package/src/Graph/common/BaseChart/useComputeBaseChartDimensions.ts +23 -11
  71. package/src/Graph/common/BaseChart/useComputeYAxisMaxCharacters.ts +92 -0
  72. package/src/Graph/common/models.ts +7 -8
  73. package/src/Graph/common/timeSeries/index.test.ts +1 -1
  74. package/src/Graph/common/timeSeries/index.ts +56 -29
  75. package/src/Graph/common/timeSeries/models.ts +2 -0
  76. package/src/Graph/common/utils.ts +51 -3
  77. package/src/Graph/index.ts +4 -1
  78. package/src/Graph/mockedData/lastDayWithNullValues.json +6 -6
  79. package/src/Graph/mockedData/pingServiceLinesBars.json +47 -47
  80. package/src/Icon/DowntimeIcon.tsx +8 -1
  81. package/src/Icon/FlappingIcon.tsx +22 -0
  82. package/src/Icon/index.ts +1 -0
  83. package/src/InputField/Select/Autocomplete/Connected/Multi/index.test.tsx +21 -1
  84. package/src/InputField/Select/Autocomplete/Connected/index.test.tsx +2 -2
  85. package/src/InputField/Select/Autocomplete/Connected/index.tsx +52 -15
  86. package/src/InputField/Select/Autocomplete/Multi/index.stories.tsx +19 -0
  87. package/src/InputField/Select/Autocomplete/Multi/index.tsx +8 -5
  88. package/src/InputField/Select/Autocomplete/index.tsx +79 -54
  89. package/src/InputField/Text/index.tsx +6 -4
  90. package/src/InputField/translatedLabels.ts +2 -0
  91. package/src/Listing/ActionBar/index.tsx +1 -1
  92. package/src/Listing/Listing.styles.ts +3 -3
  93. package/src/Listing/index.tsx +40 -37
  94. package/src/Listing/models.ts +0 -8
  95. package/src/Listing/useStyleTable.ts +58 -32
  96. package/src/MultiSelectEntries/index.tsx +2 -0
  97. package/src/PopoverMenu/index.tsx +2 -9
  98. package/src/SortableItems/index.tsx +0 -1
  99. package/src/ThemeProvider/index.tsx +1 -1
  100. package/src/ThemeProvider/palettes.ts +6 -0
  101. package/src/TimePeriods/CustomTimePeriod/PopoverCustomTimePeriod/PickersStartEndDate.tsx +2 -3
  102. package/src/TimePeriods/DateTimePickerInput.tsx +3 -1
  103. package/src/api/buildListingEndpoint/getSearchQueryParameterValue.ts +7 -1
  104. package/src/api/buildListingEndpoint/models.ts +1 -0
  105. package/src/api/customFetch.ts +4 -1
  106. package/src/api/models.ts +9 -0
  107. package/src/api/useGraphQuery/index.ts +117 -20
  108. package/src/api/useGraphQuery/models.ts +1 -0
  109. package/src/api/useMutationQuery/index.ts +1 -1
  110. package/src/components/DataTable/DataTable.styles.ts +1 -1
  111. package/src/components/DataTable/EmptyState/DataTableEmptyState.styles.ts +2 -1
  112. package/src/components/DataTable/EmptyState/DataTableEmptyState.tsx +4 -1
  113. package/src/components/DataTable/Item/DataTableItem.styles.ts +28 -2
  114. package/src/components/DataTable/Item/DataTableItem.tsx +19 -4
  115. package/src/components/Form/FormActions.tsx +21 -12
  116. package/src/components/Layout/AreaIndicator.tsx +1 -1
  117. package/src/components/Layout/PageLayout/PageLayout.styles.ts +2 -7
  118. package/src/components/Layout/PageLayout/PageLayoutBody.tsx +0 -1
  119. package/src/components/Zoom/Zoom.tsx +9 -2
  120. package/src/components/Zoom/ZoomContent.tsx +143 -136
  121. package/src/components/Zoom/models.ts +18 -15
  122. package/src/components/Zoom/useMinimap.ts +5 -8
  123. package/src/components/Zoom/useZoom.ts +3 -3
  124. package/src/index.ts +2 -0
  125. package/src/utils/index.ts +1 -0
  126. package/src/utils/useLocale/index.ts +9 -0
  127. package/src/utils/useLocale/useLocale.cypress.spec.tsx +38 -0
  128. package/src/utils/useLocaleDateTimeFormat/index.ts +4 -2
  129. package/src/utils/usePluralizedTranslation.ts +2 -3
  130. package/src/Graph/common/timeSeries/index.test.ts-E +0 -622
  131. package/src/components/CrudPage/Actions/Actions.styles.ts +0 -16
  132. package/src/components/CrudPage/Actions/Actions.tsx +0 -24
  133. package/src/components/CrudPage/Actions/AddButton.tsx +0 -23
  134. package/src/components/CrudPage/Actions/Filters.tsx +0 -25
  135. package/src/components/CrudPage/Actions/Search.tsx +0 -31
  136. package/src/components/CrudPage/Actions/useSearch.tsx +0 -24
  137. package/src/components/CrudPage/Columns/Actions.tsx +0 -88
  138. package/src/components/CrudPage/CrudPage.cypress.spec.tsx +0 -559
  139. package/src/components/CrudPage/CrudPage.stories.tsx +0 -278
  140. package/src/components/CrudPage/CrudPageRoot.tsx +0 -142
  141. package/src/components/CrudPage/DeleteModal.tsx +0 -77
  142. package/src/components/CrudPage/Form/AddModal.tsx +0 -35
  143. package/src/components/CrudPage/Form/Buttons.tsx +0 -98
  144. package/src/components/CrudPage/Form/UpdateModal.tsx +0 -60
  145. package/src/components/CrudPage/Listing.tsx +0 -63
  146. package/src/components/CrudPage/atoms.ts +0 -30
  147. package/src/components/CrudPage/hooks/useDeleteItem.ts +0 -53
  148. package/src/components/CrudPage/hooks/useGetItem.ts +0 -36
  149. package/src/components/CrudPage/hooks/useGetItems.ts +0 -67
  150. package/src/components/CrudPage/hooks/useListingQueryKey.ts +0 -31
  151. package/src/components/CrudPage/index.tsx +0 -7
  152. package/src/components/CrudPage/models.ts +0 -118
  153. package/src/components/CrudPage/utils.ts +0 -4
@@ -4,6 +4,7 @@ import {
4
4
  compose,
5
5
  flatten,
6
6
  groupBy,
7
+ isEmpty,
7
8
  isNil,
8
9
  lensPath,
9
10
  pipe,
@@ -29,6 +30,8 @@ interface Props {
29
30
  start?: string;
30
31
  }
31
32
 
33
+ const getBoolean = (value) => Boolean(Number(value));
34
+
32
35
  const useGraphData = ({ data, end, start }: Props): GraphDataResult => {
33
36
  const adjustedDataRef = useRef<Data>();
34
37
 
@@ -36,13 +39,27 @@ const useGraphData = ({ data, end, start }: Props): GraphDataResult => {
36
39
  if (isNil(data)) {
37
40
  return data;
38
41
  }
42
+
43
+ if (isEmpty(data.metrics) || isEmpty(data.times)) {
44
+ return undefined;
45
+ }
46
+
39
47
  const metricsGroupedByColor = groupBy(
40
48
  (metric) => metric.ds_data.ds_color_line
41
49
  )(data?.metrics || []);
42
50
 
43
51
  const newMetrics = Object.entries(metricsGroupedByColor).map(
44
52
  ([color, value]) => {
45
- return value?.map((metric, index) =>
53
+ const adjustedValue = value?.map((item) => ({
54
+ ...item,
55
+ ds_data: {
56
+ ...item?.ds_data,
57
+ ds_invert: getBoolean(item?.ds_data?.ds_invert),
58
+ ds_filled: getBoolean(item?.ds_data?.ds_filled)
59
+ }
60
+ }));
61
+
62
+ return adjustedValue?.map((metric, index) =>
46
63
  set(
47
64
  lensPath(['ds_data', 'ds_color_line']),
48
65
  emphasizeCurveColor({ color, index }),
@@ -70,6 +87,7 @@ const useGraphData = ({ data, end, start }: Props): GraphDataResult => {
70
87
  const { title } = dataWithAdjustedMetricsColor.global;
71
88
 
72
89
  const newLineData = adjustGraphData(dataWithAdjustedMetricsColor).lines;
90
+
73
91
  const sortedLines = sortBy(compose(toLower, prop('name')), newLineData);
74
92
 
75
93
  adjustedDataRef.current = {
@@ -1,4 +1,4 @@
1
- import { useMemo } from 'react';
1
+ import { useMemo, useRef } from 'react';
2
2
 
3
3
  import { scaleLinear } from '@visx/scale';
4
4
  import { T, equals, gt, lt } from 'ramda';
@@ -13,6 +13,7 @@ import { HeatMapProps } from './model';
13
13
  const gap = 8;
14
14
  const maxTileSize = 120;
15
15
  const smallestTileSize = 44;
16
+ const toleratedRangeWidth = 10;
16
17
 
17
18
  const ResponsiveHeatMap = <TData,>({
18
19
  width,
@@ -28,6 +29,7 @@ const ResponsiveHeatMap = <TData,>({
28
29
  width: number;
29
30
  }): JSX.Element | null => {
30
31
  const { classes, cx } = useHeatMapStyles();
32
+ const previousTileSize = useRef(0);
31
33
 
32
34
  const tileSize = useMemo(() => {
33
35
  const scaleWidth = scaleLinear({
@@ -44,6 +46,13 @@ const ResponsiveHeatMap = <TData,>({
44
46
  const theoricalTotalTilesWidth =
45
47
  tilesLength * tileWidth + (tilesLength - 1) * gap;
46
48
 
49
+ const canUpdateTileSize =
50
+ Math.abs(tileWidth - previousTileSize.current) > toleratedRangeWidth;
51
+
52
+ if (!canUpdateTileSize) {
53
+ return previousTileSize.current;
54
+ }
55
+
47
56
  if (
48
57
  (lt(height, maxTileSize) ||
49
58
  (lt(width, 680) && gt(maxTotalTilesWidth, width))) &&
@@ -60,6 +69,8 @@ const ResponsiveHeatMap = <TData,>({
60
69
  }, [width, tiles, height]);
61
70
 
62
71
  const isSmallestSize = equals(tileSize, smallestTileSize);
72
+ const isMediumSize = !isSmallestSize && lt(tileSize, 90);
73
+ previousTileSize.current = tileSize;
63
74
 
64
75
  if (equals(width, 0)) {
65
76
  return null;
@@ -100,7 +111,14 @@ const ResponsiveHeatMap = <TData,>({
100
111
  position="right-start"
101
112
  >
102
113
  <div className={classes.heatMapTileContent}>
103
- {children({ backgroundColor, data, id, isSmallestSize })}
114
+ {children({
115
+ backgroundColor,
116
+ data,
117
+ id,
118
+ isSmallestSize,
119
+ tileSize,
120
+ isMediumSize
121
+ })}
104
122
  </div>
105
123
  </Tooltip>
106
124
  </Box>
@@ -1,4 +1,4 @@
1
- import { ReactElement } from 'react';
1
+ import type { ReactElement } from 'react';
2
2
 
3
3
  interface Tile<TData> {
4
4
  backgroundColor: string;
@@ -11,6 +11,8 @@ interface ChildrenProps<TData> {
11
11
  data: TData;
12
12
  id: string;
13
13
  isSmallestSize: boolean;
14
+ isMediumSize?: boolean;
15
+ tileSize?: number;
14
16
  }
15
17
 
16
18
  export interface HeatMapProps<TData> {
@@ -19,7 +21,9 @@ export interface HeatMapProps<TData> {
19
21
  backgroundColor,
20
22
  id,
21
23
  data,
22
- isSmallestSize
24
+ isSmallestSize,
25
+ tileSize,
26
+ isMediumSize
23
27
  }: ChildrenProps<TData>) => ReactElement | boolean | null;
24
28
  displayTooltipCondition?: (data: TData) => boolean;
25
29
  tileSizeFixed?: boolean;
@@ -0,0 +1,10 @@
1
+ import { makeStyles } from 'tss-react/mui';
2
+
3
+ export const useStyles = makeStyles()((theme) => ({
4
+ container: {
5
+ padding: theme.spacing(1),
6
+ border: `1px solid ${theme.palette.divider}`,
7
+ borderRadius: theme.shape.borderRadius,
8
+ width: 'fit-content'
9
+ }
10
+ }));
@@ -1,9 +1,13 @@
1
1
  import { LegendOrdinal } from '@visx/legend';
2
2
  import { scaleOrdinal } from '@visx/scale';
3
3
 
4
+ import { equals } from 'ramda';
5
+ import { useStyles } from './Legend.styles';
4
6
  import { LegendProps } from './models';
5
7
 
6
8
  const Legend = ({ scale, direction = 'column' }: LegendProps): JSX.Element => {
9
+ const { classes } = useStyles();
10
+
7
11
  const legendScale = scaleOrdinal({
8
12
  domain: scale.domain,
9
13
  range: scale.range
@@ -12,8 +16,9 @@ const Legend = ({ scale, direction = 'column' }: LegendProps): JSX.Element => {
12
16
  return (
13
17
  <LegendOrdinal
14
18
  direction={direction}
15
- labelMargin="0 16px 0 0"
16
19
  scale={legendScale}
20
+ labelMargin={equals(direction, 'row') ? '0 12px 0 0' : '0 0 0 0'}
21
+ className={classes.container}
17
22
  />
18
23
  );
19
24
  };
@@ -4,7 +4,7 @@ import { animated, useSpring } from '@react-spring/web';
4
4
  import { scaleLinear } from '@visx/scale';
5
5
  import { Bar } from '@visx/shape';
6
6
  import { Group, Tooltip } from '@visx/visx';
7
- import { equals, flatten, head, lt, pluck } from 'ramda';
7
+ import { clamp, equals, flatten, head, pluck } from 'ramda';
8
8
 
9
9
  import { Box, alpha, useTheme } from '@mui/material';
10
10
 
@@ -40,8 +40,6 @@ const ResponsiveSingleBar = ({
40
40
  const { classes } = useTooltipStyles();
41
41
  const theme = useTheme();
42
42
 
43
- const isSmallHeight = lt(height, 150);
44
-
45
43
  const metric = getMetricWithLatestData(data) as Metric;
46
44
  const latestMetricData = head(metric.data) as number;
47
45
  const thresholdValues = thresholds.enabled
@@ -72,7 +70,7 @@ const ResponsiveSingleBar = ({
72
70
  [latestMetricData, thresholds, theme]
73
71
  );
74
72
 
75
- const isSmall = equals(size, 'small') || isSmallHeight;
73
+ const isSmall = equals(size, 'small');
76
74
 
77
75
  const textStyle = isSmall ? theme.typography.h6 : theme.typography.h4;
78
76
 
@@ -118,14 +116,15 @@ const ResponsiveSingleBar = ({
118
116
 
119
117
  const springStyle = useSpring({ width: metricBarWidth });
120
118
 
121
- const barHeight = isSmallHeight ? barHeights.small : barHeights[size];
122
-
123
119
  const barY = groupMargin + (isSmall ? 0 : 2 * margins.top);
124
120
 
125
- const realBarHeight =
126
- !isSmall && textHeight + barHeight > height
127
- ? height - textHeight - 2 * margins.top
128
- : barHeight;
121
+ const realBarHeight = isSmall
122
+ ? barHeights.small
123
+ : clamp(
124
+ barHeights.small,
125
+ barHeights.medium,
126
+ height - textHeight - 2 * margins.top
127
+ );
129
128
 
130
129
  return (
131
130
  <div
@@ -63,15 +63,15 @@ export const ThresholdLine = ({
63
63
  strokeDasharray="6, 6"
64
64
  strokeWidth={2}
65
65
  x1={scaledValue}
66
- x2={scaledValue}
66
+ x2={scaledValue + 1}
67
67
  y1={
68
68
  isSmall
69
- ? groupMargin - lineMargin + 6
69
+ ? groupMargin - lineMargin
70
70
  : groupMargin + lineMargin + margins.top
71
71
  }
72
72
  y2={
73
73
  isSmall
74
- ? barHeight + groupMargin - lineMargin + margins.top - 2
74
+ ? barHeight + groupMargin - lineMargin + margins.top - 6
75
75
  : barHeight + groupMargin + lineMargin + 2 * margins.top
76
76
  }
77
77
  />
@@ -80,15 +80,15 @@ export const ThresholdLine = ({
80
80
  stroke="transparent"
81
81
  strokeWidth={5}
82
82
  x1={scaledValue}
83
- x2={scaledValue}
83
+ x2={scaledValue + 1}
84
84
  y1={
85
85
  isSmall
86
- ? groupMargin - lineMargin + 5
86
+ ? groupMargin - lineMargin
87
87
  : groupMargin + lineMargin + margins.top
88
88
  }
89
89
  y2={
90
90
  isSmall
91
- ? barHeight + groupMargin - lineMargin + margins.top + 5
91
+ ? barHeight + groupMargin - lineMargin + margins.top - 6
92
92
  : barHeight + groupMargin + lineMargin + 2 * margins.top
93
93
  }
94
94
  onMouseEnter={onMouseEnter}
@@ -11,13 +11,13 @@ export const useTextStyles = makeStyles()((theme) => ({
11
11
  gap: theme.spacing(1),
12
12
  justifyContent: 'center'
13
13
  },
14
- threshold: {
14
+ thresholdLabel: {
15
15
  textAlign: 'center'
16
16
  },
17
17
  thresholds: {
18
18
  display: 'flex',
19
19
  flexDirection: 'row',
20
- gap: theme.spacing(5),
20
+ gap: theme.spacing(1),
21
21
  whiteSpace: 'nowrap',
22
22
  width: '100%'
23
23
  },
@@ -10,6 +10,8 @@ import {
10
10
  } from '../common/timeSeries';
11
11
  import { getColorFromDataAndTresholds } from '../common/utils';
12
12
 
13
+ import { type ReactElement } from 'react';
14
+ import useResizeObserver from 'use-resize-observer';
13
15
  import { useTextStyles } from './Text.styles';
14
16
 
15
17
  export interface Props {
@@ -21,6 +23,8 @@ export interface Props {
21
23
  warning: string;
22
24
  };
23
25
  thresholds: Thresholds;
26
+ prefThresholds?: number;
27
+ minThresholds?: string;
24
28
  }
25
29
 
26
30
  export const Text = ({
@@ -28,10 +32,13 @@ export const Text = ({
28
32
  data,
29
33
  displayAsRaw,
30
34
  labels,
31
- baseColor
32
- }: Props): JSX.Element | null => {
35
+ baseColor,
36
+ prefThresholds = 14,
37
+ minThresholds
38
+ }: Props): ReactElement | null => {
33
39
  const theme = useTheme();
34
40
  const { classes, cx } = useTextStyles();
41
+ const { ref, width = 0 } = useResizeObserver();
35
42
 
36
43
  if (isNil(data)) {
37
44
  return null;
@@ -62,11 +69,15 @@ export const Text = ({
62
69
  })
63
70
  );
64
71
 
72
+ const canDisplayThresholdLabel = width > 150;
73
+ const warningLabel = canDisplayThresholdLabel ? `${labels.warning}: ` : '';
74
+ const criticalLabel = canDisplayThresholdLabel ? `${labels.critical}: ` : '';
75
+
65
76
  return (
66
- <div className={classes.graphText}>
77
+ <div className={classes.graphText} ref={ref}>
67
78
  <FluidTypography
68
79
  max="40px"
69
- pref={16}
80
+ pref={14}
70
81
  sx={{ color, fontWeight: 'bold', textAlign: 'center' }}
71
82
  text={
72
83
  formatMetricValueWithUnit({
@@ -80,18 +91,20 @@ export const Text = ({
80
91
  {thresholds.enabled && (
81
92
  <div className={classes.thresholds}>
82
93
  <FluidTypography
83
- containerClassName={cx(classes.threshold, classes.warning)}
94
+ containerClassName={cx(classes.thresholdLabel, classes.warning)}
84
95
  max="30px"
85
- pref={14}
86
- text={`${labels.warning}: ${warningThresholdLabels.join(' - ')}`}
96
+ pref={prefThresholds}
97
+ text={`${warningLabel}${warningThresholdLabels.join(' - ')}`}
87
98
  variant="h5"
99
+ min={minThresholds}
88
100
  />
89
101
  <FluidTypography
90
- containerClassName={cx(classes.threshold, classes.critical)}
102
+ containerClassName={cx(classes.thresholdLabel, classes.critical)}
91
103
  max="30px"
92
- pref={14}
93
- text={`${labels.critical}: ${criticalThresholdLabels.join(' - ')}`}
104
+ pref={prefThresholds}
105
+ text={`${criticalLabel}${criticalThresholdLabels.join(' - ')}`}
94
106
  variant="h5"
107
+ min={minThresholds}
95
108
  />
96
109
  </div>
97
110
  )}
@@ -0,0 +1,152 @@
1
+ import {
2
+ dateTimeFormat,
3
+ getXAxisTickFormat,
4
+ useLocaleDateTimeFormat
5
+ } from '@centreon/ui';
6
+
7
+ import { Typography, useTheme } from '@mui/material';
8
+
9
+ import dayjs from 'dayjs';
10
+ import timezonePlugin from 'dayjs/plugin/timezone';
11
+ import utc from 'dayjs/plugin/utc';
12
+
13
+ import { userAtom } from '@centreon/ui-context';
14
+ import { Axis } from '@visx/visx';
15
+
16
+ import { scaleTime } from '@visx/scale';
17
+ import { BarRounded } from '@visx/shape';
18
+ import { useAtomValue } from 'jotai';
19
+ import { equals } from 'ramda';
20
+ import { useCallback } from 'react';
21
+ import { Tooltip } from '../../components';
22
+ import { margins } from '../common/margins';
23
+ import type { TimelineProps } from './models';
24
+ import { useStyles } from './timeline.styles';
25
+ import { useTimeline } from './useTimeline';
26
+
27
+ dayjs.extend(utc);
28
+ dayjs.extend(timezonePlugin);
29
+
30
+ interface Props extends TimelineProps {
31
+ width: number;
32
+ height: number;
33
+ }
34
+
35
+ const axisPadding = 4;
36
+
37
+ const Timeline = ({
38
+ data,
39
+ startDate,
40
+ endDate,
41
+ width,
42
+ height,
43
+ TooltipContent,
44
+ tooltipClassName
45
+ }: Props) => {
46
+ const { classes, cx } = useStyles();
47
+ const { format } = useLocaleDateTimeFormat();
48
+ const { timezone } = useAtomValue(userAtom);
49
+
50
+ const theme = useTheme();
51
+
52
+ const xScale = scaleTime({
53
+ domain: [new Date(startDate), new Date(endDate)],
54
+ range: [margins.left, width - margins.right],
55
+ clamp: true
56
+ });
57
+
58
+ const numTicks = Math.min(Math.ceil(width / 82), 12);
59
+
60
+ const { getTimeDifference } = useTimeline();
61
+
62
+ const getFormattedStart = useCallback(
63
+ (start) =>
64
+ format({
65
+ date: dayjs(start).tz(timezone).toDate(),
66
+ formatString: dateTimeFormat
67
+ }),
68
+ [dateTimeFormat, timezone]
69
+ );
70
+
71
+ const getFormattedEnd = useCallback(
72
+ (end) =>
73
+ format({
74
+ date: dayjs(end).tz(timezone).toDate(),
75
+ formatString: dateTimeFormat
76
+ }),
77
+ [dateTimeFormat, timezone]
78
+ );
79
+
80
+ return (
81
+ <svg width={width} height={height + axisPadding}>
82
+ {data.map(({ start, end, color }, idx) => (
83
+ <Tooltip
84
+ hasCaret
85
+ classes={{
86
+ tooltip: cx(classes.tooltip, tooltipClassName)
87
+ }}
88
+ followCursor={false}
89
+ key={`rect-${start}--${end}`}
90
+ label={
91
+ TooltipContent ? (
92
+ <TooltipContent
93
+ start={getFormattedStart(start)}
94
+ end={getFormattedEnd(end)}
95
+ color={color}
96
+ duration={getTimeDifference({
97
+ start: dayjs(start),
98
+ end: dayjs(end)
99
+ })}
100
+ />
101
+ ) : (
102
+ <div style={{ color }}>
103
+ <Typography variant="body2">
104
+ {getTimeDifference({ start: dayjs(start), end: dayjs(end) })}
105
+ </Typography>
106
+ <Typography variant="body2">{`${format({ date: start, formatString: 'L LT' })} - ${format({ date: end, formatString: 'L LT' })}`}</Typography>
107
+ </div>
108
+ )
109
+ }
110
+ position="top"
111
+ >
112
+ <g>
113
+ <BarRounded
114
+ x={xScale(dayjs(start).tz(timezone))}
115
+ y={0}
116
+ width={
117
+ xScale(dayjs(end).tz(timezone)) -
118
+ xScale(dayjs(start).tz(timezone))
119
+ }
120
+ height={height - margins.bottom}
121
+ fill={color}
122
+ left={equals(idx, 0)}
123
+ radius={4}
124
+ right={equals(idx, data.length - 1)}
125
+ />
126
+ </g>
127
+ </Tooltip>
128
+ ))}
129
+
130
+ <Axis.AxisBottom
131
+ top={height - margins.bottom + axisPadding}
132
+ scale={xScale}
133
+ numTicks={numTicks}
134
+ tickFormat={(value) =>
135
+ format({
136
+ date: new Date(value),
137
+ formatString: getXAxisTickFormat({ end: endDate, start: startDate })
138
+ })
139
+ }
140
+ stroke={theme.palette.text.primary}
141
+ tickStroke={theme.palette.text.primary}
142
+ tickLabelProps={() => ({
143
+ fill: theme.palette.text.primary,
144
+ fontSize: theme.typography.caption.fontSize,
145
+ textAnchor: 'middle'
146
+ })}
147
+ />
148
+ </svg>
149
+ );
150
+ };
151
+
152
+ export default Timeline;
@@ -0,0 +1,148 @@
1
+ import { userAtom } from '@centreon/ui-context';
2
+ import { Provider, createStore } from 'jotai';
3
+ import Timeline from './Timeline';
4
+ import { Tooltip } from './models';
5
+
6
+ const data = [
7
+ {
8
+ start: '2024-09-09T10:57:42+02:00',
9
+ end: '2024-09-09T11:15:00+02:00',
10
+ color: 'green'
11
+ },
12
+ {
13
+ start: '2024-09-09T11:15:00+02:00',
14
+ end: '2024-09-09T11:30:00+02:00',
15
+ color: 'red'
16
+ },
17
+ {
18
+ start: '2024-09-09T11:30:00+02:00',
19
+ end: '2024-09-09T11:45:00+02:00',
20
+ color: 'gray'
21
+ },
22
+ {
23
+ start: '2024-09-09T11:45:00+02:00',
24
+ end: '2024-09-09T12:00:00+02:00',
25
+ color: 'green'
26
+ },
27
+ {
28
+ start: '2024-09-09T12:00:00+02:00',
29
+ end: '2024-09-09T12:20:00+02:00',
30
+ color: 'red'
31
+ },
32
+ {
33
+ start: '2024-09-09T12:20:00+02:00',
34
+ end: '2024-09-09T12:40:00+02:00',
35
+ color: 'gray'
36
+ },
37
+ {
38
+ start: '2024-09-09T12:40:00+02:00',
39
+ end: '2024-09-09T12:57:42+02:00',
40
+ color: 'green'
41
+ }
42
+ ];
43
+
44
+ const startDate = '2024-09-09T10:57:42+02:00';
45
+ const endDate = '2024-09-09T12:57:42+02:00';
46
+
47
+ const TooltipContent = ({ start, end, color, duration }: Tooltip) => (
48
+ <div
49
+ data-testid="tooltip-content"
50
+ style={{
51
+ display: 'flex',
52
+ flexDirection: 'column',
53
+ justifyContent: 'center',
54
+ alignItems: 'center',
55
+ gap: '10px',
56
+ padding: '5px'
57
+ }}
58
+ >
59
+ <span>{start}</span>
60
+ <span>{end}</span>
61
+ <span>{color}</span>
62
+ <span>{duration}</span>
63
+ </div>
64
+ );
65
+
66
+ const store = createStore();
67
+ store.set(userAtom, { timezone: 'Europe/Paris', locale: 'en' });
68
+
69
+ const initialize = (displayDefaultTooltip = true): void => {
70
+ cy.mount({
71
+ Component: (
72
+ <Provider store={store}>
73
+ <div
74
+ style={{
75
+ height: '100px',
76
+ width: '70%'
77
+ }}
78
+ >
79
+ <Timeline
80
+ data={data}
81
+ startDate={startDate}
82
+ endDate={endDate}
83
+ TooltipContent={displayDefaultTooltip ? undefined : TooltipContent}
84
+ />
85
+ </div>
86
+ </Provider>
87
+ )
88
+ });
89
+ };
90
+
91
+ describe('Timeline', () => {
92
+ it('checks that the correct number of bars are rendered', () => {
93
+ initialize();
94
+
95
+ cy.get('path').should('have.length', data.length);
96
+
97
+ cy.makeSnapshot();
98
+ });
99
+
100
+ it('checks that each bar has the correct color', () => {
101
+ initialize();
102
+
103
+ data.forEach(({ color }, index) => {
104
+ cy.get('path').eq(index).should('have.attr', 'fill', color);
105
+ });
106
+ });
107
+
108
+ it('displays tooltip with correct information when hovered over a bar', () => {
109
+ initialize(false);
110
+
111
+ cy.get('path').first().trigger('mouseover');
112
+
113
+ cy.get('[data-testid="tooltip-content"]').within(() => {
114
+ cy.contains('09/09/2024 10:57 AM').should('be.visible');
115
+ cy.contains('09/09/2024 11:15 AM').should('be.visible');
116
+ cy.contains('green').should('be.visible');
117
+ cy.contains('17 minutes').should('be.visible');
118
+ });
119
+
120
+ cy.makeSnapshot();
121
+ });
122
+
123
+ it('displays the default tooltip with correct information when hovered over a bar', () => {
124
+ initialize();
125
+
126
+ cy.get('path').first().trigger('mouseover');
127
+
128
+ cy.get('[role="tooltip"]').within(() => {
129
+ cy.contains('09/09/2024 10:57 AM')
130
+ .should('be.visible')
131
+ .and('have.css', 'color', 'rgb(0, 128, 0)');
132
+ cy.contains('09/09/2024 11:15 AM')
133
+ .should('be.visible')
134
+ .and('have.css', 'color', 'rgb(0, 128, 0)');
135
+ cy.contains('17 minutes')
136
+ .should('be.visible')
137
+ .and('have.css', 'color', 'rgb(0, 128, 0)');
138
+ });
139
+
140
+ cy.makeSnapshot();
141
+ });
142
+
143
+ it('displays correct tick labels on the x-axis', () => {
144
+ initialize();
145
+
146
+ cy.get('.visx-axis-bottom .visx-axis-tick').first().contains('11:00');
147
+ });
148
+ });