@scality/core-ui 0.170.0 → 0.172.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/__mocks__/uuid.js +11 -0
  2. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  3. package/dist/components/barchartv2/Barchart.component.js +4 -4
  4. package/dist/components/barchartv2/BarchartTooltip.d.ts +11 -0
  5. package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -0
  6. package/dist/components/barchartv2/BarchartTooltip.js +27 -0
  7. package/dist/components/chartlegend/ChartLegend.d.ts +3 -1
  8. package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -1
  9. package/dist/components/chartlegend/ChartLegend.js +2 -2
  10. package/dist/components/chartlegend/ChartLegendWrapper.d.ts +3 -1
  11. package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -1
  12. package/dist/components/chartlegend/ChartLegendWrapper.js +43 -9
  13. package/dist/components/charttooltip/ChartTooltip.d.ts +13 -0
  14. package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -0
  15. package/dist/components/charttooltip/ChartTooltip.js +49 -0
  16. package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts +4 -0
  17. package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts.map +1 -1
  18. package/dist/components/globalhealthbar/GlobalHealthBar.component.js +4 -0
  19. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts +10 -0
  20. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -0
  21. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +78 -0
  22. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +18 -0
  23. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -0
  24. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +95 -0
  25. package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts +7 -0
  26. package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts.map +1 -0
  27. package/dist/components/globalhealthbar/components/HealthBarXAxis.js +25 -0
  28. package/dist/components/globalhealthbar/healthBarUtils.d.ts +77 -0
  29. package/dist/components/globalhealthbar/healthBarUtils.d.ts.map +1 -0
  30. package/dist/components/globalhealthbar/healthBarUtils.js +196 -0
  31. package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts +2 -0
  32. package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts.map +1 -0
  33. package/dist/components/globalhealthbar/healthBarUtils.spec.js +391 -0
  34. package/dist/components/globalhealthbar/useHealthBarData.d.ts +18 -0
  35. package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -0
  36. package/dist/components/globalhealthbar/useHealthBarData.js +46 -0
  37. package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts +2 -0
  38. package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts.map +1 -0
  39. package/dist/components/globalhealthbar/useHealthBarData.spec.js +207 -0
  40. package/dist/components/icon/Icon.component.d.ts +2 -0
  41. package/dist/components/icon/Icon.component.d.ts.map +1 -1
  42. package/dist/components/icon/Icon.component.js +2 -0
  43. package/dist/components/linetemporalchart/ChartUtil.d.ts.map +1 -1
  44. package/dist/components/linetemporalchart/ChartUtil.js +12 -0
  45. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +8 -5
  46. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
  47. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +95 -100
  48. package/dist/components/sparkline/sparkline.component.d.ts +16 -0
  49. package/dist/components/sparkline/sparkline.component.d.ts.map +1 -0
  50. package/dist/components/sparkline/sparkline.component.js +20 -0
  51. package/dist/components/text/Text.component.d.ts +2 -1
  52. package/dist/components/text/Text.component.d.ts.map +1 -1
  53. package/dist/components/text/Text.component.js +6 -1
  54. package/dist/next.d.ts +4 -2
  55. package/dist/next.d.ts.map +1 -1
  56. package/dist/next.js +4 -2
  57. package/package.json +4 -2
  58. package/src/lib/components/barchartv2/Barchart.component.tsx +5 -4
  59. package/src/lib/components/barchartv2/{ChartTooltip.test.tsx → BarchartTooltip.test.tsx} +35 -12
  60. package/src/lib/components/barchartv2/BarchartTooltip.tsx +89 -0
  61. package/src/lib/components/chartlegend/ChartLegend.tsx +4 -2
  62. package/src/lib/components/chartlegend/ChartLegendWrapper.test.tsx +197 -0
  63. package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +65 -9
  64. package/src/lib/components/charttooltip/ChartTooltip.tsx +83 -0
  65. package/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx +4 -1
  66. package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +203 -0
  67. package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +173 -0
  68. package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +94 -0
  69. package/src/lib/components/globalhealthbar/healthBarUtils.spec.ts +701 -0
  70. package/src/lib/components/globalhealthbar/healthBarUtils.ts +311 -0
  71. package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +487 -0
  72. package/src/lib/components/globalhealthbar/useHealthBarData.ts +74 -0
  73. package/src/lib/components/icon/Icon.component.tsx +2 -0
  74. package/src/lib/components/linetemporalchart/ChartUtil.ts +26 -0
  75. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +272 -229
  76. package/src/lib/components/sparkline/sparkline.component.tsx +54 -0
  77. package/src/lib/components/text/Text.component.tsx +15 -2
  78. package/src/lib/next.ts +12 -2
  79. package/stories/BarChart/barchart.stories.tsx +7 -1
  80. package/stories/GlobalHealthBar/globalhealthbarRecharts.stories.tsx +145 -0
  81. package/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx +5 -0
  82. package/stories/InlineInput/InlineInput.stories.tsx +7 -1
  83. package/stories/globalhealthbar.stories.tsx +25 -5
  84. package/stories/linetimeseriechart.stories.tsx +217 -1
  85. package/stories/sparkline.stories.tsx +168 -0
  86. package/dist/components/barchartv2/ChartTooltip.d.ts +0 -14
  87. package/dist/components/barchartv2/ChartTooltip.d.ts.map +0 -1
  88. package/dist/components/barchartv2/ChartTooltip.js +0 -41
  89. package/src/lib/components/barchartv2/ChartTooltip.tsx +0 -106
