@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.
- package/dist/components/UnsuccessfulResult.component.d.ts.map +1 -1
- package/dist/components/accordion/Accordion.component.d.ts.map +1 -1
- package/dist/components/banner/Banner.component.d.ts +6 -1
- package/dist/components/banner/Banner.component.d.ts.map +1 -1
- package/dist/components/banner/Banner.component.js +30 -9
- package/dist/components/breadcrumb/Breadcrumb.component.d.ts.map +1 -1
- package/dist/components/buttonv2/CopyButton.component.d.ts.map +1 -1
- package/dist/components/charts/MetricsTimeSpanProvider.d.ts.map +1 -1
- package/dist/components/charts/barchart/Barchart.d.ts.map +1 -1
- package/dist/components/charts/barchart/Barchart.js +29 -19
- package/dist/components/charts/barchart/Barchart.utils.d.ts.map +1 -1
- package/dist/components/charts/barchart/BarchartTooltip.d.ts.map +1 -1
- package/dist/components/charts/common/ChartTooltip.d.ts.map +1 -1
- package/dist/components/charts/common/SharedComponents.d.ts +6 -6
- package/dist/components/charts/common/SharedComponents.d.ts.map +1 -1
- package/dist/components/charts/common/SharedComponents.js +7 -3
- package/dist/components/charts/common/chartUtils.d.ts +7 -2
- package/dist/components/charts/common/chartUtils.d.ts.map +1 -1
- package/dist/components/charts/common/chartUtils.js +55 -20
- package/dist/components/charts/globalhealthbar/GlobalHealthBar.hooks.d.ts.map +1 -1
- package/dist/components/charts/globalhealthbar/GlobalHealthBar.utils.d.ts +3 -1
- package/dist/components/charts/globalhealthbar/GlobalHealthBar.utils.d.ts.map +1 -1
- package/dist/components/charts/globalhealthbar/GlobalHealthBarTooltip.d.ts.map +1 -1
- package/dist/components/charts/globalhealthbar/HealthBarXAxis.d.ts.map +1 -1
- package/dist/components/charts/index.d.ts +1 -1
- package/dist/components/charts/index.d.ts.map +1 -1
- package/dist/components/charts/legend/ChartLegend.d.ts.map +1 -1
- package/dist/components/charts/legend/ChartLegendWrapper.d.ts.map +1 -1
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.d.ts +12 -47
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.d.ts.map +1 -1
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.js +46 -220
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.types.d.ts +77 -0
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.types.d.ts.map +1 -0
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.types.js +6 -0
- package/dist/components/charts/linetimeseries/LineTimeSerieChart.utils.d.ts.map +1 -1
- package/dist/components/charts/linetimeseries/LineTimeSerieChartTooltip.d.ts +18 -0
- package/dist/components/charts/linetimeseries/LineTimeSerieChartTooltip.d.ts.map +1 -0
- package/dist/components/charts/linetimeseries/LineTimeSerieChartTooltip.js +65 -0
- package/dist/components/charts/linetimeseries/useChartData.d.ts +44 -0
- package/dist/components/charts/linetimeseries/useChartData.d.ts.map +1 -0
- package/dist/components/charts/linetimeseries/useChartData.js +207 -0
- package/dist/components/charts/linetimeseries/useChartHover.d.ts +15 -0
- package/dist/components/charts/linetimeseries/useChartHover.d.ts.map +1 -0
- package/dist/components/charts/linetimeseries/useChartHover.js +29 -0
- package/dist/components/checkbox/Checkbox.component.d.ts.map +1 -1
- package/dist/components/checkbox/Checkbox.component.js +15 -7
- package/dist/components/constrainedtext/Constrainedtext.component.d.ts.map +1 -1
- package/dist/components/constrainedtext/Constrainedtext.component.js +3 -2
- package/dist/components/coreuithemeprovider/CoreUiThemeProvider.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
- package/dist/components/dropdown/Dropdown.component.d.ts.map +1 -1
- package/dist/components/dropdown/Dropdown.component.js +3 -0
- package/dist/components/error-pages/ErrorPage401.component.js +1 -1
- package/dist/components/error-pages/ErrorPage404.component.js +1 -1
- package/dist/components/error-pages/ErrorPage500.component.js +1 -1
- package/dist/components/form/Form.component.d.ts.map +1 -1
- package/dist/components/form/Form.component.js +3 -3
- package/dist/components/icon/CustomsIcons.d.ts +10 -0
- package/dist/components/icon/CustomsIcons.d.ts.map +1 -1
- package/dist/components/icon/CustomsIcons.js +8 -0
- package/dist/components/icon/Icon.component.d.ts +2 -131
- package/dist/components/icon/Icon.component.d.ts.map +1 -1
- package/dist/components/icon/Icon.component.js +10 -133
- package/dist/components/icon/iconTable.d.ts +138 -0
- package/dist/components/icon/iconTable.d.ts.map +1 -0
- package/dist/components/icon/iconTable.js +137 -0
- package/dist/components/iconhelper/IconHelper.d.ts.map +1 -1
- package/dist/components/infomessage/InfoMessage.component.d.ts.map +1 -1
- package/dist/components/infomessage/InfoMessage.component.js +1 -1
- package/dist/components/infomessage/InfoMessageUtils.d.ts.map +1 -1
- package/dist/components/inlineinput/InlineInput.d.ts.map +1 -1
- package/dist/components/inputlist/InputButtons.d.ts.map +1 -1
- package/dist/components/inputlist/InputList.component.d.ts +2 -0
- package/dist/components/inputlist/InputList.component.d.ts.map +1 -1
- package/dist/components/inputlist/InputList.component.js +2 -2
- package/dist/components/inputv2/inputv2.d.ts +2 -0
- package/dist/components/inputv2/inputv2.d.ts.map +1 -1
- package/dist/components/inputv2/inputv2.js +6 -2
- package/dist/components/layout/v2/panels.d.ts.map +1 -1
- package/dist/components/modal/Modal.component.d.ts.map +1 -1
- package/dist/components/searchinput/SearchInput.component.d.ts.map +1 -1
- package/dist/components/searchinput/SearchInput.component.js +1 -1
- package/dist/components/statusicon/StatusIcon.component.d.ts.map +1 -1
- package/dist/components/tablev2/MultiSelectableContent.d.ts.map +1 -1
- package/dist/components/tablev2/Search.d.ts.map +1 -1
- package/dist/components/tablev2/TableCommon.d.ts.map +1 -1
- package/dist/components/tablev2/TableUtils.d.ts.map +1 -1
- package/dist/components/tablev2/Tablestyle.d.ts.map +1 -1
- package/dist/components/tablev2/Tablestyle.js +2 -3
- package/dist/components/tablev2/Tablev2.component.d.ts.map +1 -1
- package/dist/components/tabsv2/useScrollingTabs.d.ts.map +1 -1
- package/dist/components/text/Text.component.d.ts +9 -6
- package/dist/components/text/Text.component.d.ts.map +1 -1
- package/dist/components/text/Text.component.js +5 -0
- package/dist/components/toast/Toast.component.d.ts.map +1 -1
- package/dist/components/toast/useMutationsHandler.d.ts.map +1 -1
- package/dist/components/tooltip/Tooltip.component.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/next.d.ts +3 -3
- package/dist/next.d.ts.map +1 -1
- package/dist/organisms/attachments/AttachmentTable.d.ts.map +1 -1
- package/dist/spacing.d.ts.map +1 -1
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +27 -0
- package/jest.config.js +6 -1
- package/package.json +7 -7
- package/src/lib/components/banner/Banner.component.test.tsx +58 -0
- package/src/lib/components/banner/Banner.component.tsx +57 -10
- package/src/lib/components/charts/barchart/Barchart.test.tsx +3 -1
- package/src/lib/components/charts/barchart/Barchart.tsx +123 -106
- package/src/lib/components/charts/common/SharedComponents.tsx +15 -11
- package/src/lib/components/charts/common/chartUtils.test.ts +27 -12
- package/src/lib/components/charts/common/chartUtils.ts +67 -23
- package/src/lib/components/charts/index.ts +1 -1
- package/src/lib/components/charts/linetimeseries/LineTimeSerieChart.tsx +136 -516
- package/src/lib/components/charts/linetimeseries/LineTimeSerieChart.types.ts +93 -0
- package/src/lib/components/charts/linetimeseries/LineTimeSerieChartTooltip.tsx +137 -0
- package/src/lib/components/charts/linetimeseries/useChartData.ts +322 -0
- package/src/lib/components/charts/linetimeseries/useChartHover.ts +35 -0
- package/src/lib/components/checkbox/Checkbox.component.tsx +19 -20
- package/src/lib/components/constrainedtext/Constrainedtext.component.tsx +3 -2
- package/src/lib/components/dropdown/Dropdown.component.tsx +3 -0
- package/src/lib/components/error-pages/ErrorPage401.component.tsx +1 -1
- package/src/lib/components/error-pages/ErrorPage404.component.tsx +1 -1
- package/src/lib/components/error-pages/ErrorPage500.component.tsx +1 -1
- package/src/lib/components/form/Form.component.tsx +5 -19
- package/src/lib/components/icon/CustomsIcons.tsx +36 -0
- package/src/lib/components/icon/Icon.component.tsx +17 -137
- package/src/lib/components/icon/iconTable.ts +137 -0
- package/src/lib/components/iconhelper/IconHelper.test.tsx +2 -2
- package/src/lib/components/infomessage/InfoMessage.component.tsx +1 -1
- package/src/lib/components/inputlist/InputList.component.tsx +4 -2
- package/src/lib/components/inputv2/inputv2.tsx +11 -5
- package/src/lib/components/searchinput/SearchInput.component.tsx +1 -0
- package/src/lib/components/searchinput/SearchInput.test.tsx +6 -6
- package/src/lib/components/tablev2/Tablestyle.tsx +2 -4
- package/src/lib/components/text/Text.component.tsx +18 -10
- package/src/lib/components/tooltip/Tooltip.component.tsx +1 -1
- package/src/lib/index.ts +3 -2
- package/src/lib/next.ts +3 -3
- package/src/lib/utils.ts +42 -0
- package/stories/GlobalHealthBar/globalhealthbar.stories.tsx +1 -1
- package/stories/banner.stories.tsx +37 -5
- package/stories/inputlist.stories.tsx +18 -6
- package/stories/linetimeseriechart.stories.tsx +325 -6
- package/tsconfig.json +1 -1
- package/dist/components/date/FormattedDateTime.spec.d.ts +0 -2
- package/dist/components/date/FormattedDateTime.spec.d.ts.map +0 -1
- package/dist/components/date/FormattedDateTime.spec.js +0 -161
- package/dist/components/date/dateDiffer.spec.d.ts +0 -2
- package/dist/components/date/dateDiffer.spec.d.ts.map +0 -1
- 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
|
-
<
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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};
|