@scality/core-ui 0.193.0 → 0.195.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 (155) hide show
  1. package/dist/components/UnsuccessfulResult.component.d.ts.map +1 -1
  2. package/dist/components/accordion/Accordion.component.d.ts.map +1 -1
  3. package/dist/components/banner/Banner.component.d.ts +6 -1
  4. package/dist/components/banner/Banner.component.d.ts.map +1 -1
  5. package/dist/components/banner/Banner.component.js +30 -9
  6. package/dist/components/breadcrumb/Breadcrumb.component.d.ts.map +1 -1
  7. package/dist/components/buttonv2/CopyButton.component.d.ts.map +1 -1
  8. package/dist/components/charts/MetricsTimeSpanProvider.d.ts.map +1 -1
  9. package/dist/components/charts/barchart/Barchart.d.ts.map +1 -1
  10. package/dist/components/charts/barchart/Barchart.js +29 -19
  11. package/dist/components/charts/barchart/Barchart.utils.d.ts.map +1 -1
  12. package/dist/components/charts/barchart/BarchartTooltip.d.ts.map +1 -1
  13. package/dist/components/charts/common/ChartTooltip.d.ts.map +1 -1
  14. package/dist/components/charts/common/SharedComponents.d.ts +6 -6
  15. package/dist/components/charts/common/SharedComponents.d.ts.map +1 -1
  16. package/dist/components/charts/common/SharedComponents.js +7 -3
  17. package/dist/components/charts/common/chartUtils.d.ts +7 -2
  18. package/dist/components/charts/common/chartUtils.d.ts.map +1 -1
  19. package/dist/components/charts/common/chartUtils.js +55 -20
  20. package/dist/components/charts/globalhealthbar/GlobalHealthBar.hooks.d.ts.map +1 -1
  21. package/dist/components/charts/globalhealthbar/GlobalHealthBar.utils.d.ts +3 -1
  22. package/dist/components/charts/globalhealthbar/GlobalHealthBar.utils.d.ts.map +1 -1
  23. package/dist/components/charts/globalhealthbar/GlobalHealthBarTooltip.d.ts.map +1 -1
  24. package/dist/components/charts/globalhealthbar/HealthBarXAxis.d.ts.map +1 -1
  25. package/dist/components/charts/index.d.ts +1 -1
  26. package/dist/components/charts/index.d.ts.map +1 -1
  27. package/dist/components/charts/legend/ChartLegend.d.ts.map +1 -1
  28. package/dist/components/charts/legend/ChartLegendWrapper.d.ts.map +1 -1
  29. package/dist/components/charts/linetimeseries/LineTimeSerieChart.d.ts +12 -47
  30. package/dist/components/charts/linetimeseries/LineTimeSerieChart.d.ts.map +1 -1
  31. package/dist/components/charts/linetimeseries/LineTimeSerieChart.js +46 -220
  32. package/dist/components/charts/linetimeseries/LineTimeSerieChart.types.d.ts +77 -0
  33. package/dist/components/charts/linetimeseries/LineTimeSerieChart.types.d.ts.map +1 -0
  34. package/dist/components/charts/linetimeseries/LineTimeSerieChart.types.js +6 -0
  35. package/dist/components/charts/linetimeseries/LineTimeSerieChart.utils.d.ts.map +1 -1
  36. package/dist/components/charts/linetimeseries/LineTimeSerieChartTooltip.d.ts +18 -0
  37. package/dist/components/charts/linetimeseries/LineTimeSerieChartTooltip.d.ts.map +1 -0
  38. package/dist/components/charts/linetimeseries/LineTimeSerieChartTooltip.js +65 -0
  39. package/dist/components/charts/linetimeseries/useChartData.d.ts +44 -0
  40. package/dist/components/charts/linetimeseries/useChartData.d.ts.map +1 -0
  41. package/dist/components/charts/linetimeseries/useChartData.js +207 -0
  42. package/dist/components/charts/linetimeseries/useChartHover.d.ts +15 -0
  43. package/dist/components/charts/linetimeseries/useChartHover.d.ts.map +1 -0
  44. package/dist/components/charts/linetimeseries/useChartHover.js +29 -0
  45. package/dist/components/checkbox/Checkbox.component.d.ts.map +1 -1
  46. package/dist/components/checkbox/Checkbox.component.js +15 -7
  47. package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
  48. package/dist/components/constrainedtext/Constrainedtext.component.js +3 -2
  49. package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts.map +1 -1
  50. package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
  51. package/dist/components/dropdown/Dropdown.component.d.ts.map +1 -1
  52. package/dist/components/dropdown/Dropdown.component.js +3 -0
  53. package/dist/components/error-pages/ErrorPage401.component.js +1 -1
  54. package/dist/components/error-pages/ErrorPage404.component.js +1 -1
  55. package/dist/components/error-pages/ErrorPage500.component.js +1 -1
  56. package/dist/components/form/Form.component.d.ts.map +1 -1
  57. package/dist/components/form/Form.component.js +3 -3
  58. package/dist/components/icon/CustomsIcons.d.ts +10 -0
  59. package/dist/components/icon/CustomsIcons.d.ts.map +1 -1
  60. package/dist/components/icon/CustomsIcons.js +8 -0
  61. package/dist/components/icon/Icon.component.d.ts +2 -131
  62. package/dist/components/icon/Icon.component.d.ts.map +1 -1
  63. package/dist/components/icon/Icon.component.js +10 -133
  64. package/dist/components/icon/iconTable.d.ts +138 -0
  65. package/dist/components/icon/iconTable.d.ts.map +1 -0
  66. package/dist/components/icon/iconTable.js +137 -0
  67. package/dist/components/iconhelper/IconHelper.d.ts.map +1 -1
  68. package/dist/components/infomessage/InfoMessage.component.d.ts.map +1 -1
  69. package/dist/components/infomessage/InfoMessage.component.js +1 -1
  70. package/dist/components/infomessage/InfoMessageUtils.d.ts.map +1 -1
  71. package/dist/components/inlineinput/InlineInput.d.ts.map +1 -1
  72. package/dist/components/inputlist/InputButtons.d.ts.map +1 -1
  73. package/dist/components/inputlist/InputList.component.d.ts +2 -0
  74. package/dist/components/inputlist/InputList.component.d.ts.map +1 -1
  75. package/dist/components/inputlist/InputList.component.js +2 -2
  76. package/dist/components/inputv2/inputv2.d.ts +2 -0
  77. package/dist/components/inputv2/inputv2.d.ts.map +1 -1
  78. package/dist/components/inputv2/inputv2.js +6 -2
  79. package/dist/components/layout/v2/panels.d.ts.map +1 -1
  80. package/dist/components/modal/Modal.component.d.ts.map +1 -1
  81. package/dist/components/searchinput/SearchInput.component.d.ts.map +1 -1
  82. package/dist/components/searchinput/SearchInput.component.js +1 -1
  83. package/dist/components/statusicon/StatusIcon.component.d.ts.map +1 -1
  84. package/dist/components/tablev2/MultiSelectableContent.d.ts.map +1 -1
  85. package/dist/components/tablev2/Search.d.ts.map +1 -1
  86. package/dist/components/tablev2/TableCommon.d.ts.map +1 -1
  87. package/dist/components/tablev2/TableUtils.d.ts.map +1 -1
  88. package/dist/components/tablev2/Tablestyle.d.ts.map +1 -1
  89. package/dist/components/tablev2/Tablestyle.js +2 -3
  90. package/dist/components/tablev2/Tablev2.component.d.ts.map +1 -1
  91. package/dist/components/tabsv2/useScrollingTabs.d.ts.map +1 -1
  92. package/dist/components/text/Text.component.d.ts +9 -6
  93. package/dist/components/text/Text.component.d.ts.map +1 -1
  94. package/dist/components/text/Text.component.js +5 -0
  95. package/dist/components/toast/Toast.component.d.ts.map +1 -1
  96. package/dist/components/toast/useMutationsHandler.d.ts.map +1 -1
  97. package/dist/components/tooltip/Tooltip.component.js +1 -1
  98. package/dist/index.d.ts +4 -2
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +1 -0
  101. package/dist/next.d.ts +3 -3
  102. package/dist/next.d.ts.map +1 -1
  103. package/dist/organisms/attachments/AttachmentTable.d.ts.map +1 -1
  104. package/dist/spacing.d.ts.map +1 -1
  105. package/dist/utils.d.ts +16 -0
  106. package/dist/utils.d.ts.map +1 -1
  107. package/dist/utils.js +27 -0
  108. package/jest.config.js +6 -1
  109. package/package.json +7 -7
  110. package/src/lib/components/banner/Banner.component.test.tsx +58 -0
  111. package/src/lib/components/banner/Banner.component.tsx +57 -10
  112. package/src/lib/components/charts/barchart/Barchart.test.tsx +3 -1
  113. package/src/lib/components/charts/barchart/Barchart.tsx +123 -106
  114. package/src/lib/components/charts/common/SharedComponents.tsx +15 -11
  115. package/src/lib/components/charts/common/chartUtils.test.ts +27 -12
  116. package/src/lib/components/charts/common/chartUtils.ts +67 -23
  117. package/src/lib/components/charts/index.ts +1 -1
  118. package/src/lib/components/charts/linetimeseries/LineTimeSerieChart.tsx +136 -516
  119. package/src/lib/components/charts/linetimeseries/LineTimeSerieChart.types.ts +93 -0
  120. package/src/lib/components/charts/linetimeseries/LineTimeSerieChartTooltip.tsx +137 -0
  121. package/src/lib/components/charts/linetimeseries/useChartData.ts +322 -0
  122. package/src/lib/components/charts/linetimeseries/useChartHover.ts +35 -0
  123. package/src/lib/components/checkbox/Checkbox.component.tsx +19 -20
  124. package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +3 -2
  125. package/src/lib/components/dropdown/Dropdown.component.tsx +3 -0
  126. package/src/lib/components/error-pages/ErrorPage401.component.tsx +1 -1
  127. package/src/lib/components/error-pages/ErrorPage404.component.tsx +1 -1
  128. package/src/lib/components/error-pages/ErrorPage500.component.tsx +1 -1
  129. package/src/lib/components/form/Form.component.tsx +5 -19
  130. package/src/lib/components/icon/CustomsIcons.tsx +36 -0
  131. package/src/lib/components/icon/Icon.component.tsx +17 -137
  132. package/src/lib/components/icon/iconTable.ts +137 -0
  133. package/src/lib/components/iconhelper/IconHelper.test.tsx +2 -2
  134. package/src/lib/components/infomessage/InfoMessage.component.tsx +1 -1
  135. package/src/lib/components/inputlist/InputList.component.tsx +4 -2
  136. package/src/lib/components/inputv2/inputv2.tsx +11 -5
  137. package/src/lib/components/searchinput/SearchInput.component.tsx +1 -0
  138. package/src/lib/components/searchinput/SearchInput.test.tsx +6 -6
  139. package/src/lib/components/tablev2/Tablestyle.tsx +2 -4
  140. package/src/lib/components/text/Text.component.tsx +18 -10
  141. package/src/lib/components/tooltip/Tooltip.component.tsx +1 -1
  142. package/src/lib/index.ts +3 -2
  143. package/src/lib/next.ts +3 -3
  144. package/src/lib/utils.ts +42 -0
  145. package/stories/GlobalHealthBar/globalhealthbar.stories.tsx +1 -1
  146. package/stories/banner.stories.tsx +37 -5
  147. package/stories/inputlist.stories.tsx +18 -6
  148. package/stories/linetimeseriechart.stories.tsx +325 -6
  149. package/tsconfig.json +1 -1
  150. package/dist/components/date/FormattedDateTime.spec.d.ts +0 -2
  151. package/dist/components/date/FormattedDateTime.spec.d.ts.map +0 -1
  152. package/dist/components/date/FormattedDateTime.spec.js +0 -161
  153. package/dist/components/date/dateDiffer.spec.d.ts +0 -2
  154. package/dist/components/date/dateDiffer.spec.d.ts.map +0 -1
  155. package/dist/components/date/dateDiffer.spec.js +0 -6
