@scality/core-ui 0.171.0 → 0.172.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
  2. package/dist/components/barchartv2/Barchart.component.js +2 -2
  3. package/dist/components/barchartv2/BarchartTooltip.d.ts +11 -0
  4. package/dist/components/barchartv2/BarchartTooltip.d.ts.map +1 -0
  5. package/dist/components/barchartv2/BarchartTooltip.js +27 -0
  6. package/dist/components/charttooltip/ChartTooltip.d.ts +13 -0
  7. package/dist/components/charttooltip/ChartTooltip.d.ts.map +1 -0
  8. package/dist/components/charttooltip/ChartTooltip.js +49 -0
  9. package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts +4 -0
  10. package/dist/components/globalhealthbar/GlobalHealthBar.component.d.ts.map +1 -1
  11. package/dist/components/globalhealthbar/GlobalHealthBar.component.js +4 -0
  12. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts +10 -0
  13. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.d.ts.map +1 -0
  14. package/dist/components/globalhealthbar/GlobalHealthBarRecharts.component.js +78 -0
  15. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts +18 -0
  16. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.d.ts.map +1 -0
  17. package/dist/components/globalhealthbar/components/GlobalHealthBarTooltip.js +95 -0
  18. package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts +7 -0
  19. package/dist/components/globalhealthbar/components/HealthBarXAxis.d.ts.map +1 -0
  20. package/dist/components/globalhealthbar/components/HealthBarXAxis.js +25 -0
  21. package/dist/components/globalhealthbar/healthBarUtils.d.ts +77 -0
  22. package/dist/components/globalhealthbar/healthBarUtils.d.ts.map +1 -0
  23. package/dist/components/globalhealthbar/healthBarUtils.js +196 -0
  24. package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts +2 -0
  25. package/dist/components/globalhealthbar/healthBarUtils.spec.d.ts.map +1 -0
  26. package/dist/components/globalhealthbar/healthBarUtils.spec.js +391 -0
  27. package/dist/components/globalhealthbar/useHealthBarData.d.ts +18 -0
  28. package/dist/components/globalhealthbar/useHealthBarData.d.ts.map +1 -0
  29. package/dist/components/globalhealthbar/useHealthBarData.js +46 -0
  30. package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts +2 -0
  31. package/dist/components/globalhealthbar/useHealthBarData.spec.d.ts.map +1 -0
  32. package/dist/components/globalhealthbar/useHealthBarData.spec.js +207 -0
  33. package/dist/components/icon/Icon.component.js +2 -2
  34. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +0 -2
  35. package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -1
  36. package/dist/components/linetimeseriechart/linetimeseriechart.component.js +17 -57
  37. package/dist/components/sparkline/sparkline.component.d.ts +16 -0
  38. package/dist/components/sparkline/sparkline.component.d.ts.map +1 -0
  39. package/dist/components/sparkline/sparkline.component.js +20 -0
  40. package/dist/components/text/Text.component.d.ts +1 -1
  41. package/dist/components/text/Text.component.d.ts.map +1 -1
  42. package/dist/components/text/Text.component.js +6 -1
  43. package/dist/next.d.ts +3 -1
  44. package/dist/next.d.ts.map +1 -1
  45. package/dist/next.js +3 -1
  46. package/package.json +2 -2
  47. package/src/lib/components/barchartv2/Barchart.component.tsx +3 -2
  48. package/src/lib/components/barchartv2/{ChartTooltip.test.tsx → BarchartTooltip.test.tsx} +35 -12
  49. package/src/lib/components/barchartv2/BarchartTooltip.tsx +89 -0
  50. package/src/lib/components/charttooltip/ChartTooltip.tsx +83 -0
  51. package/src/lib/components/globalhealthbar/GlobalHealthBar.component.tsx +4 -1
  52. package/src/lib/components/globalhealthbar/GlobalHealthBarRecharts.component.tsx +203 -0
  53. package/src/lib/components/globalhealthbar/components/GlobalHealthBarTooltip.tsx +173 -0
  54. package/src/lib/components/globalhealthbar/components/HealthBarXAxis.tsx +94 -0
  55. package/src/lib/components/globalhealthbar/healthBarUtils.spec.ts +701 -0
  56. package/src/lib/components/globalhealthbar/healthBarUtils.ts +311 -0
  57. package/src/lib/components/globalhealthbar/useHealthBarData.spec.tsx +487 -0
  58. package/src/lib/components/globalhealthbar/useHealthBarData.ts +74 -0
  59. package/src/lib/components/icon/Icon.component.tsx +2 -2
  60. package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +50 -77
  61. package/src/lib/components/sparkline/sparkline.component.tsx +54 -0
  62. package/src/lib/components/text/Text.component.tsx +8 -2
  63. package/src/lib/next.ts +8 -1
  64. package/stories/GlobalHealthBar/globalhealthbarRecharts.stories.tsx +145 -0
  65. package/stories/GlobalHealthBar/globalheathbarrecharts.guideline.mdx +5 -0
  66. package/stories/InlineInput/InlineInput.stories.tsx +7 -1
  67. package/stories/globalhealthbar.stories.tsx +25 -5
  68. package/stories/sparkline.stories.tsx +168 -0
  69. package/dist/components/barchartv2/ChartTooltip.d.ts +0 -14
  70. package/dist/components/barchartv2/ChartTooltip.d.ts.map +0 -1
  71. package/dist/components/barchartv2/ChartTooltip.js +0 -41
  72. package/src/lib/components/barchartv2/ChartTooltip.tsx +0 -106
