@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.
- package/__mocks__/uuid.js +11 -0
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +4 -4
- 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/chartlegend/ChartLegend.d.ts +3 -1
- package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -1
- package/dist/components/chartlegend/ChartLegend.js +2 -2
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts +3 -1
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -1
- package/dist/components/chartlegend/ChartLegendWrapper.js +43 -9
- 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.d.ts +2 -0
- package/dist/components/icon/Icon.component.d.ts.map +1 -1
- package/dist/components/icon/Icon.component.js +2 -0
- package/dist/components/linetemporalchart/ChartUtil.d.ts.map +1 -1
- package/dist/components/linetemporalchart/ChartUtil.js +12 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +8 -5
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +95 -100
- 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 +2 -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 -2
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +4 -2
- package/package.json +4 -2
- package/src/lib/components/barchartv2/Barchart.component.tsx +5 -4
- 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/chartlegend/ChartLegend.tsx +4 -2
- package/src/lib/components/chartlegend/ChartLegendWrapper.test.tsx +197 -0
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +65 -9
- 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 -0
- package/src/lib/components/linetemporalchart/ChartUtil.ts +26 -0
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +272 -229
- package/src/lib/components/sparkline/sparkline.component.tsx +54 -0
- package/src/lib/components/text/Text.component.tsx +15 -2
- package/src/lib/next.ts +12 -2
- package/stories/BarChart/barchart.stories.tsx +7 -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/linetimeseriechart.stories.tsx +217 -1
- 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
|
@@ -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:
|
|
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
|
|
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(
|
|
50
|
-
}, [
|
|
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 =
|
|
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
|
-
[
|
|
132
|
+
[internalColorSet],
|
|
78
133
|
);
|
|
79
134
|
|
|
80
135
|
const listResources = useCallback(() => {
|
|
81
|
-
return Object.keys(
|
|
82
|
-
}, [
|
|
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
|
+
};
|