@scality/core-ui 0.171.0 → 0.173.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.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +2 -2
- package/dist/components/barchartv2/BarchartTooltip.d.ts +11 -0
- package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -0
- package/dist/components/barchartv2/BarchartTooltip.js +27 -0
- package/dist/components/charttooltip/ChartTooltip.d.ts +13 -0
- package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -0
- package/dist/components/charttooltip/ChartTooltip.js +49 -0
- package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts +4 -0
- package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts.map +1 -1
- package/dist/components/globalhealthbar/GlobalHealthBar.component.js +4 -0
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts +10 -0
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -0
- package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +78 -0
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +18 -0
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -0
- package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +95 -0
- package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts +7 -0
- package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts.map +1 -0
- package/dist/components/globalhealthbar/components/HealthBarXAxis.js +25 -0
- package/dist/components/globalhealthbar/healthBarUtils.d.ts +77 -0
- package/dist/components/globalhealthbar/healthBarUtils.d.ts.map +1 -0
- package/dist/components/globalhealthbar/healthBarUtils.js +196 -0
- package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts +2 -0
- package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts.map +1 -0
- package/dist/components/globalhealthbar/healthBarUtils.spec.js +391 -0
- package/dist/components/globalhealthbar/useHealthBarData.d.ts +18 -0
- package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -0
- package/dist/components/globalhealthbar/useHealthBarData.js +46 -0
- package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts +2 -0
- package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts.map +1 -0
- package/dist/components/globalhealthbar/useHealthBarData.spec.js +207 -0
- package/dist/components/icon/Icon.component.js +2 -2
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +0 -2
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +17 -57
- package/dist/components/sparkline/sparkline.component.d.ts +16 -0
- package/dist/components/sparkline/sparkline.component.d.ts.map +1 -0
- package/dist/components/sparkline/sparkline.component.js +20 -0
- package/dist/components/text/Text.component.d.ts +1 -1
- package/dist/components/text/Text.component.d.ts.map +1 -1
- package/dist/components/text/Text.component.js +6 -1
- package/dist/next.d.ts +4 -1
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +4 -1
- package/package.json +2 -2
- package/src/lib/components/barchartv2/Barchart.component.tsx +3 -2
- package/src/lib/components/barchartv2/{ChartTooltip.test.tsx → BarchartTooltip.test.tsx} +35 -12
- package/src/lib/components/barchartv2/BarchartTooltip.tsx +89 -0
- package/src/lib/components/charttooltip/ChartTooltip.tsx +83 -0
- package/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx +4 -1
- package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +203 -0
- package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +173 -0
- package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +94 -0
- package/src/lib/components/globalhealthbar/healthBarUtils.spec.ts +701 -0
- package/src/lib/components/globalhealthbar/healthBarUtils.ts +311 -0
- package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +487 -0
- package/src/lib/components/globalhealthbar/useHealthBarData.ts +74 -0
- package/src/lib/components/icon/Icon.component.tsx +2 -2
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +50 -77
- package/src/lib/components/sparkline/sparkline.component.tsx +54 -0
- package/src/lib/components/text/Text.component.tsx +8 -2
- package/src/lib/next.ts +9 -1
- package/stories/GlobalHealthBar/globalhealthbarRecharts.stories.tsx +145 -0
- package/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx +5 -0
- package/stories/InlineInput/InlineInput.stories.tsx +7 -1
- package/stories/globalhealthbar.stories.tsx +25 -5
- package/stories/sparkline.stories.tsx +168 -0
- package/dist/components/barchartv2/ChartTooltip.d.ts +0 -14
- package/dist/components/barchartv2/ChartTooltip.d.ts.map +0 -1
- package/dist/components/barchartv2/ChartTooltip.js +0 -41
- package/src/lib/components/barchartv2/ChartTooltip.tsx +0 -106
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BarchartBars,
|
|
3
|
+
BarchartTooltipFn,
|
|
4
|
+
CategoryType,
|
|
5
|
+
TimeType,
|
|
6
|
+
} from './Barchart.component';
|
|
7
|
+
import { FormattedDateTime } from '../date/FormattedDateTime';
|
|
8
|
+
import { TooltipContentProps } from 'recharts';
|
|
9
|
+
import { getCurrentPoint } from './utils';
|
|
10
|
+
import {
|
|
11
|
+
ChartTooltipContainer,
|
|
12
|
+
ChartTooltipItem,
|
|
13
|
+
ChartTooltipHeader,
|
|
14
|
+
ChartTooltipItemsContainer,
|
|
15
|
+
} from '../charttooltip/ChartTooltip';
|
|
16
|
+
import { LegendShape } from '../chartlegend/ChartLegend';
|
|
17
|
+
|
|
18
|
+
export const BarchartTooltip = <T extends BarchartBars>({
|
|
19
|
+
type,
|
|
20
|
+
tooltipProps,
|
|
21
|
+
colorSet,
|
|
22
|
+
hoveredValue,
|
|
23
|
+
tooltip,
|
|
24
|
+
unitLabel,
|
|
25
|
+
}: {
|
|
26
|
+
type: TimeType | CategoryType;
|
|
27
|
+
tooltipProps: TooltipContentProps<number, string>;
|
|
28
|
+
colorSet?: Record<string, string>;
|
|
29
|
+
hoveredValue: string | undefined;
|
|
30
|
+
tooltip?: BarchartTooltipFn<T>;
|
|
31
|
+
unitLabel?: string;
|
|
32
|
+
}) => {
|
|
33
|
+
const { active } = tooltipProps;
|
|
34
|
+
|
|
35
|
+
if (!active) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const currentPoint = getCurrentPoint(tooltipProps, hoveredValue);
|
|
40
|
+
if (tooltip) {
|
|
41
|
+
return tooltip(currentPoint);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ChartTooltipContainer>
|
|
46
|
+
<ChartTooltipHeader>
|
|
47
|
+
{type.type === 'time' ? (
|
|
48
|
+
<FormattedDateTime
|
|
49
|
+
format={
|
|
50
|
+
type.timeRange.interval < 24 * 60 * 60 * 1000
|
|
51
|
+
? 'day-month-abbreviated-hour-minute-second'
|
|
52
|
+
: 'long-date-without-weekday'
|
|
53
|
+
}
|
|
54
|
+
value={new Date(currentPoint.category)}
|
|
55
|
+
/>
|
|
56
|
+
) : (
|
|
57
|
+
currentPoint.category
|
|
58
|
+
)}
|
|
59
|
+
</ChartTooltipHeader>
|
|
60
|
+
<ChartTooltipItemsContainer>
|
|
61
|
+
{currentPoint.values.map((value) => {
|
|
62
|
+
const legendIcon = colorSet && (
|
|
63
|
+
<LegendShape
|
|
64
|
+
color={colorSet[value.label as keyof typeof colorSet]}
|
|
65
|
+
shape="rectangle"
|
|
66
|
+
chartColors={colorSet}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const formattedValue = Number.isInteger(value.value)
|
|
71
|
+
? `${value.value}`
|
|
72
|
+
: value.value.toFixed(2);
|
|
73
|
+
const valueWithUnit = unitLabel
|
|
74
|
+
? `${formattedValue} ${unitLabel}`
|
|
75
|
+
: formattedValue;
|
|
76
|
+
return (
|
|
77
|
+
<ChartTooltipItem
|
|
78
|
+
key={value.label}
|
|
79
|
+
label={value.label}
|
|
80
|
+
value={valueWithUnit}
|
|
81
|
+
isHovered={value.isHovered}
|
|
82
|
+
legendIcon={legendIcon}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</ChartTooltipItemsContainer>
|
|
87
|
+
</ChartTooltipContainer>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { XAxis } from 'recharts';
|
|
2
|
+
import { useTheme } from 'styled-components';
|
|
3
|
+
import { fontSize } from '../../../style/theme';
|
|
4
|
+
import {
|
|
5
|
+
getTicks,
|
|
6
|
+
calculateLabelVisibility,
|
|
7
|
+
TIME_CONSTANTS,
|
|
8
|
+
getEdgeMargin,
|
|
9
|
+
} from '../healthBarUtils';
|
|
10
|
+
import { FormattedDateTime } from '../../date/FormattedDateTime';
|
|
11
|
+
|
|
12
|
+
interface HealthBarXAxisProps {
|
|
13
|
+
startTimestamp: number;
|
|
14
|
+
endTimestamp: number;
|
|
15
|
+
}
|
|
16
|
+
const CustomTick = ({
|
|
17
|
+
tickProps,
|
|
18
|
+
startTimestamp,
|
|
19
|
+
endTimestamp,
|
|
20
|
+
}: {
|
|
21
|
+
tickProps: any;
|
|
22
|
+
startTimestamp: number;
|
|
23
|
+
endTimestamp: number;
|
|
24
|
+
}) => {
|
|
25
|
+
const theme = useTheme();
|
|
26
|
+
const { y, payload, width, index, visibleTicksCount } = tickProps;
|
|
27
|
+
const span = endTimestamp - startTimestamp;
|
|
28
|
+
const is7DaySpan = span === 7 * TIME_CONSTANTS.ONE_DAY;
|
|
29
|
+
const isDaySpan = span === TIME_CONSTANTS.ONE_DAY;
|
|
30
|
+
|
|
31
|
+
const shouldShowLabel = calculateLabelVisibility(
|
|
32
|
+
width,
|
|
33
|
+
visibleTicksCount,
|
|
34
|
+
span,
|
|
35
|
+
index,
|
|
36
|
+
endTimestamp,
|
|
37
|
+
);
|
|
38
|
+
const edgeMargin = getEdgeMargin(index, visibleTicksCount, isDaySpan);
|
|
39
|
+
return (
|
|
40
|
+
// use coordinate to center the text
|
|
41
|
+
shouldShowLabel && (
|
|
42
|
+
<g transform={`translate(${payload.coordinate},${y})`}>
|
|
43
|
+
<text
|
|
44
|
+
textAnchor="middle"
|
|
45
|
+
dy={10}
|
|
46
|
+
dx={edgeMargin}
|
|
47
|
+
fontSize={fontSize.smaller}
|
|
48
|
+
fill={theme.textSecondary}
|
|
49
|
+
>
|
|
50
|
+
{is7DaySpan || isDaySpan ? (
|
|
51
|
+
<FormattedDateTime
|
|
52
|
+
format="day-month-abbreviated-hour-minute"
|
|
53
|
+
value={new Date(payload.value)}
|
|
54
|
+
/>
|
|
55
|
+
) : (
|
|
56
|
+
<FormattedDateTime format="time" value={new Date(payload.value)} />
|
|
57
|
+
)}
|
|
58
|
+
</text>
|
|
59
|
+
</g>
|
|
60
|
+
)
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const HealthBarXAxis = ({
|
|
65
|
+
startTimestamp,
|
|
66
|
+
endTimestamp,
|
|
67
|
+
}: HealthBarXAxisProps) => {
|
|
68
|
+
const theme = useTheme();
|
|
69
|
+
const ticks = getTicks(startTimestamp, endTimestamp);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<XAxis
|
|
73
|
+
allowDataOverflow={true}
|
|
74
|
+
dataKey="start"
|
|
75
|
+
type="number"
|
|
76
|
+
domain={[startTimestamp, endTimestamp]}
|
|
77
|
+
tickSize={5}
|
|
78
|
+
minTickGap={10}
|
|
79
|
+
interval={0}
|
|
80
|
+
tick={(props: any) => {
|
|
81
|
+
return (
|
|
82
|
+
<CustomTick
|
|
83
|
+
tickProps={props}
|
|
84
|
+
startTimestamp={startTimestamp}
|
|
85
|
+
endTimestamp={endTimestamp}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}}
|
|
89
|
+
ticks={ticks}
|
|
90
|
+
tickLine={{ stroke: theme.textSecondary }}
|
|
91
|
+
axisLine={false}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
};
|