@scality/core-ui 0.176.0 → 0.177.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/barchartv2/Barchart.component.d.ts +1 -1
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +10 -6
- package/dist/components/barchartv2/BarchartTooltip.d.ts +3 -2
- package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -1
- package/dist/components/barchartv2/BarchartTooltip.js +6 -8
- package/dist/components/barchartv2/utils.d.ts +6 -1
- package/dist/components/barchartv2/utils.d.ts.map +1 -1
- package/dist/components/barchartv2/utils.js +34 -8
- package/dist/components/charttooltip/ChartTooltip.d.ts +23 -0
- package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -1
- package/dist/components/charttooltip/ChartTooltip.js +83 -1
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -1
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +27 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +1 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -1
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +19 -59
- package/dist/components/globalhealthbar/components/HealthBarXAxis.js +1 -1
- package/dist/components/globalhealthbar/useHealthBarData.d.ts +1 -0
- package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -1
- package/dist/components/globalhealthbar/useHealthBarData.js +1 -0
- package/dist/components/globalhealthbar/useHealthBarData.spec.js +2 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +43 -47
- package/dist/components/linetimeseriechart/utils.js +2 -2
- package/dist/style/theme.js +1 -1
- package/package.json +1 -1
- package/src/lib/components/barchartv2/Barchart.component.tsx +19 -12
- package/src/lib/components/barchartv2/BarchartTooltip.test.tsx +30 -0
- package/src/lib/components/barchartv2/BarchartTooltip.tsx +21 -8
- package/src/lib/components/barchartv2/utils.test.ts +72 -17
- package/src/lib/components/barchartv2/utils.ts +39 -7
- package/src/lib/components/charttooltip/ChartTooltip.tsx +134 -1
- package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +56 -11
- package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +75 -117
- package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +1 -1
- package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +2 -0
- package/src/lib/components/globalhealthbar/useHealthBarData.ts +2 -0
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +86 -82
- package/src/lib/components/linetimeseriechart/utils.test.ts +3 -3
- package/src/lib/components/linetimeseriechart/utils.ts +2 -2
- package/src/lib/style/theme.ts +1 -1
- package/stories/BarChart/barchart.stories.tsx +23 -8
|
@@ -4,23 +4,55 @@ import { chartColors, ChartColors } from '../../style/theme';
|
|
|
4
4
|
import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
|
|
5
5
|
|
|
6
6
|
export const getRoundReferenceValue = (value: number): number => {
|
|
7
|
-
if (value <= 0) return
|
|
7
|
+
if (value <= 0) return 1; // Default for zero or negative values
|
|
8
8
|
|
|
9
9
|
// Get the magnitude (10^n where n is the number of digits - 1)
|
|
10
10
|
const magnitude = Math.pow(10, Math.floor(Math.log10(value)));
|
|
11
11
|
|
|
12
|
+
// Buffer the value by 10% to avoid being too close to the edge of the chart
|
|
13
|
+
const bufferedValue = value * 1.1;
|
|
14
|
+
|
|
12
15
|
// Normalized value between 1 and 10
|
|
13
|
-
const normalized =
|
|
16
|
+
const normalized = bufferedValue / magnitude;
|
|
14
17
|
|
|
15
18
|
// Round to nice numbers based on normalized value
|
|
19
|
+
// skip 1.5, 3, 4, 7.5 as top value for better chart
|
|
20
|
+
// appearance for small values
|
|
16
21
|
let result: number;
|
|
22
|
+
|
|
17
23
|
if (normalized <= 1) result = magnitude;
|
|
18
|
-
else if (normalized <= 2
|
|
24
|
+
else if (normalized <= 2) result = 2 * magnitude;
|
|
25
|
+
else if (value > 10 && normalized <= 4) result = 4 * magnitude;
|
|
19
26
|
else if (normalized <= 5) result = 5 * magnitude;
|
|
27
|
+
else if (value > 10 && normalized <= 7.5) result = 7.5 * magnitude;
|
|
20
28
|
else result = 10 * magnitude;
|
|
21
29
|
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
return result;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const getTicks = (topValue: number, isSymmetrical: boolean) => {
|
|
34
|
+
if (topValue < 10) {
|
|
35
|
+
if (isSymmetrical) {
|
|
36
|
+
return [-topValue, 0, topValue];
|
|
37
|
+
} else {
|
|
38
|
+
return [0, topValue];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const numberOfTicks = topValue % 3 === 0 ? 4 : 3;
|
|
42
|
+
const tickInterval = topValue / (numberOfTicks - 1);
|
|
43
|
+
const ticks = Array.from(
|
|
44
|
+
{ length: numberOfTicks },
|
|
45
|
+
(_, index) => index * tickInterval,
|
|
46
|
+
);
|
|
47
|
+
if (isSymmetrical) {
|
|
48
|
+
// Create negative ticks in order without 0
|
|
49
|
+
const negativeTicks = Array.from(
|
|
50
|
+
{ length: numberOfTicks - 1 },
|
|
51
|
+
(_, index) => -(numberOfTicks - 1 - index) * tickInterval,
|
|
52
|
+
);
|
|
53
|
+
ticks.unshift(...negativeTicks);
|
|
54
|
+
}
|
|
55
|
+
return ticks;
|
|
24
56
|
};
|
|
25
57
|
|
|
26
58
|
export const getMaxBarValue = (
|
|
@@ -300,11 +332,11 @@ export const computeUnitLabelAndRoundReferenceValue = (
|
|
|
300
332
|
) => {
|
|
301
333
|
if (!unitRange) {
|
|
302
334
|
const roundReferenceValue = getRoundReferenceValue(maxValue);
|
|
303
|
-
return { unitLabel:
|
|
335
|
+
return { unitLabel: undefined, roundReferenceValue, rechartsData: data };
|
|
304
336
|
}
|
|
305
337
|
|
|
306
338
|
const { valueBase, unitLabel } = getUnitLabel(unitRange, maxValue);
|
|
307
|
-
const topValue =
|
|
339
|
+
const topValue = maxValue / valueBase;
|
|
308
340
|
const roundReferenceValue = getRoundReferenceValue(topValue);
|
|
309
341
|
const rechartsData = data.map((dataPoint) => {
|
|
310
342
|
const normalizedDataPoint = { ...dataPoint };
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
useFloating,
|
|
6
|
+
autoUpdate,
|
|
7
|
+
offset,
|
|
8
|
+
flip,
|
|
9
|
+
shift,
|
|
10
|
+
Middleware,
|
|
11
|
+
} from '@floating-ui/react';
|
|
2
12
|
import styled from 'styled-components';
|
|
3
13
|
import { spacing } from '../../spacing';
|
|
4
14
|
import { fontSize, fontWeight } from '../../style/theme';
|
|
@@ -12,7 +22,8 @@ export const ChartTooltipContainer = styled.div`
|
|
|
12
22
|
font-size: ${fontSize.small};
|
|
13
23
|
padding: ${spacing.r8};
|
|
14
24
|
min-width: 10rem;
|
|
15
|
-
max-width:
|
|
25
|
+
max-width: 40rem;
|
|
26
|
+
|
|
16
27
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
17
28
|
`;
|
|
18
29
|
|
|
@@ -121,3 +132,125 @@ export const TooltipHeader = ({
|
|
|
121
132
|
</ChartTooltipHeader>
|
|
122
133
|
);
|
|
123
134
|
};
|
|
135
|
+
|
|
136
|
+
export interface ChartTooltipPortalProps {
|
|
137
|
+
children: React.ReactNode;
|
|
138
|
+
coordinate?: { x: number; y: number };
|
|
139
|
+
chartContainerRef: React.RefObject<HTMLDivElement>;
|
|
140
|
+
isVisible?: boolean;
|
|
141
|
+
middleware?: Middleware[];
|
|
142
|
+
offset?: number | (({ placement }: { placement: string }) => number);
|
|
143
|
+
customPosition?: (
|
|
144
|
+
chartRect: DOMRect,
|
|
145
|
+
coordinate?: { x: number; y: number },
|
|
146
|
+
) => { x: number; y: number };
|
|
147
|
+
containerComponent?: React.ComponentType<any>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export const ChartTooltipPortal: React.FC<ChartTooltipPortalProps> = ({
|
|
151
|
+
children,
|
|
152
|
+
coordinate,
|
|
153
|
+
chartContainerRef,
|
|
154
|
+
isVisible = true,
|
|
155
|
+
middleware,
|
|
156
|
+
offset: customOffset,
|
|
157
|
+
customPosition,
|
|
158
|
+
containerComponent: ContainerComponent = ChartTooltipContainer,
|
|
159
|
+
}) => {
|
|
160
|
+
const [virtualElement, setVirtualElement] = useState<any>(null);
|
|
161
|
+
const previousPositionRef = useRef<{ x: number; y: number } | null>(null);
|
|
162
|
+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(
|
|
163
|
+
null,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Default middleware configuration
|
|
167
|
+
const defaultMiddleware = [
|
|
168
|
+
offset(customOffset || 20),
|
|
169
|
+
flip(),
|
|
170
|
+
shift({ padding: 10 }),
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const { refs, floatingStyles } = useFloating({
|
|
174
|
+
elements: {
|
|
175
|
+
reference: virtualElement,
|
|
176
|
+
},
|
|
177
|
+
placement: 'top',
|
|
178
|
+
middleware: middleware || defaultMiddleware,
|
|
179
|
+
whileElementsMounted: autoUpdate,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Create portal container once
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const container = document.createElement('div');
|
|
185
|
+
document.body.appendChild(container);
|
|
186
|
+
setPortalContainer(container);
|
|
187
|
+
|
|
188
|
+
return () => {
|
|
189
|
+
document.body.removeChild(container);
|
|
190
|
+
};
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
// Create virtual element from coordinate or custom position
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
if (chartContainerRef.current) {
|
|
196
|
+
const chartRect = chartContainerRef.current.getBoundingClientRect();
|
|
197
|
+
|
|
198
|
+
let tooltipX: number;
|
|
199
|
+
let tooltipY: number;
|
|
200
|
+
|
|
201
|
+
if (customPosition) {
|
|
202
|
+
// Use custom positioning function
|
|
203
|
+
const position = customPosition(chartRect, coordinate);
|
|
204
|
+
tooltipX = position.x;
|
|
205
|
+
tooltipY = position.y;
|
|
206
|
+
} else if (coordinate) {
|
|
207
|
+
// Use default coordinate-based positioning
|
|
208
|
+
tooltipX = chartRect.left + coordinate.x;
|
|
209
|
+
tooltipY = chartRect.top + coordinate.y;
|
|
210
|
+
} else {
|
|
211
|
+
return; // No positioning method available
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check if position has changed significantly
|
|
215
|
+
const hasPositionChanged =
|
|
216
|
+
!previousPositionRef.current ||
|
|
217
|
+
Math.abs(previousPositionRef.current.x - tooltipX) > 5 ||
|
|
218
|
+
Math.abs(previousPositionRef.current.y - tooltipY) > 5;
|
|
219
|
+
|
|
220
|
+
if (hasPositionChanged) {
|
|
221
|
+
previousPositionRef.current = { x: tooltipX, y: tooltipY };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
setVirtualElement({
|
|
225
|
+
getBoundingClientRect() {
|
|
226
|
+
return {
|
|
227
|
+
width: 0,
|
|
228
|
+
height: 0,
|
|
229
|
+
x: tooltipX,
|
|
230
|
+
y: tooltipY,
|
|
231
|
+
left: tooltipX,
|
|
232
|
+
top: tooltipY,
|
|
233
|
+
right: tooltipX,
|
|
234
|
+
bottom: tooltipY,
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}, [coordinate, chartContainerRef, customPosition]);
|
|
240
|
+
|
|
241
|
+
if (!isVisible || !virtualElement || !portalContainer) return null;
|
|
242
|
+
|
|
243
|
+
const tooltipContent = (
|
|
244
|
+
<ContainerComponent
|
|
245
|
+
ref={refs.setFloating}
|
|
246
|
+
style={{
|
|
247
|
+
...floatingStyles,
|
|
248
|
+
opacity: isVisible ? 1 : 0,
|
|
249
|
+
}}
|
|
250
|
+
>
|
|
251
|
+
{children}
|
|
252
|
+
</ContainerComponent>
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
return createPortal(tooltipContent, portalContainer);
|
|
256
|
+
};
|
|
@@ -26,12 +26,17 @@ export interface GlobalHealthProps {
|
|
|
26
26
|
|
|
27
27
|
const ChartInteractiveContainer = styled.div`
|
|
28
28
|
position: relative;
|
|
29
|
+
outline: none;
|
|
30
|
+
.recharts-surface {
|
|
31
|
+
outline: none;
|
|
32
|
+
}
|
|
29
33
|
`;
|
|
30
34
|
|
|
31
35
|
export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
32
36
|
const [tooltipData, setTooltipData] = useState<Alert | null>(null);
|
|
33
37
|
const [focusedAlertIndex, setFocusedAlertIndex] = useState<number>(-1);
|
|
34
38
|
const [keyboardActive, setKeyboardActive] = useState<boolean>(false);
|
|
39
|
+
const [activeBarKey, setActiveBarKey] = useState<string | null>(null);
|
|
35
40
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
|
36
41
|
const theme = useTheme();
|
|
37
42
|
const startTimestamp = new Date(start).getTime();
|
|
@@ -47,6 +52,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
|
47
52
|
const handlePointerEnter = useCallback(
|
|
48
53
|
(key: string) => {
|
|
49
54
|
setTooltipData(alertsMap[key]);
|
|
55
|
+
setActiveBarKey(key);
|
|
50
56
|
},
|
|
51
57
|
[alertsMap],
|
|
52
58
|
);
|
|
@@ -54,6 +60,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
|
54
60
|
const handlePointerLeave = useCallback(() => {
|
|
55
61
|
if (!keyboardActive) {
|
|
56
62
|
setTooltipData(null);
|
|
63
|
+
setActiveBarKey(null);
|
|
57
64
|
}
|
|
58
65
|
}, [keyboardActive]);
|
|
59
66
|
|
|
@@ -83,6 +90,11 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
|
83
90
|
setFocusedAlertIndex(update.newIndex);
|
|
84
91
|
setTooltipData(update.selectedAlert);
|
|
85
92
|
setKeyboardActive(update.shouldActivateKeyboard);
|
|
93
|
+
|
|
94
|
+
// Set active bar key for keyboard navigation
|
|
95
|
+
if (update.selectedAlert) {
|
|
96
|
+
setActiveBarKey(update.selectedAlert.key);
|
|
97
|
+
}
|
|
86
98
|
},
|
|
87
99
|
[allAlertKeys, focusedAlertIndex],
|
|
88
100
|
);
|
|
@@ -93,6 +105,9 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
|
93
105
|
setFocusedAlertIndex(0);
|
|
94
106
|
setTooltipData(allAlertKeys[0]);
|
|
95
107
|
setKeyboardActive(true);
|
|
108
|
+
|
|
109
|
+
// Set active bar key for initial focus
|
|
110
|
+
setActiveBarKey(allAlertKeys[0].key);
|
|
96
111
|
}
|
|
97
112
|
}, [allAlertKeys, focusedAlertIndex]);
|
|
98
113
|
|
|
@@ -100,6 +115,7 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
|
100
115
|
setKeyboardActive(false);
|
|
101
116
|
setFocusedAlertIndex(-1);
|
|
102
117
|
setTooltipData(null);
|
|
118
|
+
setActiveBarKey(null);
|
|
103
119
|
}, []);
|
|
104
120
|
|
|
105
121
|
// Handle mouse enter to disable keyboard mode
|
|
@@ -182,17 +198,46 @@ export function GlobalHealthBar({ id, alerts, start, end }: GlobalHealthProps) {
|
|
|
182
198
|
isAnimationActive={false}
|
|
183
199
|
/>
|
|
184
200
|
|
|
185
|
-
{/* Alert bars */}
|
|
186
|
-
{allAlertBars.map(({ key, fill }) =>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
201
|
+
{/* Alert bars - render non-active bars first */}
|
|
202
|
+
{allAlertBars.map(({ key, fill }) => {
|
|
203
|
+
const isActive = key === activeBarKey;
|
|
204
|
+
// Skip active bar here - it will be rendered last
|
|
205
|
+
if (isActive) return null;
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<Bar
|
|
209
|
+
key={key}
|
|
210
|
+
dataKey={key}
|
|
211
|
+
yAxisId={key}
|
|
212
|
+
fill={fill}
|
|
213
|
+
onPointerEnter={() => handlePointerEnter(key)}
|
|
214
|
+
onPointerLeave={() => handlePointerLeave()}
|
|
215
|
+
isAnimationActive={false}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
})}
|
|
219
|
+
|
|
220
|
+
{/* Render active bar last to ensure it's on top */}
|
|
221
|
+
{activeBarKey &&
|
|
222
|
+
(() => {
|
|
223
|
+
const activeBar = allAlertBars.find(
|
|
224
|
+
(bar) => bar.key === activeBarKey,
|
|
225
|
+
);
|
|
226
|
+
if (!activeBar) return null;
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<Bar
|
|
230
|
+
key={`${activeBar.key}-active`}
|
|
231
|
+
dataKey={activeBar.key}
|
|
232
|
+
yAxisId={activeBar.key}
|
|
233
|
+
fill={activeBar.fill}
|
|
234
|
+
stroke={theme.selectedActive}
|
|
235
|
+
onPointerEnter={() => handlePointerEnter(activeBar.key)}
|
|
236
|
+
onPointerLeave={() => handlePointerLeave()}
|
|
237
|
+
isAnimationActive={false}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
})()}
|
|
196
241
|
</BarChart>
|
|
197
242
|
</ResponsiveContainer>
|
|
198
243
|
</ChartInteractiveContainer>
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { createPortal } from 'react-dom';
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
2
|
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
3
|
import { FormattedDateTime, Stack, Text, Wrap, spacing } from '../../../index';
|
|
13
4
|
import { Alert } from '../GlobalHealthBarRecharts.component';
|
|
14
5
|
import { TooltipContentProps } from 'recharts';
|
|
15
6
|
import { zIndex } from '../../../style/theme';
|
|
16
7
|
import { CHART_CONFIG, getTooltipPosition } from '../healthBarUtils';
|
|
8
|
+
import { ChartTooltipPortal } from '../../charttooltip/ChartTooltip';
|
|
17
9
|
|
|
18
10
|
interface GlobalHealthBarTooltipProps {
|
|
19
11
|
tooltipData: Alert | null;
|
|
@@ -53,121 +45,87 @@ export const GlobalHealthBarTooltip = (props: GlobalHealthBarTooltipProps) => {
|
|
|
53
45
|
endTimestamp = 0,
|
|
54
46
|
} = props;
|
|
55
47
|
const { coordinate } = tooltipProps;
|
|
56
|
-
const [virtualElement, setVirtualElement] = useState<any>(null);
|
|
57
48
|
|
|
58
|
-
|
|
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();
|
|
49
|
+
if (!tooltipData) return null;
|
|
78
50
|
|
|
79
|
-
|
|
80
|
-
let tooltipY: number;
|
|
51
|
+
const { description, startsAt, endsAt, severity } = tooltipData;
|
|
81
52
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
53
|
+
const tooltipContent = (
|
|
54
|
+
<Stack direction="vertical" gap="r8">
|
|
55
|
+
<Wrap>
|
|
56
|
+
<Text variant="Smaller">Severity</Text>
|
|
57
|
+
<Text color="textPrimary" variant="Smaller">
|
|
58
|
+
{severity}
|
|
59
|
+
</Text>
|
|
60
|
+
</Wrap>
|
|
61
|
+
<Wrap>
|
|
62
|
+
<Text variant="Smaller">Start</Text>
|
|
63
|
+
<Text color="textPrimary" variant="Smaller">
|
|
64
|
+
<FormattedDateTime format="date-time" value={new Date(startsAt)} />
|
|
65
|
+
</Text>
|
|
66
|
+
</Wrap>
|
|
67
|
+
<Wrap>
|
|
68
|
+
<Text variant="Smaller">End</Text>
|
|
69
|
+
<Text color="textPrimary" variant="Smaller">
|
|
70
|
+
<FormattedDateTime format="date-time" value={new Date(endsAt)} />
|
|
71
|
+
</Text>
|
|
72
|
+
</Wrap>
|
|
73
|
+
<Wrap>
|
|
74
|
+
<Text variant="Smaller" style={{ paddingRight: spacing.r32 }}>
|
|
75
|
+
Description
|
|
76
|
+
</Text>
|
|
77
|
+
<Text
|
|
78
|
+
color="textPrimary"
|
|
79
|
+
variant="Smaller"
|
|
80
|
+
style={{ whiteSpace: 'wrap', textAlign: 'justify' }}
|
|
81
|
+
>
|
|
82
|
+
{description}
|
|
83
|
+
</Text>
|
|
84
|
+
</Wrap>
|
|
85
|
+
</Stack>
|
|
86
|
+
);
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
88
|
+
return (
|
|
89
|
+
<ChartTooltipPortal
|
|
90
|
+
coordinate={coordinate}
|
|
91
|
+
chartContainerRef={chartContainerRef}
|
|
92
|
+
isVisible={!!tooltipData}
|
|
93
|
+
customPosition={(chartRect, coordinate) => {
|
|
94
|
+
if (isKeyboardActive && tooltipData && startTimestamp && endTimestamp) {
|
|
95
|
+
// Calculate the chart's usable width (excluding margins)
|
|
96
|
+
const chartUsableWidth =
|
|
97
|
+
chartRect.width -
|
|
98
|
+
CHART_CONFIG.MARGINS.left -
|
|
99
|
+
CHART_CONFIG.MARGINS.right;
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
tooltipY = chartRect.top + coordinate?.y;
|
|
105
|
-
}
|
|
101
|
+
// Use the same positioning logic as alert bars
|
|
102
|
+
const alertCenterX = getTooltipPosition(
|
|
103
|
+
tooltipData,
|
|
104
|
+
startTimestamp,
|
|
105
|
+
endTimestamp,
|
|
106
|
+
chartUsableWidth,
|
|
107
|
+
);
|
|
106
108
|
|
|
107
|
-
setVirtualElement({
|
|
108
|
-
getBoundingClientRect() {
|
|
109
109
|
return {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
x: tooltipX,
|
|
113
|
-
y: tooltipY,
|
|
114
|
-
left: tooltipX,
|
|
115
|
-
top: tooltipY,
|
|
116
|
-
right: tooltipX,
|
|
117
|
-
bottom: tooltipY,
|
|
110
|
+
x: chartRect.left + alertCenterX,
|
|
111
|
+
y: chartRect.top + CHART_CONFIG.BAR_SIZE,
|
|
118
112
|
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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>
|
|
113
|
+
} else {
|
|
114
|
+
// For mouse navigation, use the provided coordinate
|
|
115
|
+
return {
|
|
116
|
+
x: chartRect.left + (coordinate?.x || 0),
|
|
117
|
+
y: chartRect.top + (coordinate?.y || 0),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}}
|
|
121
|
+
containerComponent={TooltipContainer}
|
|
122
|
+
offset={({ placement }) => {
|
|
123
|
+
// Use larger offset when tooltip is on top
|
|
124
|
+
// to avoid tooltip over bar
|
|
125
|
+
return placement.includes('top') ? 20 : 30;
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{tooltipContent}
|
|
129
|
+
</ChartTooltipPortal>
|
|
170
130
|
);
|
|
171
|
-
|
|
172
|
-
return createPortal(tooltipContent, document.body);
|
|
173
131
|
};
|
|
@@ -16,6 +16,7 @@ describe('useHealthBarData', () => {
|
|
|
16
16
|
severity,
|
|
17
17
|
startsAt,
|
|
18
18
|
endsAt,
|
|
19
|
+
key: `${severity}_${startsAt}`,
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
describe('Alert Filtering', () => {
|
|
@@ -231,6 +232,7 @@ describe('useHealthBarData', () => {
|
|
|
231
232
|
severity: 'warning',
|
|
232
233
|
startsAt: '2023-12-01T02:00:00Z',
|
|
233
234
|
endsAt: '2023-12-01T04:00:00Z',
|
|
235
|
+
key: 'warning_0',
|
|
234
236
|
});
|
|
235
237
|
});
|
|
236
238
|
|
|
@@ -5,6 +5,7 @@ export interface Alert {
|
|
|
5
5
|
startsAt: string;
|
|
6
6
|
endsAt: string;
|
|
7
7
|
severity: 'warning' | 'critical' | 'unavailable';
|
|
8
|
+
key: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const useHealthBarData = (
|
|
@@ -44,6 +45,7 @@ export const useHealthBarData = (
|
|
|
44
45
|
// Store alert data separately for tooltip access
|
|
45
46
|
alertsMapData[uniqueKey] = {
|
|
46
47
|
...alert,
|
|
48
|
+
key: uniqueKey, // Add the consistent key to the alert object
|
|
47
49
|
};
|
|
48
50
|
});
|
|
49
51
|
|