@@ -5,9 +5,22 @@ import {
5
5
  ReactNode,
6
6
  useMemo,
7
7
  useCallback,
8
+ useEffect,
9
+ useRef,
8
10
  } from 'react';
11
+ import { v4 as uuidv4 } from 'uuid';
9
12
  import { ChartColors } from '../../style/theme';
10
13
 
14
+ export const useChartId = (): string => {
15
+ const idRef = useRef<string | null>(null);
16
+
17
+ if (idRef.current === null) {
18
+ idRef.current = uuidv4();
19
+ }
20
+
21
+ return idRef.current;
22
+ };
23
+
11
24
  export type ChartLegendState = {
12
25
  selectedResources: string[];
13
26
  addSelectedResource: (resource: string) => void;
@@ -18,23 +31,65 @@ export type ChartLegendState = {
18
31
  getColor: (resource: string) => string | undefined;
19
32
  listResources: () => string[];
20
33
  isOnlyOneSelected: () => boolean;
34
+ register: (chartId: string, seriesNames: string[]) => void;
21
35
  };
22
36
 
23
37
  const ChartLegendContext = createContext<ChartLegendState | null>(null);
24
38
 
25
39
  export type ChartLegendWrapperProps = {
26
40
  children: ReactNode;
27
- colorSet: Record<string, ChartColors | string>;
41
+ colorSet:
42
+ | Record<string, ChartColors | string>
43
+ | ((seriesNames: string[]) => Record<string, ChartColors | string>);
28
44
  };
29
45
 
30
46
  export const ChartLegendWrapper = ({
31
47
  children,
32
48
  colorSet,
33
49
  }: ChartLegendWrapperProps) => {
34
- const allResources = Object.keys(colorSet);
50
+ const [registeredColorSets, setRegisteredColorSets] = useState<
51
+ Record<string, string[]>
52
+ >({});
53
+
54
+ const [internalColorSet, setInternalColorSet] = useState<
55
+ Record<string, ChartColors | string>
56
+ >(() => {
57
+ return typeof colorSet === 'function' ? {} : colorSet;
58
+ });
59
+
60
+ useEffect(() => {
61
+ if (typeof colorSet === 'function') {
62
+ const allUniqueSeriesNames = Array.from(
63
+ new Set(Object.values(registeredColorSets).flat()),
64
+ );
65
+
66
+ if (allUniqueSeriesNames.length > 0) {
67
+ const newColorSet = colorSet(allUniqueSeriesNames);
68
+ setInternalColorSet(newColorSet);
69
+ }
70
+ } else {
71
+ setInternalColorSet(colorSet);
72
+ }
73
+ }, [registeredColorSets, colorSet]);
74
+
75
+ const allResources = useMemo(
76
+ () => Object.keys(internalColorSet),
77
+ [internalColorSet],
78
+ );
35
79
  const [selectedResources, setSelectedResources] =
36
80
  useState<string[]>(allResources);
37
81
 
82
+ useEffect(() => {
83
+ setSelectedResources(allResources);
84
+ }, [allResources]);
85
+
86
+ const register = useCallback((chartId: string, seriesNames: string[]) => {
87
+ setRegisteredColorSets((prev) => ({
88
+ ...prev,
89
+ [chartId]: seriesNames,
90
+ }));
91
+ }, []);
92
+
38
93
  const addSelectedResource = useCallback((resource: string) => {
39
94
  setSelectedResources((prev) =>
40
95
  prev.includes(resource) ? prev : [...prev, resource],
@@ -46,8 +101,8 @@ export const ChartLegendWrapper = ({
46
101
  }, []);
47
102
 
48
103
  const selectAllResources = useCallback(() => {
49
- setSelectedResources(allResources);
50
- }, [allResources]);
104
+ setSelectedResources(Object.keys(internalColorSet));
105
+ }, [internalColorSet]);
51
106
 
52
107
  const selectOnlyResource = useCallback((resource: string) => {
53
108
  setSelectedResources([resource]);
@@ -65,7 +120,7 @@ export const ChartLegendWrapper = ({
65
120
 
66
121
  const getColor = useCallback(
67
122
  (resource: string) => {
68
- const color = colorSet[resource];
123
+ const color = internalColorSet[resource];
69
124
  if (!color) {
70
125
  console.warn(
71
126
  `ChartLegendWrapper: No color defined for resource "${resource}"`,
@@ -74,12 +129,12 @@ export const ChartLegendWrapper = ({
74
129
  }
75
130
  return color;
76
131
  },
77
- [colorSet],
132
+ [internalColorSet],
78
133
  );
79
134
 
80
135
  const listResources = useCallback(() => {
81
- return Object.keys(colorSet);
82
- }, [colorSet]);
136
+ return Object.keys(internalColorSet);
137
+ }, [internalColorSet]);
83
138
 
84
139
  const chartLegendState = useMemo(
85
140
  () => ({
@@ -92,6 +147,7 @@ export const ChartLegendWrapper = ({
92
147
  getColor,
93
148
  listResources,
94
149
  isOnlyOneSelected,
150
+ register,
95
151
  }),
96
152
  [
97
153
  selectedResources,
@@ -103,6 +159,7 @@ export const ChartLegendWrapper = ({
103
159
  getColor,
104
160
  listResources,
105
161
  isOnlyOneSelected,
162
+ register,
106
163
  ],
107
164
  );
108
165
 
@@ -113,7 +170,6 @@ export const ChartLegendWrapper = ({
113
170
  );
114
171
  };
115
172
 
116
- // Hook for accessing legend state in custom components
117
173
  export const useChartLegend = () => {
118
174
  const context = useContext(ChartLegendContext);
119
175
  if (!context) {
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import { spacing } from '../../spacing';
4
+ import { fontSize, fontWeight } from '../../style/theme';
5
+
6
+ export const ChartTooltipContainer = styled.div`
7
+ border: 1px solid ${({ theme }) => theme.border};
8
+ background-color: ${({ theme }) => theme.backgroundLevel1};
9
+ color: ${({ theme }) => theme.textPrimary};
10
+ border-radius: 4px;
11
+ font-size: ${fontSize.small};
12
+ padding: ${spacing.r8};
13
+ min-width: 10rem;
14
+ max-width: 250px;
15
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
16
+ `;
17
+
18
+ const TooltipText = styled.div<{
19
+ isHovered?: boolean;
20
+ align?: 'left' | 'right';
21
+ }>`
22
+ color: ${({ theme, isHovered }) =>
23
+ isHovered ? theme.textPrimary : theme.textSecondary};
24
+ font-size: ${fontSize.smaller};
25
+ font-weight: ${({ isHovered }) =>
26
+ isHovered ? fontWeight.bold : fontWeight.base};
27
+ text-align: ${({ align }) => align || 'left'};
28
+ ${({ align }) => align === 'right' && 'flex-shrink: 0;'}
29
+ `;
30
+
31
+ const TooltipRow = styled.div`
32
+ display: flex;
33
+ justify-content: space-between;
34
+ align-items: center;
35
+ gap: ${spacing.r32};
36
+ width: 100%;
37
+ `;
38
+
39
+ const TooltipLabel = styled.div`
40
+ display: flex;
41
+ align-items: center;
42
+ gap: ${spacing.r8};
43
+ flex: 1;
44
+ min-width: 0;
45
+ `;
46
+
47
+ interface ChartTooltipItemProps {
48
+ label: React.ReactNode;
49
+ value: React.ReactNode;
50
+ isHovered?: boolean;
51
+ legendIcon?: React.ReactNode;
52
+ }
53
+
54
+ export const ChartTooltipItem: React.FC<ChartTooltipItemProps> = ({
55
+ label,
56
+ value,
57
+ isHovered = false,
58
+ legendIcon,
59
+ }) => (
60
+ <TooltipRow>
61
+ <TooltipLabel>
62
+ {legendIcon}
63
+ <TooltipText isHovered={isHovered}>{label}</TooltipText>
64
+ </TooltipLabel>
65
+ <TooltipText isHovered={isHovered} align="right">
66
+ {value}
67
+ </TooltipText>
68
+ </TooltipRow>
69
+ );
70
+
71
+ export const ChartTooltipHeader = styled.div`
72
+ color: ${({ theme }) => theme.textPrimary};
73
+ font-weight: ${fontWeight.bold};
74
+ text-align: center;
75
+ margin-bottom: ${spacing.r8};
76
+ `;
77
+
78
+ export const ChartTooltipItemsContainer = styled.div`
79
+ display: flex;
80
+ flex-direction: column;
81
+ gap: ${spacing.r8};
82
+ width: 100%;
83
+ `;
@@ -19,7 +19,10 @@ export type GlobalHealthProps = {
19
19
  height?: number;
20
20
  tooltipPosition?: Position;
21
21
  };
22
-
22
+ /**
23
+ * @deprecated Use GlobalHealthBar v2 instead
24
+ * @example import { GlobalHealthBar } from '@scality/core-ui/dist/next';
25
+ */
23
26
  function GlobalHealthBar({
24
27
  id,
25
28
  alerts,
@@ -0,0 +1,203 @@
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ Bar,
4
+ BarChart,
5
+ ResponsiveContainer,
6
+ Tooltip,
7
+ TooltipContentProps,
8
+ YAxis,
9
+ } from 'recharts';
10
+ import styled, { useTheme } from 'styled-components';
11
+ import { GlobalHealthBarTooltip } from './components/GlobalHealthBarTooltip';
12
+ import { HealthBarXAxis } from './components/HealthBarXAxis';
13
+ import {
14
+ CHART_CONFIG,
15
+ getNavigationAction,
16
+ getNavigationStateUpdate,
17
+ } from './healthBarUtils';
18
+ import { Alert, useHealthBarData } from './useHealthBarData';
19
+
20
+ export interface GlobalHealthProps {
21
+ id: string;
22
+ alerts: Alert[];
23
+ start: Date;
24
+ end: Date;
25
+ }
26
+
27
+ const ChartInteractiveContainer = styled.div`
28
+ position: relative;
29
+ `;
30
+
31
+ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
32
+ const [tooltipData, setTooltipData] = useState<Alert | null>(null);
33
+ const [focusedAlertIndex, setFocusedAlertIndex] = useState<number>(-1);
34
+ const [keyboardActive, setKeyboardActive] = useState<boolean>(false);
35
+ const chartContainerRef = useRef<HTMLDivElement>(null);
36
+ const theme = useTheme();
37
+ const startTimestamp = new Date(start).getTime();
38
+ const endTimestamp = new Date(end).getTime();
39
+
40
+ const { chartData, alertsMap, alertKeys } = useHealthBarData(
41
+ alerts,
42
+ startTimestamp,
43
+ endTimestamp,
44
+ id,
45
+ );
46
+
47
+ const handlePointerEnter = useCallback(
48
+ (key: string) => {
49
+ setTooltipData(alertsMap[key]);
50
+ },
51
+ [alertsMap],
52
+ );
53
+
54
+ const handlePointerLeave = useCallback(() => {
55
+ if (!keyboardActive) {
56
+ setTooltipData(null);
57
+ }
58
+ }, [keyboardActive]);
59
+
60
+ const { warningKeys, criticalKeys, unavailableKeys } = alertKeys;
61
+
62
+ // Get all alert keys in order for keyboard navigation
63
+ const allAlertKeys = useMemo(() => {
64
+ return Object.values(alertsMap).sort((a, b) => {
65
+ return new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime();
66
+ });
67
+ }, [alertsMap]);
68
+
69
+ // Handle keyboard navigation
70
+ const handleKeyDown = useCallback(
71
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
72
+ const action = getNavigationAction(event.key);
73
+ if (!action || allAlertKeys.length === 0) return;
74
+
75
+ event.preventDefault();
76
+
77
+ const update = getNavigationStateUpdate(
78
+ action,
79
+ focusedAlertIndex,
80
+ allAlertKeys,
81
+ );
82
+
83
+ setFocusedAlertIndex(update.newIndex);
84
+ setTooltipData(update.selectedAlert);
85
+ setKeyboardActive(update.shouldActivateKeyboard);
86
+ },
87
+ [allAlertKeys, focusedAlertIndex],
88
+ );
89
+
90
+ // Handle focus events
91
+ const handleFocus = useCallback(() => {
92
+ if (allAlertKeys.length > 0 && focusedAlertIndex === -1) {
93
+ setFocusedAlertIndex(0);
94
+ setTooltipData(allAlertKeys[0]);
95
+ setKeyboardActive(true);
96
+ }
97
+ }, [allAlertKeys, focusedAlertIndex]);
98
+
99
+ const handleBlur = useCallback(() => {
100
+ setKeyboardActive(false);
101
+ setFocusedAlertIndex(-1);
102
+ setTooltipData(null);
103
+ }, []);
104
+
105
+ // Handle mouse enter to disable keyboard mode
106
+ const handleMouseEnter = useCallback(() => {
107
+ setKeyboardActive(false);
108
+ }, []);
109
+
110
+ const allAlertBars = useMemo(() => {
111
+ const configs = [
112
+ { keys: unavailableKeys, fill: theme.textSecondary },
113
+ { keys: warningKeys, fill: theme.statusWarning },
114
+ { keys: criticalKeys, fill: theme.statusCritical },
115
+ ];
116
+
117
+ return configs.flatMap(({ keys, fill }) =>
118
+ keys.map((key) => ({ key, fill })),
119
+ );
120
+ }, [unavailableKeys, warningKeys, criticalKeys, theme]);
121
+
122
+ return (
123
+ <ChartInteractiveContainer
124
+ ref={chartContainerRef}
125
+ tabIndex={0}
126
+ role="application"
127
+ aria-label={`Health bar chart with ${allAlertKeys.length} alerts. Use arrow keys to navigate, Escape to close tooltip.`}
128
+ onKeyDown={handleKeyDown}
129
+ onFocus={handleFocus}
130
+ onBlur={handleBlur}
131
+ onMouseEnter={handleMouseEnter}
132
+ >
133
+ <ResponsiveContainer width={'100%'} height={CHART_CONFIG.CHART_HEIGHT}>
134
+ <BarChart
135
+ data={chartData}
136
+ layout="vertical"
137
+ barSize={CHART_CONFIG.BAR_SIZE}
138
+ accessibilityLayer
139
+ margin={CHART_CONFIG.MARGINS}
140
+ >
141
+ <HealthBarXAxis
142
+ startTimestamp={startTimestamp}
143
+ endTimestamp={endTimestamp}
144
+ />
145
+
146
+ <Tooltip
147
+ allowEscapeViewBox={{ x: true, y: true }}
148
+ isAnimationActive={false}
149
+ shared={false}
150
+ wrapperStyle={{
151
+ width: '20rem',
152
+ position: 'fixed',
153
+ }}
154
+ content={(props: TooltipContentProps<number, string>) => {
155
+ return (
156
+ <GlobalHealthBarTooltip
157
+ tooltipData={tooltipData}
158
+ tooltipProps={props}
159
+ chartContainerRef={chartContainerRef}
160
+ isKeyboardActive={keyboardActive}
161
+ startTimestamp={startTimestamp}
162
+ endTimestamp={endTimestamp}
163
+ />
164
+ );
165
+ }}
166
+ />
167
+
168
+ {/* YAxis for the Background healthy bar */}
169
+ <YAxis yAxisId={'background'} type="category" hide />
170
+
171
+ {/* Generate YAxis for all alert keys */}
172
+ {allAlertBars.map(({ key }) => (
173
+ <YAxis key={`yAxis${key}`} yAxisId={key} type="category" hide />
174
+ ))}
175
+
176
+ {/* Background healthy bar */}
177
+ <Bar
178
+ dataKey="range"
179
+ fill={theme.statusHealthy}
180
+ radius={CHART_CONFIG.RADIUS_SIZE}
181
+ yAxisId="background"
182
+ isAnimationActive={false}
183
+ />
184
+
185
+ {/* Alert bars */}
186
+ {allAlertBars.map(({ key, fill }) => (
187
+ <Bar
188
+ dataKey={key}
189
+ yAxisId={key}
190
+ fill={fill}
191
+ onPointerEnter={() => handlePointerEnter(key)}
192
+ onPointerLeave={() => handlePointerLeave()}
193
+ isAnimationActive={false}
194
+ />
195
+ ))}
196
+ </BarChart>
197
+ </ResponsiveContainer>
198
+ </ChartInteractiveContainer>
199
+ );
200
+ }
201
+
202
+ // Re-export Alert type for external use
203
+ export type { Alert };
@@ -0,0 +1,173 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { useEffect, useState } from 'react';
4
+ import styled, { css, useTheme } from 'styled-components';
5
+ import {
6
+ useFloating,
7
+ autoUpdate,
8
+ offset,
9
+ flip,
10
+ shift,
11
+ } from '@floating-ui/react';
12
+ import { FormattedDateTime, Stack, Text, Wrap, spacing } from '../../../index';
13
+ import { Alert } from '../GlobalHealthBarRecharts.component';
14
+ import { TooltipContentProps } from 'recharts';
15
+ import { zIndex } from '../../../style/theme';
16
+ import { CHART_CONFIG, getTooltipPosition } from '../healthBarUtils';
17
+
18
+ interface GlobalHealthBarTooltipProps {
19
+ tooltipData: Alert | null;
20
+ coordinate?: { x: number; y: number };
21
+ tooltipProps: TooltipContentProps<number, string>;
22
+ chartContainerRef: React.RefObject<HTMLDivElement>;
23
+ isKeyboardActive?: boolean;
24
+ startTimestamp?: number;
25
+ endTimestamp?: number;
26
+ }
27
+
28
+ const TooltipContainer = styled.div`
29
+ ${(props) => {
30
+ const theme = useTheme();
31
+
32
+ return css`
33
+ border: 1px solid ${theme.border};
34
+ width: 24rem;
35
+ z-index: ${zIndex.tooltip};
36
+ color: ${theme.textSecondary};
37
+ background-color: ${theme.backgroundLevel1};
38
+ border-radius: 4px;
39
+ padding: ${spacing.r8};
40
+ pointer-events: none;
41
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
42
+ `;
43
+ }}
44
+ `;
45
+
46
+ export const GlobalHealthBarTooltip = (props: GlobalHealthBarTooltipProps) => {
47
+ const {
48
+ tooltipData,
49
+ tooltipProps,
50
+ chartContainerRef,
51
+ isKeyboardActive = false,
52
+ startTimestamp = 0,
53
+ endTimestamp = 0,
54
+ } = props;
55
+ const { coordinate } = tooltipProps;
56
+ const [virtualElement, setVirtualElement] = useState<any>(null);
57
+
58
+ const { refs, floatingStyles } = useFloating({
59
+ elements: {
60
+ reference: virtualElement,
61
+ },
62
+ middleware: [
63
+ offset(({ placement }) => {
64
+ // Use larger offset when tooltip is on top
65
+ // to avoid tooltip over bar
66
+ return placement.includes('top') ? 20 : 30;
67
+ }),
68
+ flip(),
69
+ shift({ padding: 10 }),
70
+ ],
71
+ whileElementsMounted: autoUpdate,
72
+ });
73
+
74
+ // Create virtual element from coordinate
75
+ useEffect(() => {
76
+ if (chartContainerRef.current) {
77
+ const chartRect = chartContainerRef.current.getBoundingClientRect();
78
+
79
+ let tooltipX: number;
80
+ let tooltipY: number;
81
+
82
+ if (isKeyboardActive && tooltipData && startTimestamp && endTimestamp) {
83
+ // Calculate the chart's usable width (excluding margins)
84
+ const chartUsableWidth =
85
+ chartRect.width -
86
+ CHART_CONFIG.MARGINS.left -
87
+ CHART_CONFIG.MARGINS.right;
88
+
89
+ // Use the same positioning logic as alert bars
90
+ const alertCenterX = getTooltipPosition(
91
+ tooltipData,
92
+ startTimestamp,
93
+ endTimestamp,
94
+ chartUsableWidth,
95
+ );
96
+
97
+ // Position tooltip at the center of the alert's time span
98
+ // alertCenterX already includes the margin offset, so just add chartRect.left
99
+ tooltipX = chartRect.left + alertCenterX;
100
+ tooltipY = chartRect.top + CHART_CONFIG.BAR_SIZE;
101
+ } else {
102
+ // For mouse navigation, use the provided coordinate
103
+ tooltipX = chartRect.left + coordinate?.x;
104
+ tooltipY = chartRect.top + coordinate?.y;
105
+ }
106
+
107
+ setVirtualElement({
108
+ getBoundingClientRect() {
109
+ return {
110
+ width: 0,
111
+ height: 0,
112
+ x: tooltipX,
113
+ y: tooltipY,
114
+ left: tooltipX,
115
+ top: tooltipY,
116
+ right: tooltipX,
117
+ bottom: tooltipY,
118
+ };
119
+ },
120
+ });
121
+ }
122
+ }, [
123
+ coordinate,
124
+ chartContainerRef,
125
+ isKeyboardActive,
126
+ tooltipData,
127
+ startTimestamp,
128
+ endTimestamp,
129
+ ]);
130
+
131
+ if (!tooltipData) return null;
132
+
133
+ const { description, startsAt, endsAt, severity } = tooltipData;
134
+
135
+ const tooltipContent = (
136
+ <TooltipContainer ref={refs.setFloating} style={floatingStyles}>
137
+ <Stack direction="vertical" gap="r8">
138
+ <Wrap>
139
+ <Text variant="Smaller">Severity</Text>
140
+ <Text color="textPrimary" variant="Smaller">
141
+ {severity}
142
+ </Text>
143
+ </Wrap>
144
+ <Wrap>
145
+ <Text variant="Smaller">Start</Text>
146
+ <Text color="textPrimary" variant="Smaller">
147
+ <FormattedDateTime format="date-time" value={new Date(startsAt)} />
148
+ </Text>
149
+ </Wrap>
150
+ <Wrap>
151
+ <Text variant="Smaller">End</Text>
152
+ <Text color="textPrimary" variant="Smaller">
153
+ <FormattedDateTime format="date-time" value={new Date(endsAt)} />
154
+ </Text>
155
+ </Wrap>
156
+ <Wrap>
157
+ <Text variant="Smaller" style={{ paddingRight: spacing.r32 }}>
158
+ Description
159
+ </Text>
160
+ <Text
161
+ color="textPrimary"
162
+ variant="Smaller"
163
+ style={{ whiteSpace: 'wrap', textAlign: 'justify' }}
164
+ >
165
+ {description}
166
+ </Text>
167
+ </Wrap>
168
+ </Stack>
169
+ </TooltipContainer>
170
+ );
171
+
172
+ return createPortal(tooltipContent, document.body);
173
+ };