@scality/core-ui 0.162.0 → 0.164.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/barchartv2/Barchart.component.d.ts +9 -3
- package/dist/components/barchartv2/Barchart.component.d.ts.map +1 -1
- package/dist/components/barchartv2/Barchart.component.js +22 -5
- package/dist/components/barchartv2/utils.d.ts +26 -3
- package/dist/components/barchartv2/utils.d.ts.map +1 -1
- package/dist/components/barchartv2/utils.js +76 -22
- package/dist/components/chartlegend/ChartLegend.d.ts +8 -0
- package/dist/components/chartlegend/ChartLegend.d.ts.map +1 -0
- package/dist/components/chartlegend/ChartLegend.js +65 -0
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts +17 -0
- package/dist/components/chartlegend/ChartLegendWrapper.d.ts.map +1 -0
- package/dist/components/chartlegend/ChartLegendWrapper.js +50 -0
- package/dist/components/date/FormattedDateTime.d.ts +3 -1
- package/dist/components/date/FormattedDateTime.d.ts.map +1 -1
- package/dist/components/date/FormattedDateTime.js +19 -1
- package/dist/components/date/FormattedDateTime.spec.js +12 -0
- package/dist/components/icon/Icon.component.d.ts +5 -5
- package/dist/components/icon/Icon.component.d.ts.map +1 -1
- package/dist/components/icon/Icon.component.js +33 -31
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts +33 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.d.ts.map +1 -0
- package/dist/components/linetimeseriechart/linetimeseriechart.component.js +249 -0
- package/dist/components/selectv2/Selectv2.component.d.ts.map +1 -1
- package/dist/components/selectv2/Selectv2.component.js +11 -6
- package/dist/components/steppers/Stepper.component.d.ts.map +1 -1
- package/dist/components/steppers/Stepper.component.js +9 -8
- package/dist/components/toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/toast/ToastProvider.js +4 -5
- package/dist/components/vegachartv2/SyncedCursorCharts.d.ts.map +1 -1
- package/dist/components/vegachartv2/SyncedCursorCharts.js +3 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/next.d.ts +2 -0
- package/dist/next.d.ts.map +1 -1
- package/dist/next.js +2 -0
- package/dist/style/theme.d.ts +1 -0
- package/dist/style/theme.d.ts.map +1 -1
- package/dist/style/theme.js +28 -0
- package/package.json +2 -2
- package/src/lib/components/accordion/Accordion.test.tsx +8 -16
- package/src/lib/components/barchartv2/Barchart.component.test.tsx +117 -111
- package/src/lib/components/barchartv2/Barchart.component.tsx +54 -7
- package/src/lib/components/barchartv2/utils.test.ts +127 -2
- package/src/lib/components/barchartv2/utils.ts +103 -19
- package/src/lib/components/chartlegend/ChartLegend.tsx +113 -0
- package/src/lib/components/chartlegend/ChartLegendWrapper.tsx +85 -0
- package/src/lib/components/date/FormattedDateTime.spec.tsx +24 -0
- package/src/lib/components/date/FormattedDateTime.tsx +36 -2
- package/src/lib/components/healthselectorv2/HealthSelector.component.test.tsx +10 -10
- package/src/lib/components/icon/Icon.component.tsx +48 -60
- package/src/lib/components/infomessage/InfoMessageUtils.test.tsx +0 -1
- package/src/lib/components/inlineinput/InlineInput.test.tsx +28 -22
- package/src/lib/components/inputlist/InputList.test.tsx +22 -21
- package/src/lib/components/linetemporalchart/ChartUtil.test.ts +5 -4
- package/src/lib/components/linetimeseriechart/linetimeseriechart.component.tsx +502 -0
- package/src/lib/components/searchinput/SearchInput.test.tsx +3 -7
- package/src/lib/components/selectv2/Selectv2.component.tsx +13 -5
- package/src/lib/components/selectv2/selectv2.test.tsx +70 -61
- package/src/lib/components/steppers/Stepper.component.tsx +10 -8
- package/src/lib/components/tablev2/TableSync.test.tsx +8 -12
- package/src/lib/components/tablev2/TableUtils.test.ts +6 -3
- package/src/lib/components/tablev2/Tablev2.test.tsx +38 -40
- package/src/lib/components/toast/ToastProvider.tsx +14 -6
- package/src/lib/components/toggle/Toggle.test.tsx +1 -1
- package/src/lib/components/vegachartv2/SyncedCursorCharts.tsx +5 -7
- package/src/lib/index.ts +1 -0
- package/src/lib/next.ts +2 -0
- package/src/lib/style/theme.ts +29 -0
- package/stories/BarChart/barchart.stories.tsx +387 -129
- package/stories/format.mdx +4 -2
- package/stories/linetimeseriechart.stories.tsx +485 -0
- package/tsconfig.json +0 -1
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Line,
|
|
3
|
+
LineChart,
|
|
4
|
+
ReferenceLine,
|
|
5
|
+
ResponsiveContainer,
|
|
6
|
+
Tooltip,
|
|
7
|
+
XAxis,
|
|
8
|
+
YAxis,
|
|
9
|
+
CartesianGrid,
|
|
10
|
+
} from 'recharts';
|
|
11
|
+
import { useMemo, useRef } from 'react';
|
|
12
|
+
import { useTheme } from 'styled-components';
|
|
13
|
+
import { useMetricsTimeSpan } from '../linetemporalchart/MetricTimespanProvider';
|
|
14
|
+
import { addMissingDataPoint } from '../linetemporalchart/ChartUtil';
|
|
15
|
+
import styled from 'styled-components';
|
|
16
|
+
import { fontSize, fontWeight } from '../../style/theme';
|
|
17
|
+
import { useChartLegend } from '../chartlegend/ChartLegendWrapper';
|
|
18
|
+
import { ChartTitleText, SmallerText } from '../text/Text.component';
|
|
19
|
+
import { Loader } from '../loader/Loader.component';
|
|
20
|
+
import { spacing } from '../../spacing';
|
|
21
|
+
import { getUnitLabel } from '../linetemporalchart/ChartUtil';
|
|
22
|
+
import { Icon } from '../icon/Icon.component';
|
|
23
|
+
import { Tooltip as TooltipComponent } from '../tooltip/Tooltip.component';
|
|
24
|
+
import {
|
|
25
|
+
DAY_MONTH_ABBREVIATED_HOUR_MINUTE,
|
|
26
|
+
FormattedDateTime,
|
|
27
|
+
} from '../date/FormattedDateTime';
|
|
28
|
+
|
|
29
|
+
const LineTemporalChartWrapper = styled.div`
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
justify-content: flex-start;
|
|
33
|
+
flex: 1;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const ChartHeader = styled.div`
|
|
37
|
+
display: flex;
|
|
38
|
+
align-items: center;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const TooltipContainer = styled.div`
|
|
42
|
+
background-color: ${(props) => props.theme.backgroundLevel1};
|
|
43
|
+
padding: ${spacing.r8};
|
|
44
|
+
border: 1px solid ${(props) => props.theme.border};
|
|
45
|
+
border-radius: 4px;
|
|
46
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
47
|
+
max-width: 250px;
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const TooltipTime = styled.div`
|
|
51
|
+
margin-bottom: ${spacing.r8};
|
|
52
|
+
color: ${(props) => props.theme.textPrimary};
|
|
53
|
+
font-size: ${fontSize.smaller};
|
|
54
|
+
font-weight: ${fontWeight.bold};
|
|
55
|
+
text-align: center;
|
|
56
|
+
`;
|
|
57
|
+
|
|
58
|
+
const TooltipValue = styled.div`
|
|
59
|
+
font-size: ${fontSize.smaller};
|
|
60
|
+
margin-top: 4px;
|
|
61
|
+
color: ${(props) => props.theme.textSecondary};
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: flex-start;
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
const TooltipLegend = styled.div<{ color: string }>`
|
|
67
|
+
width: 12px;
|
|
68
|
+
height: 3px;
|
|
69
|
+
background-color: ${(props) => props.color};
|
|
70
|
+
margin-right: 8px;
|
|
71
|
+
flex-shrink: 0;
|
|
72
|
+
margin-top: 8px;
|
|
73
|
+
`;
|
|
74
|
+
|
|
75
|
+
const TooltipContent = styled.div`
|
|
76
|
+
display: flex;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
flex: 1;
|
|
79
|
+
`;
|
|
80
|
+
|
|
81
|
+
const TooltipName = styled.div`
|
|
82
|
+
margin-right: 4px;
|
|
83
|
+
word-wrap: break-word;
|
|
84
|
+
word-break: break-word;
|
|
85
|
+
justify-content: flex-start;
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
const TooltipInstanceValue = styled.div`
|
|
89
|
+
justify-content: flex-end;
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
export type Serie = {
|
|
93
|
+
// the name of the resource
|
|
94
|
+
resource: string;
|
|
95
|
+
// the original data format from prometheus
|
|
96
|
+
data: [number, string | null][];
|
|
97
|
+
// it's mandatory to display tooltip label in the tooltip
|
|
98
|
+
getTooltipLabel: (metricPrefix?: string, resource?: string) => string;
|
|
99
|
+
// the name of the metric prefix with read, write, in, out
|
|
100
|
+
metricPrefix?: string;
|
|
101
|
+
// to specify if the line is dash
|
|
102
|
+
isLineDashed?: boolean;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type NonSymmetricalChartSerie = {
|
|
106
|
+
yAxisType?: 'default' | 'percentage';
|
|
107
|
+
series: Serie[];
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// The symmetrical chart props are used to display two series on the same chart, such as in/out, write/read
|
|
111
|
+
type SymmetricalChartSerie = {
|
|
112
|
+
yAxisType: 'symmetrical';
|
|
113
|
+
series: {
|
|
114
|
+
above: Serie[];
|
|
115
|
+
below: Serie[];
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export type LineChartProps = (
|
|
120
|
+
| NonSymmetricalChartSerie
|
|
121
|
+
| SymmetricalChartSerie
|
|
122
|
+
) & {
|
|
123
|
+
title: string;
|
|
124
|
+
height: number;
|
|
125
|
+
startingTimeStamp: number;
|
|
126
|
+
unitRange?: {
|
|
127
|
+
threshold: number;
|
|
128
|
+
label: string;
|
|
129
|
+
}[];
|
|
130
|
+
isLoading?: boolean;
|
|
131
|
+
yAxisTitle?: string;
|
|
132
|
+
helpText?: string;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const CustomTooltip = ({
|
|
136
|
+
active,
|
|
137
|
+
payload,
|
|
138
|
+
label,
|
|
139
|
+
unitLabel,
|
|
140
|
+
}: {
|
|
141
|
+
active?: boolean;
|
|
142
|
+
payload?: Array<{
|
|
143
|
+
value: number;
|
|
144
|
+
name: string;
|
|
145
|
+
color: string;
|
|
146
|
+
dataKey: string;
|
|
147
|
+
}>;
|
|
148
|
+
label?: string;
|
|
149
|
+
unitLabel?: string;
|
|
150
|
+
}) => {
|
|
151
|
+
if (!active || !payload || !payload.length || !label) return null;
|
|
152
|
+
// We can't use the default itemSorter method because it's a custom tooltip.
|
|
153
|
+
// Sort the payload here instead
|
|
154
|
+
const sortedPayload = [...payload].sort((a, b) => {
|
|
155
|
+
const aValue = Number(a.value);
|
|
156
|
+
const bValue = Number(b.value);
|
|
157
|
+
|
|
158
|
+
if (aValue >= 0 && bValue >= 0) {
|
|
159
|
+
return bValue - aValue; // Higher positive values first
|
|
160
|
+
}
|
|
161
|
+
if (aValue < 0 && bValue < 0) {
|
|
162
|
+
return bValue - aValue; // Lower negative values first
|
|
163
|
+
}
|
|
164
|
+
return bValue - aValue; // Positives before negatives
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<TooltipContainer>
|
|
169
|
+
<TooltipTime>
|
|
170
|
+
<FormattedDateTime
|
|
171
|
+
format="day-month-abbreviated-hour-minute-second"
|
|
172
|
+
value={new Date(label)}
|
|
173
|
+
/>
|
|
174
|
+
</TooltipTime>
|
|
175
|
+
{sortedPayload.map((entry, index) => (
|
|
176
|
+
<TooltipValue key={index}>
|
|
177
|
+
<TooltipLegend color={entry.color} />
|
|
178
|
+
<TooltipContent>
|
|
179
|
+
<TooltipName>{entry.name}</TooltipName>
|
|
180
|
+
<TooltipInstanceValue>
|
|
181
|
+
{isNaN(Number(entry.value))
|
|
182
|
+
? '-'
|
|
183
|
+
: `${Number(entry.value).toFixed(2)}${unitLabel}`}
|
|
184
|
+
</TooltipInstanceValue>
|
|
185
|
+
</TooltipContent>
|
|
186
|
+
</TooltipValue>
|
|
187
|
+
))}
|
|
188
|
+
</TooltipContainer>
|
|
189
|
+
);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const isSymmetricalSeries = (
|
|
193
|
+
series: Serie[] | { above: Serie[]; below: Serie[] },
|
|
194
|
+
): series is { above: Serie[]; below: Serie[] } => {
|
|
195
|
+
return 'above' in series && 'below' in series;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export function LineTimeSerieChart({
|
|
199
|
+
series,
|
|
200
|
+
title,
|
|
201
|
+
height,
|
|
202
|
+
startingTimeStamp,
|
|
203
|
+
unitRange,
|
|
204
|
+
isLoading = false,
|
|
205
|
+
yAxisType = 'default',
|
|
206
|
+
yAxisTitle,
|
|
207
|
+
helpText,
|
|
208
|
+
...rest
|
|
209
|
+
}: LineChartProps) {
|
|
210
|
+
const theme = useTheme();
|
|
211
|
+
const { frequency, duration } = useMetricsTimeSpan();
|
|
212
|
+
const { getColor } = useChartLegend();
|
|
213
|
+
const chartRef = useRef(null);
|
|
214
|
+
|
|
215
|
+
const chartData = useMemo(() => {
|
|
216
|
+
// 1. Add missing data points
|
|
217
|
+
const normalizedSeries =
|
|
218
|
+
yAxisType === 'symmetrical' && isSymmetricalSeries(series)
|
|
219
|
+
? {
|
|
220
|
+
above: series.above.map((line) => ({
|
|
221
|
+
...line,
|
|
222
|
+
data: addMissingDataPoint(
|
|
223
|
+
line.data,
|
|
224
|
+
startingTimeStamp,
|
|
225
|
+
duration,
|
|
226
|
+
frequency,
|
|
227
|
+
),
|
|
228
|
+
})),
|
|
229
|
+
// Convert positive values to negative values
|
|
230
|
+
below: series.below.map((line) => ({
|
|
231
|
+
...line,
|
|
232
|
+
data: addMissingDataPoint(
|
|
233
|
+
line.data,
|
|
234
|
+
startingTimeStamp,
|
|
235
|
+
duration,
|
|
236
|
+
frequency,
|
|
237
|
+
).map(
|
|
238
|
+
([timestamp, value]) =>
|
|
239
|
+
[timestamp, value === null ? null : `-${Number(value)}`] as [
|
|
240
|
+
number,
|
|
241
|
+
string | null,
|
|
242
|
+
],
|
|
243
|
+
),
|
|
244
|
+
})),
|
|
245
|
+
}
|
|
246
|
+
: (series as Serie[]).map((line) => ({
|
|
247
|
+
...line,
|
|
248
|
+
data: addMissingDataPoint(
|
|
249
|
+
line.data,
|
|
250
|
+
startingTimeStamp,
|
|
251
|
+
duration,
|
|
252
|
+
frequency,
|
|
253
|
+
),
|
|
254
|
+
}));
|
|
255
|
+
|
|
256
|
+
// 2. Convert directly to Recharts format
|
|
257
|
+
// Initialize an object to hold data points by timestamp
|
|
258
|
+
const dataPointsByTime: Record<
|
|
259
|
+
number,
|
|
260
|
+
{ timestamp: number } & Record<string, string | number | null>
|
|
261
|
+
> = {};
|
|
262
|
+
const seriesToProcess =
|
|
263
|
+
yAxisType === 'symmetrical' && isSymmetricalSeries(normalizedSeries)
|
|
264
|
+
? [...normalizedSeries.above, ...normalizedSeries.below]
|
|
265
|
+
: (normalizedSeries as Serie[]);
|
|
266
|
+
|
|
267
|
+
seriesToProcess.forEach((serie) => {
|
|
268
|
+
const label = serie.getTooltipLabel(serie.metricPrefix, serie.resource);
|
|
269
|
+
|
|
270
|
+
serie.data.forEach((point) => {
|
|
271
|
+
const timestamp =
|
|
272
|
+
typeof point[0] === 'number' ? point[0] * 1000 : Number(point[0]);
|
|
273
|
+
const value = point[1];
|
|
274
|
+
// Initialize this timestamp if it doesn't exist
|
|
275
|
+
if (!dataPointsByTime[timestamp]) {
|
|
276
|
+
dataPointsByTime[timestamp] = { timestamp };
|
|
277
|
+
}
|
|
278
|
+
// Add this metric's value to the data point, and convert the value to a number if it's a string
|
|
279
|
+
dataPointsByTime[timestamp][label] =
|
|
280
|
+
typeof value === 'string' ? Number(value) : value;
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
// Convert object to array for Recharts
|
|
284
|
+
return Object.values(dataPointsByTime).sort(
|
|
285
|
+
(
|
|
286
|
+
a: { timestamp: number } & Record<string, string | number | null>,
|
|
287
|
+
b: { timestamp: number } & Record<string, string | number | null>,
|
|
288
|
+
) => (a.timestamp as number) - (b.timestamp as number),
|
|
289
|
+
);
|
|
290
|
+
}, [series, startingTimeStamp, duration, frequency, yAxisType]);
|
|
291
|
+
|
|
292
|
+
// Calculate 5 perfectly evenly spaced ticks
|
|
293
|
+
const xAxisTicks = useMemo(() => {
|
|
294
|
+
if (!chartData || chartData.length === 0) return [];
|
|
295
|
+
|
|
296
|
+
const timestamps: number[] = chartData.map((d) => d.timestamp);
|
|
297
|
+
const minTimestamp = Math.min(...timestamps);
|
|
298
|
+
const maxTimestamp = Math.max(...timestamps);
|
|
299
|
+
|
|
300
|
+
// Calculate 5 perfectly evenly spaced ticks
|
|
301
|
+
const timeRange = maxTimestamp - minTimestamp;
|
|
302
|
+
const interval = timeRange / 4; // 4 intervals create 5 points
|
|
303
|
+
|
|
304
|
+
const exactEvenTicks = [
|
|
305
|
+
minTimestamp,
|
|
306
|
+
minTimestamp + interval,
|
|
307
|
+
minTimestamp + interval * 2,
|
|
308
|
+
minTimestamp + interval * 3,
|
|
309
|
+
maxTimestamp,
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
// Return perfectly even ticks (guaranteed to be evenly divided)
|
|
313
|
+
return exactEvenTicks;
|
|
314
|
+
}, [chartData]);
|
|
315
|
+
|
|
316
|
+
// 3. Transform the data base on the valuebase
|
|
317
|
+
const { topValue, unitLabel, rechartsData } = useMemo(() => {
|
|
318
|
+
if (yAxisType === 'percentage')
|
|
319
|
+
return {
|
|
320
|
+
topValue: 100,
|
|
321
|
+
unitLabel: '%',
|
|
322
|
+
rechartsData: chartData,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const values = chartData.flatMap((dataPoint) =>
|
|
326
|
+
Object.entries(dataPoint)
|
|
327
|
+
.filter(([key]) => key !== 'timestamp')
|
|
328
|
+
.map(([_, value]) => {
|
|
329
|
+
const num =
|
|
330
|
+
typeof value === 'string' ? Number(value) : (value ?? Infinity);
|
|
331
|
+
return !isNaN(num) && num !== null ? num : null;
|
|
332
|
+
})
|
|
333
|
+
.filter((value): value is number => value !== null),
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const top = Math.abs(Math.max(...values));
|
|
337
|
+
const bottom = Math.abs(Math.min(...values));
|
|
338
|
+
const maxValue = Math.max(top, bottom);
|
|
339
|
+
|
|
340
|
+
const { valueBase, unitLabel } = getUnitLabel(unitRange ?? [], maxValue);
|
|
341
|
+
|
|
342
|
+
const topValue = Math.ceil(maxValue / valueBase / 10) * 10;
|
|
343
|
+
|
|
344
|
+
const rechartsData = chartData.map((dataPoint) => {
|
|
345
|
+
const normalizedDataPoint = { ...dataPoint };
|
|
346
|
+
Object.entries(dataPoint).forEach(([key, value]) => {
|
|
347
|
+
if (key !== 'timestamp' && typeof value === 'number') {
|
|
348
|
+
normalizedDataPoint[key] = value / valueBase;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
return normalizedDataPoint;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return { topValue, unitLabel, rechartsData };
|
|
355
|
+
}, [chartData, yAxisType, unitRange]);
|
|
356
|
+
|
|
357
|
+
// Group series by resource and create color mapping
|
|
358
|
+
const { colorMapping, groupedSeries } = useMemo(() => {
|
|
359
|
+
const mapping: Record<string, string> = {};
|
|
360
|
+
const allSeries = isSymmetricalSeries(series)
|
|
361
|
+
? [...series.above, ...series.below]
|
|
362
|
+
: (series as Serie[]);
|
|
363
|
+
|
|
364
|
+
// Group series by resource
|
|
365
|
+
const groups = allSeries.reduce(
|
|
366
|
+
(acc, serie) => {
|
|
367
|
+
const key = serie.resource;
|
|
368
|
+
if (!acc[key]) {
|
|
369
|
+
acc[key] = [];
|
|
370
|
+
}
|
|
371
|
+
acc[key].push(serie);
|
|
372
|
+
return acc;
|
|
373
|
+
},
|
|
374
|
+
{} as Record<string, Serie[]>,
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
// Get colors from the ChartLegend context
|
|
378
|
+
Object.keys(groups).forEach((resource) => {
|
|
379
|
+
const color = getColor(resource);
|
|
380
|
+
if (color) {
|
|
381
|
+
mapping[resource] = color;
|
|
382
|
+
} else {
|
|
383
|
+
console.warn(`Color not defined for resource: ${resource}`);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
colorMapping: mapping,
|
|
389
|
+
groupedSeries: groups,
|
|
390
|
+
};
|
|
391
|
+
}, [series, getColor]);
|
|
392
|
+
|
|
393
|
+
// Format time for display the tick in the x axis
|
|
394
|
+
const formatTime = useMemo(
|
|
395
|
+
() => (timestamp: number) => {
|
|
396
|
+
const date = new Date(timestamp);
|
|
397
|
+
return DAY_MONTH_ABBREVIATED_HOUR_MINUTE.format(date).replace(',', '');
|
|
398
|
+
},
|
|
399
|
+
[],
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<LineTemporalChartWrapper>
|
|
404
|
+
<ChartHeader>
|
|
405
|
+
<ChartTitleText>
|
|
406
|
+
{title} {unitLabel && `(${unitLabel})`}
|
|
407
|
+
</ChartTitleText>
|
|
408
|
+
{helpText && (
|
|
409
|
+
<TooltipComponent
|
|
410
|
+
placement={'right'}
|
|
411
|
+
overlay={<SmallerText>{helpText}</SmallerText>}
|
|
412
|
+
>
|
|
413
|
+
<Icon name="Info" color={theme.buttonSecondary} />
|
|
414
|
+
</TooltipComponent>
|
|
415
|
+
)}
|
|
416
|
+
{isLoading && <Loader />}
|
|
417
|
+
</ChartHeader>
|
|
418
|
+
<ResponsiveContainer width="100%" height={height}>
|
|
419
|
+
<LineChart
|
|
420
|
+
data={rechartsData}
|
|
421
|
+
ref={chartRef}
|
|
422
|
+
margin={{ top: 0, right: 0, bottom: 0, left: 0 }}
|
|
423
|
+
aria-label={`Time series chart for ${title}`}
|
|
424
|
+
>
|
|
425
|
+
<CartesianGrid
|
|
426
|
+
vertical={true}
|
|
427
|
+
horizontal={true}
|
|
428
|
+
verticalPoints={[0]}
|
|
429
|
+
horizontalPoints={[0]}
|
|
430
|
+
stroke={theme.border}
|
|
431
|
+
fill={theme.backgroundLevel4}
|
|
432
|
+
strokeWidth={1}
|
|
433
|
+
/>
|
|
434
|
+
<XAxis
|
|
435
|
+
dataKey="timestamp"
|
|
436
|
+
type="number"
|
|
437
|
+
domain={['dataMin', 'dataMax']}
|
|
438
|
+
ticks={xAxisTicks}
|
|
439
|
+
tickFormatter={formatTime}
|
|
440
|
+
tickCount={5}
|
|
441
|
+
tick={{
|
|
442
|
+
fill: theme.textSecondary,
|
|
443
|
+
fontSize: fontSize.smaller,
|
|
444
|
+
}}
|
|
445
|
+
axisLine={{ stroke: theme.border }}
|
|
446
|
+
/>
|
|
447
|
+
<YAxis
|
|
448
|
+
orientation="right"
|
|
449
|
+
allowDataOverflow={false}
|
|
450
|
+
label={{
|
|
451
|
+
value: yAxisTitle,
|
|
452
|
+
angle: 90,
|
|
453
|
+
position: 'insideRight',
|
|
454
|
+
style: {
|
|
455
|
+
textAnchor: 'middle',
|
|
456
|
+
fill: theme.textSecondary,
|
|
457
|
+
fontSize: fontSize.smaller,
|
|
458
|
+
},
|
|
459
|
+
}}
|
|
460
|
+
domain={
|
|
461
|
+
yAxisType === 'percentage'
|
|
462
|
+
? [0, 100]
|
|
463
|
+
: yAxisType === 'symmetrical'
|
|
464
|
+
? [-topValue, topValue]
|
|
465
|
+
: [0, topValue]
|
|
466
|
+
}
|
|
467
|
+
axisLine={{ stroke: theme.border }}
|
|
468
|
+
tick={{
|
|
469
|
+
fill: theme.textSecondary,
|
|
470
|
+
fontSize: fontSize.smaller,
|
|
471
|
+
}}
|
|
472
|
+
tickFormatter={(value) => Math.round(value).toString()}
|
|
473
|
+
/>
|
|
474
|
+
<Tooltip content={<CustomTooltip unitLabel={unitLabel} />} />
|
|
475
|
+
{/* Add horizontal line at y=0 for symmetrical charts */}
|
|
476
|
+
{yAxisType === 'symmetrical' && (
|
|
477
|
+
<ReferenceLine y={0} stroke={theme.border} />
|
|
478
|
+
)}
|
|
479
|
+
|
|
480
|
+
{/* Chart lines */}
|
|
481
|
+
{Object.entries(groupedSeries).map(([resource, resourceSeries]) =>
|
|
482
|
+
resourceSeries.map((serie, serieIndex) => {
|
|
483
|
+
const label = serie.getTooltipLabel(
|
|
484
|
+
serie.metricPrefix,
|
|
485
|
+
serie.resource,
|
|
486
|
+
);
|
|
487
|
+
return (
|
|
488
|
+
<Line
|
|
489
|
+
key={`${title}-${resource}-${serieIndex}`}
|
|
490
|
+
type="monotone"
|
|
491
|
+
dataKey={label}
|
|
492
|
+
stroke={colorMapping[resource]}
|
|
493
|
+
dot={false}
|
|
494
|
+
/>
|
|
495
|
+
);
|
|
496
|
+
}),
|
|
497
|
+
)}
|
|
498
|
+
</LineChart>
|
|
499
|
+
</ResponsiveContainer>
|
|
500
|
+
</LineTemporalChartWrapper>
|
|
501
|
+
);
|
|
502
|
+
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, screen, waitFor } from '@testing-library/react';
|
|
3
3
|
import { SearchInput, Props } from './SearchInput.component';
|
|
4
|
-
import { QueryClient, QueryClientProvider } from 'react-query';
|
|
5
4
|
import userEvent from '@testing-library/user-event';
|
|
6
5
|
|
|
7
|
-
const queryClient = new QueryClient();
|
|
8
|
-
|
|
9
6
|
const SearchInputRender = (props: Props) => {
|
|
10
7
|
return (
|
|
11
|
-
<
|
|
12
|
-
<SearchInput {...props} />
|
|
13
|
-
</QueryClientProvider>
|
|
8
|
+
<SearchInput {...props} />
|
|
14
9
|
);
|
|
15
10
|
};
|
|
16
11
|
|
|
@@ -19,8 +14,9 @@ describe('SearchInput', () => {
|
|
|
19
14
|
searchInput: () => screen.getByRole('searchbox'),
|
|
20
15
|
clearButton: () => screen.queryByRole('button'),
|
|
21
16
|
};
|
|
22
|
-
it('should render the SearchInput component', () => {
|
|
17
|
+
it('should render the SearchInput component', async () => {
|
|
23
18
|
render(<SearchInputRender value="" onChange={() => {}} />);
|
|
19
|
+
await waitFor(() => screen.queryAllByRole('img', { hidden: true }));
|
|
24
20
|
|
|
25
21
|
const searchInput = selectors.searchInput();
|
|
26
22
|
expect(searchInput).toBeInTheDocument();
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
useImperativeHandle,
|
|
11
11
|
ReactNode,
|
|
12
12
|
Ref,
|
|
13
|
+
useMemo,
|
|
14
|
+
useCallback,
|
|
13
15
|
} from 'react';
|
|
14
16
|
import { ScrollbarWrapper, Tooltip } from '../../index';
|
|
15
17
|
import {
|
|
@@ -562,22 +564,28 @@ const SelectWithOptionContext = forwardRef<
|
|
|
562
564
|
>((props, ref) => {
|
|
563
565
|
const [options, setOptions] = useState<Record<string, SelectOptionProps>>({});
|
|
564
566
|
|
|
565
|
-
const register = (option: SelectOptionProps) => {
|
|
567
|
+
const register = useCallback((option: SelectOptionProps) => {
|
|
566
568
|
setOptions((prevOptions) => ({
|
|
567
569
|
...prevOptions,
|
|
568
570
|
[option.value]: option,
|
|
569
571
|
}));
|
|
570
|
-
};
|
|
572
|
+
}, []);
|
|
571
573
|
|
|
572
|
-
const unregister = (value: string) => {
|
|
574
|
+
const unregister = useCallback((value: string) => {
|
|
573
575
|
setOptions((prevOptions) => {
|
|
574
576
|
const { [value]: _, ...rest } = prevOptions;
|
|
575
577
|
return rest;
|
|
576
578
|
});
|
|
577
|
-
};
|
|
579
|
+
}, []);
|
|
580
|
+
|
|
581
|
+
const contextValue = useMemo(() => ({
|
|
582
|
+
options,
|
|
583
|
+
register,
|
|
584
|
+
unregister
|
|
585
|
+
}), [options, register, unregister]);
|
|
578
586
|
|
|
579
587
|
return (
|
|
580
|
-
<OptionContext.Provider value={
|
|
588
|
+
<OptionContext.Provider value={contextValue}>
|
|
581
589
|
<>
|
|
582
590
|
<SelectBox {...props} selectRef={ref} />
|
|
583
591
|
{props.children}
|