@@ -0,0 +1,93 @@
1
+ import { TooltipContentProps } from 'recharts';
2
+
3
+ export type Serie = {
4
+ /** The name of the resource */
5
+ resource: string;
6
+ /** The original data format from prometheus, extend the value to include number type */
7
+ data: [number, number | string | null][];
8
+ /** Function to generate the tooltip label - mandatory for tooltip display */
9
+ getTooltipLabel: (metricPrefix?: string, resource?: string) => string;
10
+ /** The name of the metric prefix (e.g., read, write, in, out) */
11
+ metricPrefix?: string;
12
+ /** Whether the line should be dashed */
13
+ isLineDashed?: boolean;
14
+ };
15
+
16
+ export type NonSymmetricalChartSerie = {
17
+ yAxisType?: 'default' | 'percentage';
18
+ series: Serie[] | undefined;
19
+ };
20
+
21
+ /**
22
+ * The symmetrical chart props are used to display two series on the same chart,
23
+ * such as in/out, write/read
24
+ */
25
+ export type SymmetricalChartSerie = {
26
+ yAxisType: 'symmetrical';
27
+ series:
28
+ | {
29
+ above: Serie[] | undefined;
30
+ below: Serie[] | undefined;
31
+ }
32
+ | undefined;
33
+ };
34
+
35
+ export type LineChartProps = (
36
+ | NonSymmetricalChartSerie
37
+ | SymmetricalChartSerie
38
+ ) & {
39
+ /** The title of the chart */
40
+ title: string;
41
+ /** The height of the chart in pixels */
42
+ height: number;
43
+ /** Starting timestamp in seconds */
44
+ startingTimeStamp: number;
45
+ /** Interval between data points in seconds */
46
+ interval: number;
47
+ /** Total duration of the chart in seconds */
48
+ duration: number;
49
+ /** Unit range configuration for automatic unit scaling */
50
+ unitRange?: {
51
+ threshold: number;
52
+ label: string;
53
+ }[];
54
+ /** Sync ID for synchronizing multiple charts */
55
+ syncId?: string;
56
+ /** Whether the chart is in loading state */
57
+ isLoading?: boolean;
58
+ /** Y-axis title label */
59
+ yAxisTitle?: string;
60
+ /** Help text displayed as a tooltip icon */
61
+ helpText?: string;
62
+ /** Custom tooltip renderer */
63
+ renderTooltip?: (
64
+ tooltipProps: TooltipContentProps<number, string>,
65
+ unitLabel?: string,
66
+ duration?: number,
67
+ ) => React.ReactNode;
68
+ };
69
+
70
+ export type LineTimeSerieChartTooltipProps = {
71
+ tooltipProps: TooltipContentProps<number, string>;
72
+ unitLabel?: string;
73
+ duration: number;
74
+ renderTooltip?: (
75
+ tooltipProps: TooltipContentProps<number, string>,
76
+ unitLabel?: string,
77
+ duration?: number,
78
+ ) => React.ReactNode;
79
+ isSymmetrical?: boolean;
80
+ belowSeriesLabels?: Set<string>;
81
+ chartContainerRef: React.RefObject<HTMLDivElement>;
82
+ /** The unique ID of this chart instance */
83
+ chartId: string;
84
+ };
85
+
86
+ /**
87
+ * Type guard to check if series is symmetrical (has above/below structure)
88
+ */
89
+ export const isSymmetricalSeries = (
90
+ series: Serie[] | { above: Serie[] | undefined; below: Serie[] | undefined },
91
+ ): series is { above: Serie[]; below: Serie[] } => {
92
+ return 'above' in series && 'below' in series;
93
+ };
@@ -0,0 +1,137 @@
1
+ import React from 'react';
2
+ import { LegendShape } from '../legend/ChartLegend';
3
+ import {
4
+ ChartTooltipHeader,
5
+ ChartTooltipItem,
6
+ ChartTooltipItemsContainer,
7
+ ChartTooltipPortal,
8
+ ChartTooltipSeparator,
9
+ TooltipHeader,
10
+ } from '../common/ChartTooltip';
11
+ import { LineTimeSerieChartTooltipProps } from './LineTimeSerieChart.types';
12
+ import { getCurrentlyHoveredChartId } from './useChartHover';
13
+ import { formatISONumber } from '../../../utils';
14
+
15
+ /**
16
+ * Formats a numeric value for tooltip display
17
+ * - Non-finite values (NaN, null, undefined) → "-"
18
+ * - Zero → "0" with unit
19
+ * - Large values (>= 1000) → compact notation (1k, 1M)
20
+ * - Normal values (1-999) → up to 2 decimal places
21
+ * - Small values (0.01-0.99) → 2 decimal places
22
+ * - Very small values (< 0.01) → scientific notation (e.g., 4.7e-5)
23
+ */
24
+ export const formatTooltipValue = (
25
+ value: number,
26
+ unitLabel?: string,
27
+ ): string => {
28
+ if (!Number.isFinite(value)) return '-';
29
+
30
+ const formatted = formatISONumber(value, { fixedDecimals: true, compact: true });
31
+ return `${formatted}${unitLabel ? ` ${unitLabel}` : ''}`;
32
+ };
33
+
34
+ /**
35
+ * Custom tooltip component for LineTimeSerieChart
36
+ * Handles sorting, separator placement for symmetrical charts, and value formatting
37
+ */
38
+ export const LineTimeSerieChartTooltip: React.FC<
39
+ LineTimeSerieChartTooltipProps
40
+ > = ({
41
+ unitLabel,
42
+ duration,
43
+ tooltipProps,
44
+ renderTooltip,
45
+ isSymmetrical,
46
+ belowSeriesLabels,
47
+ chartContainerRef,
48
+ chartId,
49
+ }) => {
50
+ const { active, payload, label, coordinate } = tooltipProps;
51
+
52
+ // Check at render time if this chart is the currently hovered one
53
+ // Using only the module-level variable avoids race conditions with React state updates
54
+ const isActiveChart = getCurrentlyHoveredChartId() === chartId;
55
+
56
+ if (!active || !payload || !payload.length || !label || !isActiveChart)
57
+ return null;
58
+
59
+ const tooltipContent = renderTooltip ? (
60
+ renderTooltip(tooltipProps, unitLabel, duration)
61
+ ) : (
62
+ <>
63
+ <ChartTooltipHeader>
64
+ <TooltipHeader duration={duration} value={label} />
65
+ </ChartTooltipHeader>
66
+ <ChartTooltipItemsContainer>
67
+ {(() => {
68
+ // Sort payload: above series first (descending), then below series (ascending by absolute value)
69
+ const sortedPayload = [...payload].sort((a, b) => {
70
+ const aIsBelow = belowSeriesLabels?.has(a.name) ?? false;
71
+ const bIsBelow = belowSeriesLabels?.has(b.name) ?? false;
72
+
73
+ // Above series come before below series
74
+ if (aIsBelow !== bIsBelow) {
75
+ return aIsBelow ? 1 : -1;
76
+ }
77
+
78
+ // Within the same group:
79
+ // - Above series: higher values first (descending)
80
+ // - Below series: higher absolute values last (ascending)
81
+ if (aIsBelow) {
82
+ return Math.abs(a.value) - Math.abs(b.value);
83
+ }
84
+ return Math.abs(b.value) - Math.abs(a.value);
85
+ });
86
+
87
+ // Find the transition point between above and below series
88
+ const separatorIndex = sortedPayload.findIndex((entry) =>
89
+ belowSeriesLabels?.has(entry.name),
90
+ );
91
+ const hasBothAboveAndBelow =
92
+ isSymmetrical &&
93
+ belowSeriesLabels &&
94
+ belowSeriesLabels.size > 0 &&
95
+ separatorIndex > 0 &&
96
+ separatorIndex < sortedPayload.length;
97
+
98
+ return sortedPayload.map((entry, index) => {
99
+ const legendIcon = (
100
+ <LegendShape
101
+ color={entry.color}
102
+ shape="line"
103
+ chartColors={{ [entry.color]: entry.color }}
104
+ />
105
+ );
106
+
107
+ const formattedValue = formatTooltipValue(entry.value, unitLabel);
108
+
109
+ return (
110
+ <React.Fragment key={index}>
111
+ {/* Add separator between above and below series for symmetrical charts */}
112
+ {hasBothAboveAndBelow && index === separatorIndex && (
113
+ <ChartTooltipSeparator />
114
+ )}
115
+ <ChartTooltipItem
116
+ label={entry.name}
117
+ value={formattedValue}
118
+ legendIcon={legendIcon}
119
+ />
120
+ </React.Fragment>
121
+ );
122
+ });
123
+ })()}
124
+ </ChartTooltipItemsContainer>
125
+ </>
126
+ );
127
+
128
+ return (
129
+ <ChartTooltipPortal
130
+ coordinate={coordinate}
131
+ chartContainerRef={chartContainerRef}
132
+ isVisible={active && isActiveChart}
133
+ >
134
+ {tooltipContent}
135
+ </ChartTooltipPortal>
136
+ );
137
+ };
@@ -0,0 +1,322 @@
1
+ import { useMemo } from 'react';
2
+ import { useChartLegend } from '../legend/ChartLegendWrapper';
3
+ import {
4
+ addMissingDataPoint,
5
+ normalizeChartDataWithUnits,
6
+ } from '../common/chartUtils';
7
+ import { Serie, isSymmetricalSeries } from './LineTimeSerieChart.types';
8
+
9
+ type ChartDataInput = {
10
+ series: Serie[] | { above: Serie[] | undefined; below: Serie[] | undefined } | undefined;
11
+ startingTimeStamp: number;
12
+ duration: number;
13
+ interval: number;
14
+ yAxisType: 'default' | 'percentage' | 'symmetrical';
15
+ unitRange?: { threshold: number; label: string }[];
16
+ };
17
+
18
+ export type LineToRender = {
19
+ key: string;
20
+ dataKey: string;
21
+ stroke: string;
22
+ strokeDasharray?: string;
23
+ };
24
+
25
+ type ChartDataOutput = {
26
+ /** Processed data ready for Recharts */
27
+ rechartsData: Record<string, number | null>[];
28
+ /** Maximum value for Y-axis domain */
29
+ topDomain: number;
30
+ /** Value used for tick calculation */
31
+ topValue: number;
32
+ /** Unit label (e.g., "KiB/s", "%") */
33
+ unitLabel: string | undefined;
34
+ /** X-axis tick positions */
35
+ xAxisTicks: number[];
36
+ /** Line configurations ready for rendering */
37
+ linesToRender: LineToRender[];
38
+ /** Set of labels belonging to "below" series (for symmetrical charts) */
39
+ belowSeriesLabels: Set<string> | undefined;
40
+ };
41
+
42
+ /**
43
+ * Hook that processes chart data for LineTimeSerieChart.
44
+ * Handles data normalization, unit scaling, empty data handling, and series grouping.
45
+ */
46
+ export function useChartData({
47
+ series,
48
+ startingTimeStamp,
49
+ duration,
50
+ interval,
51
+ yAxisType,
52
+ unitRange,
53
+ }: ChartDataInput): ChartDataOutput {
54
+ const { getColor, selectedResources } = useChartLegend();
55
+
56
+ /**
57
+ * Determines if the series data is empty.
58
+ * Used for conditional rendering and empty state handling.
59
+ */
60
+ const isSeriesEmpty = useMemo(() => {
61
+ return (
62
+ !series ||
63
+ (Array.isArray(series) && series.length === 0) ||
64
+ (isSymmetricalSeries(series) &&
65
+ (!series.above || series.above.length === 0) &&
66
+ (!series.below || series.below.length === 0))
67
+ );
68
+ }, [series]);
69
+
70
+ /**
71
+ * Processes raw series data into Recharts-compatible format.
72
+ * - Fills missing data points based on time interval
73
+ * - Converts timestamps to milliseconds
74
+ * - For symmetrical charts, negates "below" series values
75
+ * - For empty data, generates placeholder data with NaN values
76
+ */
77
+ const chartData = useMemo(() => {
78
+ if (isSeriesEmpty) {
79
+ // Generate timestamps for the time range with NaN values for each series from legend
80
+ const emptyDataPoints = addMissingDataPoint(
81
+ [],
82
+ startingTimeStamp,
83
+ duration,
84
+ interval,
85
+ );
86
+ return emptyDataPoints.map(([timestamp]) => {
87
+ const dataPoint: Record<string, number> = {
88
+ timestamp: timestamp * 1000,
89
+ };
90
+ selectedResources.forEach((resource) => {
91
+ dataPoint[resource] = NaN;
92
+ });
93
+ return dataPoint;
94
+ });
95
+ }
96
+
97
+ // Add missing data points to each series
98
+ const normalizedSeries =
99
+ yAxisType === 'symmetrical' && isSymmetricalSeries(series!)
100
+ ? {
101
+ above: series!.above
102
+ ? series!.above.map((line) => ({
103
+ ...line,
104
+ data: addMissingDataPoint(
105
+ line.data,
106
+ startingTimeStamp,
107
+ duration,
108
+ interval,
109
+ ),
110
+ }))
111
+ : [],
112
+ below: series!.below
113
+ ? series!.below.map((line) => ({
114
+ ...line,
115
+ data: addMissingDataPoint(
116
+ line.data,
117
+ startingTimeStamp,
118
+ duration,
119
+ interval,
120
+ ).map(
121
+ ([timestamp, value]) =>
122
+ [
123
+ timestamp,
124
+ value === null ? null : `-${Number(value)}`,
125
+ ] as [number, string | null],
126
+ ),
127
+ }))
128
+ : [],
129
+ }
130
+ : (series as Serie[]).map((line) => ({
131
+ ...line,
132
+ data: addMissingDataPoint(
133
+ line.data,
134
+ startingTimeStamp,
135
+ duration,
136
+ interval,
137
+ ),
138
+ }));
139
+
140
+ // Convert to Recharts format (array of objects with timestamp and values)
141
+ const dataPointsByTime: Record<
142
+ number,
143
+ { timestamp: number } & Record<string, number | null>
144
+ > = {};
145
+
146
+ const seriesToProcess =
147
+ yAxisType === 'symmetrical' && isSymmetricalSeries(normalizedSeries)
148
+ ? [...normalizedSeries.above, ...normalizedSeries.below]
149
+ : (normalizedSeries as Serie[]);
150
+
151
+ seriesToProcess.forEach((serie) => {
152
+ const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
153
+
154
+ serie.data.forEach((point) => {
155
+ const timestamp =
156
+ typeof point[0] === 'number' ? point[0] * 1000 : Number(point[0]);
157
+ const value = point[1];
158
+
159
+ if (!dataPointsByTime[timestamp]) {
160
+ dataPointsByTime[timestamp] = { timestamp };
161
+ }
162
+ dataPointsByTime[timestamp][label] =
163
+ typeof value === 'string' ? Number(value) : value;
164
+ });
165
+ });
166
+
167
+ return Object.values(dataPointsByTime).sort(
168
+ (a, b) => (a.timestamp as number) - (b.timestamp as number),
169
+ );
170
+ }, [series, startingTimeStamp, duration, interval, yAxisType, selectedResources, isSeriesEmpty]);
171
+
172
+ /**
173
+ * Calculates evenly spaced X-axis tick positions.
174
+ * Adds padding to avoid labels at the very edges (10% on each side).
175
+ */
176
+ const xAxisTicks = useMemo(() => {
177
+ if (!chartData || chartData.length === 0) return [];
178
+
179
+ const timestamps: number[] = chartData
180
+ .map((d) => d.timestamp)
181
+ .filter((t): t is number => t !== null && t !== undefined);
182
+
183
+ if (timestamps.length === 0) return [];
184
+
185
+ const minTimestamp = Math.min(...timestamps);
186
+ const maxTimestamp = Math.max(...timestamps);
187
+ const timeRange = maxTimestamp - minTimestamp;
188
+
189
+ const padding = timeRange * 0.1;
190
+ const paddedStart = minTimestamp + padding;
191
+ const paddedEnd = maxTimestamp - padding;
192
+ const paddedRange = paddedEnd - paddedStart;
193
+
194
+ const numTicks = 5;
195
+ const tickInterval = paddedRange / (numTicks - 1);
196
+
197
+ return Array.from(
198
+ { length: numTicks },
199
+ (_, index) => paddedStart + index * tickInterval,
200
+ );
201
+ }, [chartData]);
202
+
203
+ /**
204
+ * Normalizes data values and determines unit scaling.
205
+ * - Extracts valid numeric values from chart data
206
+ * - Applies unit range thresholds (e.g., B/s → KiB/s → MiB/s)
207
+ * - Calculates Y-axis domain
208
+ */
209
+ const { topValue, unitLabel, rechartsData, topDomain } = useMemo(() => {
210
+ const values = chartData.flatMap((dataPoint) =>
211
+ Object.entries(dataPoint)
212
+ .filter(([key]) => key !== 'timestamp')
213
+ .map(([_, value]) => {
214
+ if (value === null || value === undefined) return null;
215
+ const num =
216
+ typeof value === 'string'
217
+ ? Number(value)
218
+ : typeof value === 'number'
219
+ ? value
220
+ : null;
221
+ return num !== null && !isNaN(num) ? num : null;
222
+ })
223
+ .filter((value): value is number => value !== null),
224
+ );
225
+
226
+ // Default values for empty charts
227
+ if (values.length === 0 || values.every((value) => value === 0)) {
228
+ return {
229
+ topValue: 1,
230
+ unitLabel: yAxisType === 'percentage' ? '%' : undefined,
231
+ rechartsData: chartData,
232
+ topDomain: 1,
233
+ };
234
+ }
235
+
236
+ const top = Math.abs(Math.max(...values));
237
+ const bottom = Math.abs(Math.min(...values));
238
+ const maxValue = Math.max(top, bottom);
239
+
240
+ const result = normalizeChartDataWithUnits(
241
+ chartData as Record<string, number>[],
242
+ maxValue,
243
+ unitRange,
244
+ 'timestamp',
245
+ );
246
+
247
+ const finalTopDomain =
248
+ yAxisType === 'percentage'
249
+ ? Math.max(result.topDomain, 100)
250
+ : result.topDomain;
251
+
252
+ return {
253
+ topValue:
254
+ yAxisType === 'percentage'
255
+ ? Math.max(result.topValue, 100)
256
+ : result.topValue,
257
+ unitLabel: result.unitLabel ?? (yAxisType === 'percentage' ? '%' : undefined),
258
+ rechartsData: result.rechartsData,
259
+ topDomain: finalTopDomain,
260
+ };
261
+ }, [chartData, yAxisType, unitRange]);
262
+
263
+ /**
264
+ * Computes the line configurations for rendering.
265
+ * Handles both empty series (using legend resources) and populated series.
266
+ */
267
+ const linesToRender = useMemo((): LineToRender[] => {
268
+ if (isSeriesEmpty) {
269
+ // For empty series, create lines for each resource from legend
270
+ return selectedResources.map((resource) => ({
271
+ key: `empty-${resource}`,
272
+ dataKey: resource,
273
+ stroke: getColor(resource) || '',
274
+ }));
275
+ }
276
+
277
+ // For populated series, create lines from actual series data
278
+ const allSeries = isSymmetricalSeries(series!)
279
+ ? [...(series!.above || []), ...(series!.below || [])]
280
+ : (series as Serie[]);
281
+
282
+ return allSeries
283
+ .filter((serie) => selectedResources.includes(serie.resource))
284
+ .map((serie, index) => {
285
+ const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
286
+ return {
287
+ key: `${serie.resource}-${index}`,
288
+ dataKey: label,
289
+ stroke: getColor(serie.resource) || '',
290
+ strokeDasharray: serie.isLineDashed ? '4 4' : undefined,
291
+ };
292
+ });
293
+ }, [series, getColor, selectedResources, isSeriesEmpty]);
294
+
295
+ /**
296
+ * Computes the set of "below" series labels for symmetrical charts.
297
+ * Used to reliably determine separator placement in the tooltip.
298
+ */
299
+ const belowSeriesLabels = useMemo(() => {
300
+ if (yAxisType !== 'symmetrical' || !series || !isSymmetricalSeries(series)) {
301
+ return undefined;
302
+ }
303
+
304
+ const labels = new Set<string>();
305
+ (series.below || []).forEach((serie) => {
306
+ const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
307
+ labels.add(label);
308
+ });
309
+
310
+ return labels;
311
+ }, [series, yAxisType]);
312
+
313
+ return {
314
+ rechartsData,
315
+ topDomain,
316
+ topValue,
317
+ unitLabel,
318
+ xAxisTicks,
319
+ linesToRender,
320
+ belowSeriesLabels,
321
+ };
322
+ }
@@ -0,0 +1,35 @@
1
+ import { useCallback, useId } from 'react';
2
+
3
+ /**
4
+ * Module-level tracker to ensure only one chart tooltip is shown at a time
5
+ * when multiple charts are synced via syncId.
6
+ */
7
+ let currentlyHoveredChartId: string | null = null;
8
+
9
+ /**
10
+ * Gets the currently hovered chart ID.
11
+ * Called at tooltip render time to get the latest value.
12
+ */
13
+ export function getCurrentlyHoveredChartId(): string | null {
14
+ return currentlyHoveredChartId;
15
+ }
16
+
17
+ /**
18
+ * Hook to manage chart hover state for tooltip display.
19
+ * Ensures only one chart shows its tooltip at a time when using syncId.
20
+ */
21
+ export function useChartHover() {
22
+ const chartId = useId();
23
+
24
+ const handleMouseEnter = useCallback(() => {
25
+ currentlyHoveredChartId = chartId;
26
+ }, [chartId]);
27
+
28
+ const handleMouseLeave = useCallback(() => {
29
+ if (currentlyHoveredChartId === chartId) {
30
+ currentlyHoveredChartId = null;
31
+ }
32
+ }, [chartId]);
33
+
34
+ return { handleMouseEnter, handleMouseLeave, chartId };
35
+ }
@@ -5,6 +5,19 @@ import { spacing, Stack } from '../../spacing';
5
5
  import { Text } from '../text/Text.component';
