@scality/core-ui 0.175.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 +4 -5
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +23 -27
- package/dist/components/barchartv2/BarchartTooltip.d.ts +4 -3
- package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -1
- package/dist/components/barchartv2/BarchartTooltip.js +10 -12
- 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/chartlegend/ChartLegendWrapper.js +1 -1
- package/dist/components/charttooltip/ChartTooltip.d.ts +29 -0
- package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -1
- package/dist/components/charttooltip/ChartTooltip.js +105 -1
- package/dist/components/date/FormattedDateTime.d.ts +23 -8
- package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.js +51 -7
- 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 +2 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +45 -47
- package/dist/components/linetimeseriechart/utils.d.ts +1 -1
- package/dist/components/linetimeseriechart/utils.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/utils.js +13 -13
- package/dist/style/theme.js +1 -1
- package/package.json +1 -1
- package/src/lib/components/barchartv2/Barchart.component.test.tsx +23 -25
- package/src/lib/components/barchartv2/Barchart.component.tsx +41 -39
- package/src/lib/components/barchartv2/BarchartTooltip.test.tsx +33 -3
- package/src/lib/components/barchartv2/BarchartTooltip.tsx +33 -24
- package/src/lib/components/barchartv2/utils.test.ts +72 -17
- package/src/lib/components/barchartv2/utils.ts +40 -12
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +1 -1
- package/src/lib/components/charttooltip/ChartTooltip.tsx +174 -1
- package/src/lib/components/date/FormattedDateTime.tsx +73 -8
- 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 +101 -90
- package/src/lib/components/linetimeseriechart/utils.test.ts +30 -68
- package/src/lib/components/linetimeseriechart/utils.ts +13 -17
- package/src/lib/style/theme.ts +1 -1
- package/stories/BarChart/barchart.stories.tsx +23 -8
- package/stories/formattedate.stories.tsx +2 -0
- package/stories/linetimeseriechart.stories.tsx +1 -0
|
@@ -133,7 +133,7 @@ export const ChartLegendWrapper = ({
|
|
|
133
133
|
);
|
|
134
134
|
|
|
135
135
|
const listResources = useCallback(() => {
|
|
136
|
-
return Object.keys(internalColorSet);
|
|
136
|
+
return Object.keys(internalColorSet).sort();
|
|
137
137
|
}, [internalColorSet]);
|
|
138
138
|
|
|
139
139
|
const chartLegendState = useMemo(
|
|
@@ -1,7 +1,18 @@
|
|
|
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';
|
|
15
|
+
import { FormattedDateTime } from '../date/FormattedDateTime';
|
|
5
16
|
|
|
6
17
|
export const ChartTooltipContainer = styled.div`
|
|
7
18
|
border: 1px solid ${({ theme }) => theme.border};
|
|
@@ -11,7 +22,8 @@ export const ChartTooltipContainer = styled.div`
|
|
|
11
22
|
font-size: ${fontSize.small};
|
|
12
23
|
padding: ${spacing.r8};
|
|
13
24
|
min-width: 10rem;
|
|
14
|
-
max-width:
|
|
25
|
+
max-width: 40rem;
|
|
26
|
+
|
|
15
27
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
16
28
|
`;
|
|
17
29
|
|
|
@@ -81,3 +93,164 @@ export const ChartTooltipItemsContainer = styled.div`
|
|
|
81
93
|
gap: ${spacing.r8};
|
|
82
94
|
width: 100%;
|
|
83
95
|
`;
|
|
96
|
+
|
|
97
|
+
export const ChartTooltipSeparator = styled.div`
|
|
98
|
+
height: 1px;
|
|
99
|
+
background-color: ${({ theme }) => theme.border};
|
|
100
|
+
margin: ${spacing.r4} 0;
|
|
101
|
+
width: 100%;
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
export type TooltipDateFormat =
|
|
105
|
+
| 'day-month-abbreviated-year-hour-minute'
|
|
106
|
+
| 'day-month-abbreviated-hour-minute-second'
|
|
107
|
+
| 'day-month-abbreviated-hour-minute';
|
|
108
|
+
|
|
109
|
+
const getTooltipDateFormat: (duration: number) => TooltipDateFormat = (
|
|
110
|
+
duration: number,
|
|
111
|
+
) => {
|
|
112
|
+
if (duration <= 60 * 60 * 1000) {
|
|
113
|
+
return 'day-month-abbreviated-hour-minute-second';
|
|
114
|
+
} else if (duration <= 7 * 24 * 60 * 60 * 1000) {
|
|
115
|
+
return 'day-month-abbreviated-hour-minute';
|
|
116
|
+
} else {
|
|
117
|
+
return 'day-month-abbreviated-year-hour-minute';
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const TooltipHeader = ({
|
|
122
|
+
duration,
|
|
123
|
+
value,
|
|
124
|
+
}: {
|
|
125
|
+
duration: number;
|
|
126
|
+
value: string | number;
|
|
127
|
+
}) => {
|
|
128
|
+
const timeFormat = getTooltipDateFormat(duration);
|
|
129
|
+
return (
|
|
130
|
+
<ChartTooltipHeader>
|
|
131
|
+
<FormattedDateTime format={timeFormat} value={new Date(value)} />
|
|
132
|
+
</ChartTooltipHeader>
|
|
133
|
+
);
|
|
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
|
+
};
|
|
@@ -3,7 +3,7 @@ import { Tooltip } from '../tooltip/Tooltip.component';
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* @description Long date formatter, with weekday, year, month and day. Used for describing long term date.
|
|
6
|
-
* @example Wednesday
|
|
6
|
+
* @example Wednesday 06 October 2025
|
|
7
7
|
*/
|
|
8
8
|
export const LONG_DATE_FORMATER = Intl.DateTimeFormat('en-GB', {
|
|
9
9
|
weekday: 'long',
|
|
@@ -35,7 +35,7 @@ export const DATE_FORMATER = Intl.DateTimeFormat('fr-CA', {
|
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* @description Day month formatter, with weekday, day and month. Used for describing long term date.
|
|
38
|
-
* @example Wed
|
|
38
|
+
* @example Wed 06 Oct
|
|
39
39
|
*/
|
|
40
40
|
export const DAY_MONTH_FORMATER = Intl.DateTimeFormat('en-GB', {
|
|
41
41
|
weekday: 'short',
|
|
@@ -64,9 +64,30 @@ export const TIME_FORMATER = Intl.DateTimeFormat('en-GB', {
|
|
|
64
64
|
minute: '2-digit',
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* @description Day month abbreviated formatter. Used for describing long term date.
|
|
69
|
+
* @example 06 Oct
|
|
70
|
+
*/
|
|
71
|
+
export const DAY_MONTH_ABBREVIATED = Intl.DateTimeFormat('en-GB', {
|
|
72
|
+
day: '2-digit',
|
|
73
|
+
month: 'short',
|
|
74
|
+
hour12: false,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @description Day month abbreviated formatter. Used for describing long term date.
|
|
79
|
+
* @example 06 Oct 25
|
|
80
|
+
*/
|
|
81
|
+
export const DAY_MONTH_ABBREVIATED_YEAR = Intl.DateTimeFormat('en-GB', {
|
|
82
|
+
day: '2-digit',
|
|
83
|
+
month: 'short',
|
|
84
|
+
year: '2-digit',
|
|
85
|
+
hour12: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
67
88
|
/**
|
|
68
89
|
* @description Day month abbreviated hour minute second formatter. Used for describing long term date.
|
|
69
|
-
* @example
|
|
90
|
+
* @example 06 Oct 18:33:00
|
|
70
91
|
*/
|
|
71
92
|
export const DAY_MONTH_ABBREVIATED_HOUR_MINUTE_SECOND = Intl.DateTimeFormat(
|
|
72
93
|
'en-GB',
|
|
@@ -82,7 +103,7 @@ export const DAY_MONTH_ABBREVIATED_HOUR_MINUTE_SECOND = Intl.DateTimeFormat(
|
|
|
82
103
|
|
|
83
104
|
/**
|
|
84
105
|
* @description Day month abbreviated hour minute formatter. Used for describing long term date.
|
|
85
|
-
* @example
|
|
106
|
+
* @example 06 Oct 18:33
|
|
86
107
|
*/
|
|
87
108
|
export const DAY_MONTH_ABBREVIATED_HOUR_MINUTE = Intl.DateTimeFormat('en-GB', {
|
|
88
109
|
day: '2-digit',
|
|
@@ -92,6 +113,22 @@ export const DAY_MONTH_ABBREVIATED_HOUR_MINUTE = Intl.DateTimeFormat('en-GB', {
|
|
|
92
113
|
hour12: false,
|
|
93
114
|
});
|
|
94
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @description Day month abbreviated year hour minute formatter. Used for describing long term date.
|
|
118
|
+
* @example 06 Oct 2025 18:33
|
|
119
|
+
*/
|
|
120
|
+
export const DAY_MONTH_ABBREVIATED_YEAR_HOUR_MINUTE = Intl.DateTimeFormat(
|
|
121
|
+
'en-GB',
|
|
122
|
+
{
|
|
123
|
+
day: '2-digit',
|
|
124
|
+
month: 'short',
|
|
125
|
+
year: 'numeric',
|
|
126
|
+
hour: '2-digit',
|
|
127
|
+
minute: '2-digit',
|
|
128
|
+
hour12: false,
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
95
132
|
/**
|
|
96
133
|
* @description Year month day formatter, without time. Used for describing long term date.
|
|
97
134
|
* @example 2025-01-01
|
|
@@ -121,11 +158,14 @@ type FormattedDateTimeProps = {
|
|
|
121
158
|
| 'relative'
|
|
122
159
|
| 'day-month-abbreviated-hour-minute'
|
|
123
160
|
| 'day-month-abbreviated-hour-minute-second'
|
|
161
|
+
| 'day-month-abbreviated-year-hour-minute'
|
|
124
162
|
| 'long-date'
|
|
125
163
|
| 'long-date-without-weekday'
|
|
126
164
|
| 'chart-date'
|
|
127
165
|
| 'year-month-day'
|
|
128
|
-
| 'month-day'
|
|
166
|
+
| 'month-day'
|
|
167
|
+
| 'day-month-abbreviated'
|
|
168
|
+
| 'chart-long-term-date';
|
|
129
169
|
|
|
130
170
|
value: Date;
|
|
131
171
|
};
|
|
@@ -150,10 +190,10 @@ const isItFutureOrIsItPast = (
|
|
|
150
190
|
* time: '00:00'
|
|
151
191
|
* 'time-second': '00:00:00'
|
|
152
192
|
* relative: '1 month ago'
|
|
153
|
-
* 'day-month-abbreviated-hour-minute': '
|
|
154
|
-
* 'day-month-abbreviated-hour-minute-second': '
|
|
193
|
+
* 'day-month-abbreviated-hour-minute': '06 Oct 18:33'
|
|
194
|
+
* 'day-month-abbreviated-hour-minute-second': '06 Oct 18:33:00'
|
|
155
195
|
* 'long-date': 'Wednesday 6 October 2025'
|
|
156
|
-
* 'chart-date': '
|
|
196
|
+
* 'chart-date': '06 Oct'
|
|
157
197
|
* 'year-month-day': '2025-10-06'
|
|
158
198
|
*/
|
|
159
199
|
export const FormattedDateTime = ({
|
|
@@ -290,6 +330,31 @@ export const FormattedDateTime = ({
|
|
|
290
330
|
return <>{YEAR_MONTH_DAY_FORMATTER.format(value)}</>;
|
|
291
331
|
case 'month-day':
|
|
292
332
|
return <>{MONTH_DAY_FORMATTER.format(value)}</>;
|
|
333
|
+
case 'day-month-abbreviated-year-hour-minute':
|
|
334
|
+
return (
|
|
335
|
+
<>
|
|
336
|
+
{DAY_MONTH_ABBREVIATED_YEAR_HOUR_MINUTE.format(value)
|
|
337
|
+
.replace(',', '')
|
|
338
|
+
.replace(/Sept/g, 'Sep')}
|
|
339
|
+
</>
|
|
340
|
+
);
|
|
341
|
+
case 'day-month-abbreviated':
|
|
342
|
+
return (
|
|
343
|
+
<>
|
|
344
|
+
{DAY_MONTH_ABBREVIATED.format(value)
|
|
345
|
+
.replace(',', '')
|
|
346
|
+
.replace(/Sept/g, 'Sep')}
|
|
347
|
+
</>
|
|
348
|
+
);
|
|
349
|
+
case 'chart-long-term-date':
|
|
350
|
+
return (
|
|
351
|
+
<>
|
|
352
|
+
{DAY_MONTH_ABBREVIATED_YEAR.format(value)
|
|
353
|
+
.replace(/[ ,]/g, '')
|
|
354
|
+
// replace Sept with Sep to keep 3 letter month
|
|
355
|
+
.replace(/Sept/g, 'Sep')}
|
|
356
|
+
</>
|
|
357
|
+
);
|
|
293
358
|
default:
|
|
294
359
|
return <></>;
|
|
295
360
|
}
|
|
@@ -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
|
};
|