@@ -0,0 +1,196 @@
1
+ import { fontSize } from '../../style/theme';
2
+ // =============================================================================
3
+ // CONSTANTS
4
+ // =============================================================================
5
+ export const CHART_CONFIG = {
6
+ RADIUS_SIZE: 4,
7
+ EDGE_THRESHOLD: 8,
8
+ CHART_HEIGHT: 50,
9
+ BAR_SIZE: 8,
10
+ TICK_SIZE: 4,
11
+ TOOLTIP_OFFSET: 24,
12
+ FONT_SIZE: fontSize.smaller,
13
+ TEXT_DY_OFFSET: 12,
14
+ TICK_INTERVAL: 0,
15
+ MARGINS: { left: 28, right: 28, bottom: 4, top: 4 },
16
+ };
17
+ export const TIME_CONSTANTS = {
18
+ ONE_HOUR: 60 * 60 * 1000,
19
+ ONE_DAY: 24 * 60 * 60 * 1000,
20
+ ONE_WEEK: 7 * 24 * 60 * 60 * 1000,
21
+ MARGIN_HOURS: 6,
22
+ FIFTEEN_MINUTES: 15 * 60 * 1000,
23
+ SIX_HOURS: 6 * 60 * 60 * 1000,
24
+ };
25
+ export const LABEL_CONFIG = {
26
+ MIN_SPACE_PER_TICK: 80,
27
+ MODULO_CONFIG: {
28
+ [TIME_CONSTANTS.ONE_WEEK]: 2,
29
+ [TIME_CONSTANTS.ONE_DAY]: 3,
30
+ [TIME_CONSTANTS.ONE_HOUR]: 3,
31
+ },
32
+ };
33
+ // =============================================================================
34
+ // TICK CALCULATIONS
35
+ // =============================================================================
36
+ const { ONE_HOUR, ONE_DAY, MARGIN_HOURS, FIFTEEN_MINUTES, SIX_HOURS } = TIME_CONSTANTS;
37
+ const generateTickArray = (endTimestamp, count, interval) => {
38
+ return Array.from({ length: count }, (_, i) => endTimestamp - i * interval);
39
+ };
40
+ const roundToNearestHalfDay = (timestamp) => {
41
+ const date = new Date(timestamp);
42
+ const hours = date.getHours();
43
+ if (hours <= 12) {
44
+ return new Date(timestamp).setHours(0, 0, 0, 0);
45
+ }
46
+ else {
47
+ return new Date(timestamp).setHours(12, 0, 0, 0);
48
+ }
49
+ };
50
+ export const calculateSevenDayTicks = (endTimestamp) => {
51
+ const marginedEnd = endTimestamp - MARGIN_HOURS * ONE_HOUR;
52
+ const roundedEnd = roundToNearestHalfDay(marginedEnd);
53
+ return generateTickArray(roundedEnd, 7, ONE_DAY);
54
+ };
55
+ export const calculateDayTicks = (endTimestamp) => {
56
+ const is6HourTick = endTimestamp % SIX_HOURS === 0;
57
+ const closest6Hours = Math.floor(endTimestamp / SIX_HOURS) * SIX_HOURS;
58
+ const tickCount = is6HourTick ? 5 : 4;
59
+ return generateTickArray(closest6Hours, tickCount, SIX_HOURS);
60
+ };
61
+ export const calculateHourTicks = (endTimestamp) => {
62
+ const is15MinuteTick = endTimestamp % FIFTEEN_MINUTES === 0;
63
+ const closest15Minutes = Math.floor(endTimestamp / FIFTEEN_MINUTES) * FIFTEEN_MINUTES;
64
+ const tickCount = is15MinuteTick ? 5 : 4;
65
+ return generateTickArray(closest15Minutes, tickCount, FIFTEEN_MINUTES);
66
+ };
67
+ export const getEdgeMargin = (index, totalTicks, isDaySpan) => {
68
+ if (isDaySpan && totalTicks === 5) {
69
+ return index === 0 ? -8 : index === totalTicks - 1 ? 8 : 0;
70
+ }
71
+ return 0;
72
+ };
73
+ export const getTicks = (startTimestamp, endTimestamp) => {
74
+ const span = endTimestamp - startTimestamp;
75
+ if (span === 7 * ONE_DAY) {
76
+ return calculateSevenDayTicks(endTimestamp);
77
+ }
78
+ else if (span === 24 * ONE_HOUR) {
79
+ return calculateDayTicks(endTimestamp);
80
+ }
81
+ else if (span === ONE_HOUR) {
82
+ return calculateHourTicks(endTimestamp);
83
+ }
84
+ return [];
85
+ };
86
+ export const calculateLabelVisibility = (chartWidth, totalTicks, span, index, endTimestamp) => {
87
+ const hasEnoughSpace = chartWidth / totalTicks > LABEL_CONFIG.MIN_SPACE_PER_TICK;
88
+ // If enough space, show all labels
89
+ if (hasEnoughSpace)
90
+ return true;
91
+ // Apply specific rules for each time range
92
+ if (span === TIME_CONSTANTS.ONE_WEEK) {
93
+ return index % 2 === 0;
94
+ }
95
+ if (span === TIME_CONSTANTS.ONE_DAY) {
96
+ const isRoundHour = endTimestamp % (60 * 60 * 1000) === 0;
97
+ const roundHourInterval = index % 2 === 0;
98
+ const defaultInterval = index % 3 === 0;
99
+ const result = isRoundHour ? roundHourInterval : defaultInterval;
100
+ return result;
101
+ }
102
+ if (span === TIME_CONSTANTS.ONE_HOUR) {
103
+ const isRound15Minute = endTimestamp % (15 * 60 * 1000) === 0;
104
+ const round15MinuteInterval = index % 2 === 0;
105
+ const defaultInterval = index % 3 === 0;
106
+ const result = isRound15Minute ? round15MinuteInterval : defaultInterval;
107
+ return result;
108
+ }
109
+ return false;
110
+ };
111
+ // =============================================================================
112
+ // RECTANGLE PROPERTIES
113
+ // =============================================================================
114
+ /**
115
+ * Core calculation for alert positioning within a time range
116
+ */
117
+ export const calculateAlertPosition = (alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX = 0) => {
118
+ const start = Math.max(alertStartTimestamp, chartStartTimestamp);
119
+ const end = Math.min(alertEndTimestamp, chartEndTimestamp);
120
+ const totalTimeSpan = chartEndTimestamp - chartStartTimestamp;
121
+ const relativeSize = (end - start) / totalTimeSpan;
122
+ // Calculate start position relative to baseX
123
+ let startX = baseX;
124
+ if (alertStartTimestamp > chartStartTimestamp) {
125
+ const alertStartRelative = (alertStartTimestamp - chartStartTimestamp) / totalTimeSpan;
126
+ startX = baseX + alertStartRelative * availableWidth;
127
+ }
128
+ const width = relativeSize * availableWidth;
129
+ return { startX, width, relativeSize };
130
+ };
131
+ /**
132
+ * Maps keyboard events to navigation actions
133
+ */
134
+ export const getNavigationAction = (key) => {
135
+ switch (key) {
136
+ case 'ArrowLeft':
137
+ case 'ArrowUp':
138
+ return 'previous';
139
+ case 'ArrowRight':
140
+ case 'ArrowDown':
141
+ return 'next';
142
+ case 'Home':
143
+ return 'first';
144
+ case 'End':
145
+ return 'last';
146
+ case 'Escape':
147
+ return 'escape';
148
+ default:
149
+ return null;
150
+ }
151
+ };
152
+ /**
153
+ * Calculates new index based on navigation action
154
+ */
155
+ export const calculateNavigationIndex = (action, currentIndex, arrayLength) => {
156
+ if (arrayLength === 0)
157
+ return -1;
158
+ switch (action) {
159
+ case 'previous':
160
+ return currentIndex <= 0 ? arrayLength - 1 : currentIndex - 1;
161
+ case 'next':
162
+ return currentIndex >= arrayLength - 1 ? 0 : currentIndex + 1;
163
+ case 'first':
164
+ return 0;
165
+ case 'last':
166
+ return arrayLength - 1;
167
+ case 'escape':
168
+ return -1;
169
+ default:
170
+ return currentIndex;
171
+ }
172
+ };
173
+ /**
174
+ * Gets complete navigation state update for a given action
175
+ */
176
+ export const getNavigationStateUpdate = (action, currentIndex, alerts) => {
177
+ const newIndex = calculateNavigationIndex(action, currentIndex, alerts.length);
178
+ return {
179
+ newIndex,
180
+ selectedAlert: newIndex >= 0 ? alerts[newIndex] : null,
181
+ shouldActivateKeyboard: action !== 'escape',
182
+ };
183
+ };
184
+ // =============================================================================
185
+ // TOOLTIP UTILS
186
+ // =============================================================================
187
+ /**
188
+ * Calculates tooltip position for an alert based on its time range
189
+ */
190
+ export const getTooltipPosition = (alert, startTimestamp, endTimestamp, chartUsableWidth) => {
191
+ const alertStart = new Date(alert.startsAt).getTime();
192
+ const alertEnd = new Date(alert.endsAt).getTime();
193
+ const { startX, width } = calculateAlertPosition(alertStart, alertEnd, startTimestamp, endTimestamp, chartUsableWidth, CHART_CONFIG.MARGINS.left);
194
+ // Return center position of the alert
195
+ return startX + width / 2;
196
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=healthBarUtils.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"healthBarUtils.spec.d.ts","sourceRoot":"","sources":["../../../src/lib/components/globalhealthbar/healthBarUtils.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,391 @@
1
+ import { TIME_CONSTANTS, calculateSevenDayTicks, calculateDayTicks, calculateHourTicks, getTicks, getEdgeMargin, calculateLabelVisibility, calculateAlertPosition, getNavigationAction, calculateNavigationIndex, getNavigationStateUpdate, } from './healthBarUtils';
2
+ describe('Health Bar Utils', () => {
3
+ describe('Tick Calculations', () => {
4
+ describe('calculateSevenDayTicks', () => {
5
+ it('should calculate 7 ticks for a week span with proper spacing', () => {
6
+ const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
7
+ const ticks = calculateSevenDayTicks(endTimestamp);
8
+ expect(ticks).toHaveLength(7);
9
+ // Check that ticks are spaced by one day
10
+ for (let i = 1; i < ticks.length; i++) {
11
+ expect(ticks[i - 1] - ticks[i]).toBe(TIME_CONSTANTS.ONE_DAY);
12
+ }
13
+ });
14
+ it('should apply margin hours offset', () => {
15
+ const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
16
+ const ticks = calculateSevenDayTicks(endTimestamp);
17
+ // First tick should be less than endTimestamp due to margin
18
+ expect(ticks[0]).toBeLessThan(endTimestamp);
19
+ // The difference should account for margin hours
20
+ const expectedMargin = TIME_CONSTANTS.MARGIN_HOURS * TIME_CONSTANTS.ONE_HOUR;
21
+ expect(endTimestamp - ticks[0]).toBeGreaterThan(expectedMargin);
22
+ });
23
+ it('should round to nearest half day', () => {
24
+ const endTimestamp = new Date('2023-12-07T15:30:45Z').getTime();
25
+ const ticks = calculateSevenDayTicks(endTimestamp);
26
+ const firstTickDate = new Date(ticks[0]);
27
+ // Should be rounded to either 00:00 or 12:00
28
+ expect([0, 12]).toContain(firstTickDate.getUTCHours());
29
+ expect(firstTickDate.getUTCMinutes()).toBe(0);
30
+ expect(firstTickDate.getUTCSeconds()).toBe(0);
31
+ });
32
+ });
33
+ describe('calculateDayTicks', () => {
34
+ it('should calculate 5 ticks when end timestamp aligns with 6-hour boundary', () => {
35
+ const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
36
+ const ticks = calculateDayTicks(endTimestamp);
37
+ expect(ticks).toHaveLength(5);
38
+ });
39
+ it('should calculate 4 ticks when end timestamp does not align with 6-hour boundary', () => {
40
+ const endTimestamp = new Date('2023-12-07T13:30:00Z').getTime();
41
+ const ticks = calculateDayTicks(endTimestamp);
42
+ expect(ticks).toHaveLength(4);
43
+ });
44
+ it('should space ticks by 6 hours', () => {
45
+ const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
46
+ const ticks = calculateDayTicks(endTimestamp);
47
+ for (let i = 1; i < ticks.length; i++) {
48
+ expect(ticks[i - 1] - ticks[i]).toBe(TIME_CONSTANTS.SIX_HOURS);
49
+ }
50
+ });
51
+ it('should start from closest 6-hour boundary', () => {
52
+ const endTimestamp = new Date('2023-12-07T14:30:00Z').getTime();
53
+ const ticks = calculateDayTicks(endTimestamp);
54
+ const firstTickDate = new Date(ticks[0]);
55
+ expect(firstTickDate.getUTCHours() % 6).toBe(0);
56
+ expect(firstTickDate.getUTCMinutes()).toBe(0);
57
+ expect(firstTickDate.getUTCSeconds()).toBe(0);
58
+ });
59
+ });
60
+ describe('calculateHourTicks', () => {
61
+ it('should calculate 5 ticks when end timestamp aligns with 15-minute boundary', () => {
62
+ const endTimestamp = new Date('2023-12-07T12:15:00Z').getTime();
63
+ const ticks = calculateHourTicks(endTimestamp);
64
+ expect(ticks).toHaveLength(5);
65
+ });
66
+ it('should calculate 4 ticks when end timestamp does not align with 15-minute boundary', () => {
67
+ const endTimestamp = new Date('2023-12-07T12:17:00Z').getTime();
68
+ const ticks = calculateHourTicks(endTimestamp);
69
+ expect(ticks).toHaveLength(4);
70
+ });
71
+ it('should space ticks by 15 minutes', () => {
72
+ const endTimestamp = new Date('2023-12-07T12:15:00Z').getTime();
73
+ const ticks = calculateHourTicks(endTimestamp);
74
+ for (let i = 1; i < ticks.length; i++) {
75
+ expect(ticks[i - 1] - ticks[i]).toBe(TIME_CONSTANTS.FIFTEEN_MINUTES);
76
+ }
77
+ });
78
+ it('should start from closest 15-minute boundary', () => {
79
+ const endTimestamp = new Date('2023-12-07T12:17:30Z').getTime();
80
+ const ticks = calculateHourTicks(endTimestamp);
81
+ const firstTickDate = new Date(ticks[0]);
82
+ // Test time is 12:17 so the first tick should be 12:15
83
+ expect(firstTickDate.getUTCHours()).toBe(12);
84
+ expect(firstTickDate.getUTCMinutes()).toBe(15);
85
+ expect(firstTickDate.getUTCSeconds()).toBe(0);
86
+ });
87
+ });
88
+ describe('getTicks', () => {
89
+ it('should return seven day ticks for week span', () => {
90
+ const startTimestamp = new Date('2023-12-01T00:00:00Z').getTime();
91
+ const endTimestamp = startTimestamp + 7 * TIME_CONSTANTS.ONE_DAY;
92
+ const ticks = getTicks(startTimestamp, endTimestamp);
93
+ expect(ticks).toHaveLength(7);
94
+ });
95
+ it('should return day ticks for 24-hour span', () => {
96
+ const startTimestamp = new Date('2023-12-07T00:00:00Z').getTime();
97
+ const endTimestamp = startTimestamp + 24 * TIME_CONSTANTS.ONE_HOUR;
98
+ const ticks = getTicks(startTimestamp, endTimestamp);
99
+ expect(ticks.length).toBe(5);
100
+ });
101
+ it('should return hour ticks for 1-hour span', () => {
102
+ const startTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
103
+ const endTimestamp = startTimestamp + TIME_CONSTANTS.ONE_HOUR;
104
+ const ticks = getTicks(startTimestamp, endTimestamp);
105
+ expect(ticks.length).toBe(5);
106
+ });
107
+ it('should return empty array for unsupported time spans', () => {
108
+ const startTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
109
+ const endTimestamp = startTimestamp + 2 * TIME_CONSTANTS.ONE_HOUR;
110
+ const ticks = getTicks(startTimestamp, endTimestamp);
111
+ expect(ticks).toEqual([]);
112
+ });
113
+ });
114
+ });
115
+ describe('Edge Margin Calculations', () => {
116
+ describe('getEdgeMargin', () => {
117
+ it('should return -8 for first tick in day span with 5 total ticks', () => {
118
+ const margin = getEdgeMargin(0, 5, true);
119
+ expect(margin).toBe(-8);
120
+ });
121
+ it('should return 8 for last tick in day span with 5 total ticks', () => {
122
+ const margin = getEdgeMargin(4, 5, true);
123
+ expect(margin).toBe(8);
124
+ });
125
+ it('should return 0 for middle ticks in day span with 5 total ticks', () => {
126
+ const margin = getEdgeMargin(2, 5, true);
127
+ expect(margin).toBe(0);
128
+ });
129
+ it('should return 0 for non-day spans', () => {
130
+ const margin = getEdgeMargin(0, 5, false);
131
+ expect(margin).toBe(0);
132
+ });
133
+ it('should return 0 for day span with non-5 total ticks', () => {
134
+ const margin = getEdgeMargin(0, 4, true);
135
+ expect(margin).toBe(0);
136
+ });
137
+ });
138
+ });
139
+ describe('Label Visibility', () => {
140
+ describe('calculateLabelVisibility', () => {
141
+ it('should return true when chart has enough space per tick', () => {
142
+ const chartWidth = 500;
143
+ const totalTicks = 5;
144
+ const span = TIME_CONSTANTS.ONE_DAY;
145
+ const index = 0;
146
+ const endTimestamp = Date.now();
147
+ const visible = calculateLabelVisibility(chartWidth, totalTicks, span, index, endTimestamp);
148
+ expect(visible).toBe(true);
149
+ });
150
+ it('should apply week span rules when space is limited', () => {
151
+ const chartWidth = 200;
152
+ const totalTicks = 7;
153
+ const span = TIME_CONSTANTS.ONE_WEEK;
154
+ const endTimestamp = Date.now();
155
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 0, endTimestamp)).toBe(true);
156
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 1, endTimestamp)).toBe(false);
157
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 2, endTimestamp)).toBe(true);
158
+ });
159
+ it('should apply day span rules when space is limited', () => {
160
+ const chartWidth = 200;
161
+ const totalTicks = 4;
162
+ const span = TIME_CONSTANTS.ONE_DAY;
163
+ const endTimestamp = new Date('2023-12-07T12:10:00Z').getTime();
164
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 0, endTimestamp)).toBe(true);
165
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 1, endTimestamp)).toBe(false);
166
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 2, endTimestamp)).toBe(false);
167
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 3, endTimestamp)).toBe(true);
168
+ });
169
+ it('should handle special cases for day span with hour-aligned end timestamp', () => {
170
+ const chartWidth = 200;
171
+ const totalTicks = 5;
172
+ const span = TIME_CONSTANTS.ONE_DAY;
173
+ const endTimestamp = new Date('2023-12-07T12:00:00Z').getTime();
174
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 0, endTimestamp)).toBe(true);
175
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 2, endTimestamp)).toBe(true);
176
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 4, endTimestamp)).toBe(true);
177
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 3, endTimestamp)).toBe(false);
178
+ });
179
+ it('should apply day span rules when space is limited for non-round hour', () => {
180
+ const chartWidth = 200;
181
+ const totalTicks = 4;
182
+ const span = TIME_CONSTANTS.ONE_DAY;
183
+ const endTimestamp = new Date('2023-12-07T12:10:00Z').getTime();
184
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 0, endTimestamp)).toBe(true);
185
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 1, endTimestamp)).toBe(false);
186
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 2, endTimestamp)).toBe(false);
187
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 3, endTimestamp)).toBe(true);
188
+ });
189
+ it('should handle special cases for hour span with 15-minute aligned end timestamp', () => {
190
+ const chartWidth = 200;
191
+ const totalTicks = 5;
192
+ const span = TIME_CONSTANTS.ONE_HOUR;
193
+ const endTimestamp = new Date('2023-12-07T12:15:00Z').getTime();
194
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 0, endTimestamp)).toBe(true);
195
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 2, endTimestamp)).toBe(true);
196
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 4, endTimestamp)).toBe(true);
197
+ expect(calculateLabelVisibility(chartWidth, totalTicks, span, 3, endTimestamp)).toBe(false);
198
+ });
199
+ it('should return false for unsupported time spans when space is limited', () => {
200
+ const chartWidth = 200;
201
+ const totalTicks = 5;
202
+ const span = 123456;
203
+ const index = 0;
204
+ const endTimestamp = Date.now();
205
+ const visible = calculateLabelVisibility(chartWidth, totalTicks, span, index, endTimestamp);
206
+ expect(visible).toBe(false);
207
+ });
208
+ });
209
+ });
210
+ describe('Alert Position Calculations', () => {
211
+ describe('calculateAlertPosition', () => {
212
+ it('should calculate position for alert within time range', () => {
213
+ const alertStartTimestamp = 2000;
214
+ const alertEndTimestamp = 3000;
215
+ const chartStartTimestamp = 1000;
216
+ const chartEndTimestamp = 5000;
217
+ const availableWidth = 200;
218
+ const baseX = 50;
219
+ const result = calculateAlertPosition(alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX);
220
+ const expectedRelativeSize = (3000 - 2000) / (5000 - 1000);
221
+ const expectedWidth = expectedRelativeSize * 200;
222
+ const expectedStartX = 50 + ((2000 - 1000) / (5000 - 1000)) * 200;
223
+ expect(result.width).toBe(expectedWidth);
224
+ expect(result.startX).toBe(expectedStartX);
225
+ expect(result.relativeSize).toBe(expectedRelativeSize);
226
+ });
227
+ it('should handle alert starting before time range', () => {
228
+ const alertStartTimestamp = 1000;
229
+ const alertEndTimestamp = 3000;
230
+ const chartStartTimestamp = 2000;
231
+ const chartEndTimestamp = 5000;
232
+ const availableWidth = 200;
233
+ const baseX = 50;
234
+ const result = calculateAlertPosition(alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX);
235
+ const expectedRelativeSize = (3000 - 2000) / (5000 - 2000);
236
+ const expectedWidth = expectedRelativeSize * 200;
237
+ const expectedStartX = 50; // baseX since alert starts before chart range
238
+ expect(result.width).toBe(expectedWidth);
239
+ expect(result.startX).toBe(expectedStartX);
240
+ expect(result.relativeSize).toBe(expectedRelativeSize);
241
+ });
242
+ it('should handle alert ending after time range', () => {
243
+ const alertStartTimestamp = 2500;
244
+ const alertEndTimestamp = 6000;
245
+ const chartStartTimestamp = 1000;
246
+ const chartEndTimestamp = 4000;
247
+ const availableWidth = 200;
248
+ const baseX = 50;
249
+ const result = calculateAlertPosition(alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX);
250
+ const expectedRelativeSize = (4000 - 2500) / (4000 - 1000);
251
+ const expectedWidth = expectedRelativeSize * 200;
252
+ const expectedStartX = 50 + ((2500 - 1000) / (4000 - 1000)) * 200;
253
+ expect(result.width).toBe(expectedWidth);
254
+ expect(result.startX).toBe(expectedStartX);
255
+ expect(result.relativeSize).toBe(expectedRelativeSize);
256
+ });
257
+ it('should handle alert spanning entire time range', () => {
258
+ const alertStartTimestamp = 1000;
259
+ const alertEndTimestamp = 5000;
260
+ const chartStartTimestamp = 2000;
261
+ const chartEndTimestamp = 4000;
262
+ const availableWidth = 200;
263
+ const baseX = 50;
264
+ const result = calculateAlertPosition(alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth, baseX);
265
+ const expectedStartX = 50; // baseX since alert starts before chart range
266
+ expect(result.width).toBe(200);
267
+ expect(result.startX).toBe(expectedStartX);
268
+ expect(result.relativeSize).toBe(1);
269
+ });
270
+ it('should work with default baseX of 0', () => {
271
+ const alertStartTimestamp = 2000;
272
+ const alertEndTimestamp = 3000;
273
+ const chartStartTimestamp = 1000;
274
+ const chartEndTimestamp = 5000;
275
+ const availableWidth = 200;
276
+ const result = calculateAlertPosition(alertStartTimestamp, alertEndTimestamp, chartStartTimestamp, chartEndTimestamp, availableWidth);
277
+ const expectedRelativeSize = (3000 - 2000) / (5000 - 1000);
278
+ const expectedWidth = expectedRelativeSize * 200;
279
+ const expectedStartX = 0 + ((2000 - 1000) / (5000 - 1000)) * 200;
280
+ expect(result.width).toBe(expectedWidth);
281
+ expect(result.startX).toBe(expectedStartX);
282
+ expect(result.relativeSize).toBe(expectedRelativeSize);
283
+ });
284
+ });
285
+ });
286
+ describe('Keyboard Navigation Utils', () => {
287
+ describe('getNavigationAction', () => {
288
+ it('should map arrow keys to navigation actions', () => {
289
+ expect(getNavigationAction('ArrowLeft')).toBe('previous');
290
+ expect(getNavigationAction('ArrowUp')).toBe('previous');
291
+ expect(getNavigationAction('ArrowRight')).toBe('next');
292
+ expect(getNavigationAction('ArrowDown')).toBe('next');
293
+ });
294
+ it('should map Home and End keys', () => {
295
+ expect(getNavigationAction('Home')).toBe('first');
296
+ expect(getNavigationAction('End')).toBe('last');
297
+ });
298
+ it('should map Escape key', () => {
299
+ expect(getNavigationAction('Escape')).toBe('escape');
300
+ });
301
+ it('should return null for unmapped keys', () => {
302
+ expect(getNavigationAction('Enter')).toBe(null);
303
+ expect(getNavigationAction('Space')).toBe(null);
304
+ expect(getNavigationAction('Tab')).toBe(null);
305
+ });
306
+ });
307
+ describe('calculateNavigationIndex', () => {
308
+ const arrayLength = 5;
309
+ it('should handle previous navigation', () => {
310
+ expect(calculateNavigationIndex('previous', 2, arrayLength)).toBe(1);
311
+ expect(calculateNavigationIndex('previous', 0, arrayLength)).toBe(4); // wraps to end
312
+ });
313
+ it('should handle next navigation', () => {
314
+ expect(calculateNavigationIndex('next', 2, arrayLength)).toBe(3);
315
+ expect(calculateNavigationIndex('next', 4, arrayLength)).toBe(0); // wraps to start
316
+ });
317
+ it('should handle first navigation', () => {
318
+ expect(calculateNavigationIndex('first', 3, arrayLength)).toBe(0);
319
+ });
320
+ it('should handle last navigation', () => {
321
+ expect(calculateNavigationIndex('last', 1, arrayLength)).toBe(4);
322
+ });
323
+ it('should handle escape navigation', () => {
324
+ expect(calculateNavigationIndex('escape', 2, arrayLength)).toBe(-1);
325
+ });
326
+ it('should return -1 for empty array', () => {
327
+ expect(calculateNavigationIndex('next', 0, 0)).toBe(-1);
328
+ expect(calculateNavigationIndex('previous', 0, 0)).toBe(-1);
329
+ });
330
+ it('should handle single item array', () => {
331
+ expect(calculateNavigationIndex('next', 0, 1)).toBe(0);
332
+ expect(calculateNavigationIndex('previous', 0, 1)).toBe(0);
333
+ expect(calculateNavigationIndex('first', 0, 1)).toBe(0);
334
+ expect(calculateNavigationIndex('last', 0, 1)).toBe(0);
335
+ });
336
+ });
337
+ describe('getNavigationStateUpdate', () => {
338
+ const mockAlerts = [
339
+ { id: 1, name: 'Alert 1' },
340
+ { id: 2, name: 'Alert 2' },
341
+ { id: 3, name: 'Alert 3' },
342
+ ];
343
+ it('should return correct state for next navigation', () => {
344
+ const result = getNavigationStateUpdate('next', 0, mockAlerts);
345
+ expect(result.newIndex).toBe(1);
346
+ expect(result.selectedAlert).toEqual(mockAlerts[1]);
347
+ expect(result.shouldActivateKeyboard).toBe(true);
348
+ });
349
+ it('should return correct state for previous navigation', () => {
350
+ const result = getNavigationStateUpdate('previous', 2, mockAlerts);
351
+ expect(result.newIndex).toBe(1);
352
+ expect(result.selectedAlert).toEqual(mockAlerts[1]);
353
+ expect(result.shouldActivateKeyboard).toBe(true);
354
+ });
355
+ it('should return correct state for first navigation', () => {
356
+ const result = getNavigationStateUpdate('first', 2, mockAlerts);
357
+ expect(result.newIndex).toBe(0);
358
+ expect(result.selectedAlert).toEqual(mockAlerts[0]);
359
+ expect(result.shouldActivateKeyboard).toBe(true);
360
+ });
361
+ it('should return correct state for last navigation', () => {
362
+ const result = getNavigationStateUpdate('last', 0, mockAlerts);
363
+ expect(result.newIndex).toBe(2);
364
+ expect(result.selectedAlert).toEqual(mockAlerts[2]);
365
+ expect(result.shouldActivateKeyboard).toBe(true);
366
+ });
367
+ it('should return correct state for escape navigation', () => {
368
+ const result = getNavigationStateUpdate('escape', 1, mockAlerts);
369
+ expect(result.newIndex).toBe(-1);
370
+ expect(result.selectedAlert).toBe(null);
371
+ expect(result.shouldActivateKeyboard).toBe(false);
372
+ });
373
+ it('should handle empty array', () => {
374
+ const result = getNavigationStateUpdate('next', 0, []);
375
+ expect(result.newIndex).toBe(-1);
376
+ expect(result.selectedAlert).toBe(null);
377
+ expect(result.shouldActivateKeyboard).toBe(true);
378
+ });
379
+ it('should handle wrapping navigation', () => {
380
+ // Next from last index should wrap to first
381
+ const nextResult = getNavigationStateUpdate('next', 2, mockAlerts);
382
+ expect(nextResult.newIndex).toBe(0);
383
+ expect(nextResult.selectedAlert).toEqual(mockAlerts[0]);
384
+ // Previous from first index should wrap to last
385
+ const prevResult = getNavigationStateUpdate('previous', 0, mockAlerts);
386
+ expect(prevResult.newIndex).toBe(2);
387
+ expect(prevResult.selectedAlert).toEqual(mockAlerts[2]);
388
+ });
389
+ });
390
+ });
391
+ });
@@ -0,0 +1,18 @@
1
+ export interface Alert {
2
+ description: string;
3
+ startsAt: string;
4
+ endsAt: string;
5
+ severity: 'warning' | 'critical' | 'unavailable';
6
+ }
7
+ export declare const useHealthBarData: (alerts: Alert[], startTimestamp: number, endTimestamp: number, id: string) => {
8
+ chartData: {
9
+ range: number[];
10
+ }[];
11
+ alertsMap: Record<string, Alert>;
12
+ alertKeys: {
13
+ warningKeys: string[];
14
+ criticalKeys: string[];
15
+ unavailableKeys: string[];
16
+ };
17
+ };
18
+ //# sourceMappingURL=useHealthBarData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHealthBarData.d.ts","sourceRoot":"","sources":["../../../src/lib/components/globalhealthbar/useHealthBarData.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,KAAK;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,SAAS,GAAG,UAAU,GAAG,aAAa,CAAC;CAClD;AAED,eAAO,MAAM,gBAAgB,WACnB,KAAK,EAAE,kBACC,MAAM,gBACR,MAAM,MAChB,MAAM;;;;;;;;;;CA4DX,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { useMemo } from 'react';
2
+ export const useHealthBarData = (alerts, startTimestamp, endTimestamp, id) => {
3
+ // Filter alerts to only include alerts that are within the start and end timestamp
4
+ const filteredAlerts = useMemo(() => alerts.filter((alert) => (new Date(alert.endsAt).getTime() >= startTimestamp &&
5
+ new Date(alert.startsAt).getTime() <= endTimestamp) ||
6
+ (new Date(alert.startsAt).getTime() <= endTimestamp &&
7
+ new Date(alert.endsAt).getTime() >= startTimestamp)), [alerts, startTimestamp, endTimestamp]);
8
+ // Create chart data and alerts map separately
9
+ const { chartData, alertsMap, alertKeys } = useMemo(() => {
10
+ const alertBars = {};
11
+ const alertsMapData = {};
12
+ filteredAlerts.forEach((alert, index) => {
13
+ // Use alert index with severity to create unique keys for bars dataKey
14
+ // Bars format is: dataKey: [startTimestamp, endTimestamp]
15
+ const uniqueKey = `${alert.severity}_${index}`;
16
+ alertBars[uniqueKey] = [
17
+ new Date(alert.startsAt).getTime(),
18
+ new Date(alert.endsAt).getTime(),
19
+ ];
20
+ // Store alert data separately for tooltip access
21
+ alertsMapData[uniqueKey] = {
22
+ ...alert,
23
+ };
24
+ });
25
+ // Chart data - ready for BarChart (as array)
26
+ const chartDataArray = [
27
+ {
28
+ range: [startTimestamp, endTimestamp],
29
+ ...alertBars,
30
+ },
31
+ ];
32
+ // Alert keys for bar rendering
33
+ const allKeys = Object.keys(alertBars);
34
+ const alertKeysData = {
35
+ warningKeys: allKeys.filter((key) => key.startsWith('warning')),
36
+ criticalKeys: allKeys.filter((key) => key.startsWith('critical')),
37
+ unavailableKeys: allKeys.filter((key) => key.startsWith('unavailable')),
38
+ };
39
+ return {
40
+ chartData: chartDataArray,
41
+ alertsMap: alertsMapData,
42
+ alertKeys: alertKeysData,
43
+ };
44
+ }, [filteredAlerts, startTimestamp, endTimestamp]);
45
+ return { chartData, alertsMap, alertKeys };
46
+ };
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=useHealthBarData.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useHealthBarData.spec.d.ts","sourceRoot":"","sources":["../../../src/lib/components/globalhealthbar/useHealthBarData.spec.tsx"],"names":[],"mappings":""}