6
6
  import { FocusVisibleStyle } from '../buttonv2/Buttonv2.component';
7
7
 
8
+ const getCheckmarkSvgUrl = (color: string) => {
9
+ const encodedColor = color.replace('#', '%23');
10
+ return `url('data:image/svg+xml,%3Csvg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg"%3E %3Cpath d="M3 6.68646L5.0671 9L9 3" stroke="${encodedColor}" stroke-width="1.5"/%3E %3C/svg%3E')`;
11
+ };
12
+
13
+ const getIndeterminateSvgUrl = (color: string) => {
14
+ const encodedColor = color.replace('#', '%23');
15
+ return `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E %3Cline x1='6' y1='12' x2='20' y2='12' style='stroke:${encodedColor};stroke-width:4'/%3E %3C/svg%3E")`;
16
+ };
17
+
18
+ const CheckboxInput = styled.input`
19
+ transform: scale(1.5);`;
20
+
8
21
  export type Props = {
9
22
  label?: string;
10
23
  value?: string;
@@ -22,7 +35,7 @@ const Checkbox = forwardRef<HTMLInputElement, Props>(
22
35
  className="sc-checkbox"
23
36
  >
24
37
  <Stack>
25
- <input
38
+ <CheckboxInput
26
39
  type="checkbox"
27
40
  checked={checked}
28
41
  disabled={disabled}
@@ -77,8 +90,7 @@ const StyledCheckbox = styled.label<{
77
90
  border: 0;
78
91
  background-color: transparent;
79
92
  background-size: contain;
80
- box-shadow: inset 0 0 0 ${spacing.r1}
81
- ${(props) => props.theme.textSecondary};
93
+ box-shadow: inset 0 0 0 ${spacing.r1} ${(props) => props.theme.textSecondary};
82
94
  }
83
95
 
84
96
  /* Checked */
@@ -89,13 +101,7 @@ const StyledCheckbox = styled.label<{
89
101
 
90
102
  [type='checkbox']:checked::before {
91
103
  box-shadow: none;
92
- background-image: url('data:image/svg+xml,%3Csvg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg"%3E %3Cpath d="M3 6.68646L5.0671 9L9 3" stroke="${(
93
- props,
94
- ) =>
95
- props.theme.textPrimary.replace(
96
- '#',
97
- '%23',
98
- )}" stroke-width="1.5"/%3E %3C/svg%3E');
104
+ background-image: ${(props) => getCheckmarkSvgUrl(props.theme.textPrimary)};
99
105
  background-repeat: no-repeat;
100
106
  background-position: center;
101
107
  }
@@ -103,16 +109,9 @@ const StyledCheckbox = styled.label<{
103
109
  /* Indeterminate */
104
110
 
105
111
  [type='checkbox']:indeterminate::before {
106
- box-shadow: inset 0 0 0 ${spacing.r1}
107
- ${(props) => props.theme.selectedActive};
112
+ box-shadow: inset 0 0 0 ${spacing.r1} ${(props) => props.theme.selectedActive};
108
113
  background-color: ${(props) => props.theme.highlight};
109
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E %3Cline x1='6' y1='12' x2='20' y2='12' style='stroke:${(
110
- props,
111
- ) =>
112
- props.theme.textPrimary.replace(
113
- '#',
114
- '%23',
115
- )};stroke-width:4'/%3E %3C/svg%3E");
114
+ background-image: ${(props) => getIndeterminateSvgUrl(props.theme.textPrimary)};
116
115
  }
117
116
 
118
117
  /* Hover & focus */
@@ -142,4 +141,4 @@ const StyledCheckbox = styled.label<{
142
141
  cursor: not-allowed;
143
142
  background-color: ${(props) => props.theme.textSecondary};
144
143
  }
145
- `;
144
+ `
@@ -31,11 +31,12 @@ const ConstrainedTextContainer = styled.div`
31
31
  -webkit-line-clamp: ${props.lineClamp};
32
32
  -webkit-box-orient: vertical;
33
33
  overflow-wrap: break-word;
34
- word-break: break-all;
34
+ word-break: normal;
35
+ line-height: 1.2;
35
36
  `
36
37
  : `overflow-wrap: break-word;
37
38
  white-space: nowrap;
38
- word-break: break-all;
39
+ word-break: normal;
39
40
  `};
40
41
  `;
41
42
  const BlockTooltip = styled.div`
@@ -79,6 +79,9 @@ const DropdownMenuItemStyled = styled.li`
79
79
  &:active {
80
80
  background-color: ${getThemePropSelector('highlight')};
81
81
  }
82
+ &:last-child {
83
+ border-bottom: 0.3px solid ${getThemePropSelector('border')};
84
+ }
82
85
  `;
83
86
  const Caret = styled.span`
84
87
  margin-left: ${spacing.r16};