@oneuptime/common 12.0.26 → 12.0.28
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/Models/AnalyticsModels/RumSessionChunk.ts +52 -0
- package/Models/DatabaseModels/CloudResource.ts +17 -1
- package/Models/DatabaseModels/RumApplication.ts +35 -2
- package/Server/API/TelemetryAPI.ts +104 -15
- package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +12 -5
- package/Server/Utils/SessionReplay/SessionReplayIdentity.ts +102 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +11 -2
- package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +81 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +232 -0
- package/Tests/Types/API/HostnameAuthorityParsing.test.ts +1 -0
- package/Tests/Types/API/URLMailtoQueryString.test.ts +439 -0
- package/Tests/Types/API/URLQueryString.test.ts +33 -24
- package/Tests/Types/API/URLRouteQuerySeparation.test.ts +491 -0
- package/Tests/UI/Components/LogsHistogramDragTooltip.test.tsx +393 -0
- package/Tests/UI/Components/LogsViewerHistogramZoom.test.tsx +248 -0
- package/Tests/UI/Components/TelemetryHistogramDragTooltip.test.tsx +205 -0
- package/Tests/UI/Components/UseHistogramZoom.test.tsx +262 -0
- package/Types/API/URL.ts +55 -18
- package/Types/Rum/SessionReplay.ts +14 -0
- package/UI/Components/Charts/Utils/useHistogramZoom.ts +101 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +19 -3
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +52 -1
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +18 -2
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +52 -1
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js +48 -0
- package/build/dist/Models/AnalyticsModels/RumSessionChunk.js.map +1 -1
- package/build/dist/Models/DatabaseModels/CloudResource.js +19 -2
- package/build/dist/Models/DatabaseModels/CloudResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +37 -3
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +67 -14
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js +59 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayIdentity.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +9 -2
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Types/API/URL.js +48 -17
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js +47 -0
- package/build/dist/UI/Components/Charts/Utils/useHistogramZoom.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +15 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +33 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +14 -2
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +33 -5
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/** @timezone UTC */
|
|
2
|
+
|
|
3
|
+
import { HistogramBucket } from "../../../UI/Components/LogsViewer/types";
|
|
4
|
+
import LogSeverity from "../../../Types/Log/LogSeverity";
|
|
5
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
8
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* Recharts resolves a pointer to a bucket from real layout — a bounding rect,
|
|
12
|
+
* an animation frame — none of which jsdom provides, so a drag fired at the
|
|
13
|
+
* real chart never reaches the component under test. The chart is replaced by
|
|
14
|
+
* a stand-in that hands each bucket a hit target and calls the very same
|
|
15
|
+
* onMouseDown / onMouseMove / onMouseUp props with the chart state recharts
|
|
16
|
+
* would have passed. What is left under test is exactly what this file is
|
|
17
|
+
* about: how LogsHistogram reacts to a drag.
|
|
18
|
+
*
|
|
19
|
+
* The tooltip stand-in reports the `active` prop it was handed, because that
|
|
20
|
+
* is the switch the fix flips — recharts hides the tooltip outright when
|
|
21
|
+
* `active` is false and picks its own behaviour back up when the prop is not
|
|
22
|
+
* passed at all.
|
|
23
|
+
*/
|
|
24
|
+
jest.mock("recharts", () => {
|
|
25
|
+
const react: typeof React = jest.requireActual("react") as typeof React;
|
|
26
|
+
|
|
27
|
+
interface StubRow {
|
|
28
|
+
time: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface StubChartProps {
|
|
32
|
+
data: Array<StubRow>;
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
onMouseDown?: (state: { activeLabel: string }) => void;
|
|
35
|
+
onMouseMove?: (state: { activeLabel: string }) => void;
|
|
36
|
+
onMouseUp?: (state: { activeLabel: string }) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
__esModule: true,
|
|
41
|
+
ResponsiveContainer: (props: { children: React.ReactNode }) => {
|
|
42
|
+
return react.createElement("div", null, props.children);
|
|
43
|
+
},
|
|
44
|
+
BarChart: (props: StubChartProps) => {
|
|
45
|
+
return react.createElement(
|
|
46
|
+
"div",
|
|
47
|
+
{ "data-testid": "bar-chart" },
|
|
48
|
+
props.data.map((row: StubRow) => {
|
|
49
|
+
return react.createElement("div", {
|
|
50
|
+
key: row.time,
|
|
51
|
+
"data-testid": `bucket-${row.time}`,
|
|
52
|
+
onMouseDown: () => {
|
|
53
|
+
props.onMouseDown?.({ activeLabel: row.time });
|
|
54
|
+
},
|
|
55
|
+
onMouseMove: () => {
|
|
56
|
+
props.onMouseMove?.({ activeLabel: row.time });
|
|
57
|
+
},
|
|
58
|
+
onMouseUp: () => {
|
|
59
|
+
props.onMouseUp?.({ activeLabel: row.time });
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}),
|
|
63
|
+
props.children,
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
Bar: () => {
|
|
67
|
+
return null;
|
|
68
|
+
},
|
|
69
|
+
XAxis: () => {
|
|
70
|
+
return null;
|
|
71
|
+
},
|
|
72
|
+
YAxis: () => {
|
|
73
|
+
return null;
|
|
74
|
+
},
|
|
75
|
+
Tooltip: (props: { active?: boolean }) => {
|
|
76
|
+
return react.createElement("div", {
|
|
77
|
+
"data-testid": "tooltip",
|
|
78
|
+
"data-active": String(props.active),
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
ReferenceArea: (props: { x1?: string; x2?: string }) => {
|
|
82
|
+
return react.createElement("div", {
|
|
83
|
+
"data-testid": "selection-band",
|
|
84
|
+
"data-x1": props.x1,
|
|
85
|
+
"data-x2": props.x2,
|
|
86
|
+
});
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Imported after the mock so the chart picks the stand-in up.
|
|
92
|
+
import LogsHistogram from "../../../UI/Components/LogsViewer/components/LogsHistogram";
|
|
93
|
+
|
|
94
|
+
const FIRST_BUCKET: string = "2026-08-05T11:58:00Z";
|
|
95
|
+
const MIDDLE_BUCKET: string = "2026-08-05T11:59:00Z";
|
|
96
|
+
const LAST_BUCKET: string = "2026-08-05T12:00:00Z";
|
|
97
|
+
|
|
98
|
+
const BUCKETS: Array<HistogramBucket> = [
|
|
99
|
+
{ time: FIRST_BUCKET, severity: LogSeverity.Error, count: 3 },
|
|
100
|
+
{ time: MIDDLE_BUCKET, severity: LogSeverity.Error, count: 5 },
|
|
101
|
+
{ time: LAST_BUCKET, severity: LogSeverity.Error, count: 7 },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
const ZOOM_OUT_HINT: string = "Double-click to zoom out";
|
|
105
|
+
|
|
106
|
+
function bucketAt(time: string): HTMLElement {
|
|
107
|
+
return screen.getByTestId(`bucket-${time}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** What recharts was told to do with the tooltip on the last render. */
|
|
111
|
+
function tooltipActiveProp(): string | null {
|
|
112
|
+
return screen.getByTestId("tooltip").getAttribute("data-active");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isTooltipSuppressed(): boolean {
|
|
116
|
+
return tooltipActiveProp() === "false";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function selectionBand(): HTMLElement | null {
|
|
120
|
+
return screen.queryByTestId("selection-band");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface RenderedHistogram {
|
|
124
|
+
onTimeRangeSelect: MockFunction;
|
|
125
|
+
onZoomOut: MockFunction;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function renderHistogram(
|
|
129
|
+
options: { canSelect?: boolean; canZoomOut?: boolean } = {},
|
|
130
|
+
): RenderedHistogram {
|
|
131
|
+
const onTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
132
|
+
const onZoomOut: MockFunction = getJestMockFunction();
|
|
133
|
+
|
|
134
|
+
render(
|
|
135
|
+
<LogsHistogram
|
|
136
|
+
buckets={BUCKETS}
|
|
137
|
+
isLoading={false}
|
|
138
|
+
{...(options.canSelect === false
|
|
139
|
+
? {}
|
|
140
|
+
: { onTimeRangeSelect: onTimeRangeSelect })}
|
|
141
|
+
{...(options.canZoomOut ? { onZoomOut: onZoomOut } : {})}
|
|
142
|
+
/>,
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return { onTimeRangeSelect: onTimeRangeSelect, onZoomOut: onZoomOut };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
describe("LogsHistogram — dragging out a time range", () => {
|
|
149
|
+
afterEach(() => {
|
|
150
|
+
cleanup();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* The reported bug: the tooltip parks itself over the bars while the reader
|
|
155
|
+
* is dragging, hiding the very volumes they are dragging across to pick the
|
|
156
|
+
* range with.
|
|
157
|
+
*/
|
|
158
|
+
describe("the tooltip while a selection is in progress", () => {
|
|
159
|
+
test("stays under recharts' own control before anything is dragged", () => {
|
|
160
|
+
renderHistogram();
|
|
161
|
+
|
|
162
|
+
expect(tooltipActiveProp()).toBe("undefined");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("is held shut for the length of the drag", () => {
|
|
166
|
+
renderHistogram();
|
|
167
|
+
|
|
168
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
169
|
+
expect(isTooltipSuppressed()).toBe(true);
|
|
170
|
+
|
|
171
|
+
fireEvent.mouseMove(bucketAt(MIDDLE_BUCKET));
|
|
172
|
+
expect(isTooltipSuppressed()).toBe(true);
|
|
173
|
+
|
|
174
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
175
|
+
expect(isTooltipSuppressed()).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("comes back the moment the reader lets go", () => {
|
|
179
|
+
renderHistogram();
|
|
180
|
+
|
|
181
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
182
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
183
|
+
fireEvent.mouseUp(bucketAt(LAST_BUCKET));
|
|
184
|
+
|
|
185
|
+
expect(tooltipActiveProp()).toBe("undefined");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/*
|
|
189
|
+
* The stuck-tooltip case from the report: the pointer leaves a 120px-tall
|
|
190
|
+
* chart mid-drag and is released somewhere else on the page, so the
|
|
191
|
+
* chart's own mouseup never fires.
|
|
192
|
+
*/
|
|
193
|
+
test("comes back when the pointer is released outside the chart", () => {
|
|
194
|
+
renderHistogram();
|
|
195
|
+
|
|
196
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
197
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
198
|
+
expect(isTooltipSuppressed()).toBe(true);
|
|
199
|
+
|
|
200
|
+
fireEvent.mouseUp(document.body);
|
|
201
|
+
|
|
202
|
+
expect(tooltipActiveProp()).toBe("undefined");
|
|
203
|
+
expect(selectionBand()).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("comes back after a click that selected nothing", () => {
|
|
207
|
+
renderHistogram();
|
|
208
|
+
|
|
209
|
+
fireEvent.mouseDown(bucketAt(MIDDLE_BUCKET));
|
|
210
|
+
fireEvent.mouseUp(bucketAt(MIDDLE_BUCKET));
|
|
211
|
+
|
|
212
|
+
expect(tooltipActiveProp()).toBe("undefined");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/*
|
|
216
|
+
* A chart the parent cannot zoom never starts a drag, so nothing should
|
|
217
|
+
* ever take the tooltip away from the reader.
|
|
218
|
+
*/
|
|
219
|
+
test("is never suppressed on a chart that cannot be zoomed", () => {
|
|
220
|
+
renderHistogram({ canSelect: false });
|
|
221
|
+
|
|
222
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
223
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
224
|
+
|
|
225
|
+
expect(tooltipActiveProp()).toBe("undefined");
|
|
226
|
+
expect(selectionBand()).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("the selection band", () => {
|
|
231
|
+
test("is only painted once the pointer has moved off the first bucket", () => {
|
|
232
|
+
renderHistogram();
|
|
233
|
+
|
|
234
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
235
|
+
expect(selectionBand()).toBeNull();
|
|
236
|
+
|
|
237
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
238
|
+
|
|
239
|
+
expect(selectionBand()?.getAttribute("data-x1")).toBe(FIRST_BUCKET);
|
|
240
|
+
expect(selectionBand()?.getAttribute("data-x2")).toBe(LAST_BUCKET);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("follows the pointer back and forth across the chart", () => {
|
|
244
|
+
renderHistogram();
|
|
245
|
+
|
|
246
|
+
fireEvent.mouseDown(bucketAt(LAST_BUCKET));
|
|
247
|
+
fireEvent.mouseMove(bucketAt(FIRST_BUCKET));
|
|
248
|
+
expect(selectionBand()?.getAttribute("data-x2")).toBe(FIRST_BUCKET);
|
|
249
|
+
|
|
250
|
+
fireEvent.mouseMove(bucketAt(MIDDLE_BUCKET));
|
|
251
|
+
expect(selectionBand()?.getAttribute("data-x2")).toBe(MIDDLE_BUCKET);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("is cleared once the range has been handed to the parent", () => {
|
|
255
|
+
renderHistogram();
|
|
256
|
+
|
|
257
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
258
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
259
|
+
fireEvent.mouseUp(bucketAt(LAST_BUCKET));
|
|
260
|
+
|
|
261
|
+
expect(selectionBand()).toBeNull();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe("handing the range to the parent", () => {
|
|
266
|
+
test("reports the dragged window", () => {
|
|
267
|
+
const { onTimeRangeSelect } = renderHistogram();
|
|
268
|
+
|
|
269
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
270
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
271
|
+
fireEvent.mouseUp(bucketAt(LAST_BUCKET));
|
|
272
|
+
|
|
273
|
+
expect(onTimeRangeSelect).toHaveBeenCalledTimes(1);
|
|
274
|
+
const [start, end] = onTimeRangeSelect.mock.calls[0] as [Date, Date];
|
|
275
|
+
expect(start.toISOString()).toBe(new Date(FIRST_BUCKET).toISOString());
|
|
276
|
+
expect(end.toISOString()).toBe(new Date(LAST_BUCKET).toISOString());
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("orders a right-to-left drag from earlier to later", () => {
|
|
280
|
+
const { onTimeRangeSelect } = renderHistogram();
|
|
281
|
+
|
|
282
|
+
fireEvent.mouseDown(bucketAt(LAST_BUCKET));
|
|
283
|
+
fireEvent.mouseMove(bucketAt(FIRST_BUCKET));
|
|
284
|
+
fireEvent.mouseUp(bucketAt(FIRST_BUCKET));
|
|
285
|
+
|
|
286
|
+
const [start, end] = onTimeRangeSelect.mock.calls[0] as [Date, Date];
|
|
287
|
+
expect(start.toISOString()).toBe(new Date(FIRST_BUCKET).toISOString());
|
|
288
|
+
expect(end.toISOString()).toBe(new Date(LAST_BUCKET).toISOString());
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
/*
|
|
292
|
+
* A mouseup inside the chart is seen twice — once by the chart, then by
|
|
293
|
+
* the page-wide listener that catches releases outside it. Zooming twice
|
|
294
|
+
* off one drag would leave the reader on the wrong window.
|
|
295
|
+
*/
|
|
296
|
+
test("zooms once even though the release is seen twice", () => {
|
|
297
|
+
const { onTimeRangeSelect } = renderHistogram();
|
|
298
|
+
|
|
299
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
300
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
301
|
+
fireEvent.mouseUp(bucketAt(LAST_BUCKET));
|
|
302
|
+
|
|
303
|
+
expect(onTimeRangeSelect).toHaveBeenCalledTimes(1);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("still reports a drag that ended off the chart", () => {
|
|
307
|
+
const { onTimeRangeSelect } = renderHistogram();
|
|
308
|
+
|
|
309
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
310
|
+
fireEvent.mouseMove(bucketAt(MIDDLE_BUCKET));
|
|
311
|
+
fireEvent.mouseUp(document.body);
|
|
312
|
+
|
|
313
|
+
expect(onTimeRangeSelect).toHaveBeenCalledTimes(1);
|
|
314
|
+
const [start, end] = onTimeRangeSelect.mock.calls[0] as [Date, Date];
|
|
315
|
+
expect(start.toISOString()).toBe(new Date(FIRST_BUCKET).toISOString());
|
|
316
|
+
expect(end.toISOString()).toBe(new Date(MIDDLE_BUCKET).toISOString());
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("treats a click that never moved as no selection at all", () => {
|
|
320
|
+
const { onTimeRangeSelect } = renderHistogram();
|
|
321
|
+
|
|
322
|
+
fireEvent.mouseDown(bucketAt(MIDDLE_BUCKET));
|
|
323
|
+
fireEvent.mouseUp(bucketAt(MIDDLE_BUCKET));
|
|
324
|
+
|
|
325
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/*
|
|
329
|
+
* Releases go on arriving from the page for as long as the listener is
|
|
330
|
+
* attached; only the one that ends a drag may zoom.
|
|
331
|
+
*/
|
|
332
|
+
test("ignores releases once the drag is over", () => {
|
|
333
|
+
const { onTimeRangeSelect } = renderHistogram();
|
|
334
|
+
|
|
335
|
+
fireEvent.mouseDown(bucketAt(FIRST_BUCKET));
|
|
336
|
+
fireEvent.mouseMove(bucketAt(LAST_BUCKET));
|
|
337
|
+
fireEvent.mouseUp(document.body);
|
|
338
|
+
fireEvent.mouseUp(document.body);
|
|
339
|
+
fireEvent.mouseUp(document.body);
|
|
340
|
+
|
|
341
|
+
expect(onTimeRangeSelect).toHaveBeenCalledTimes(1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe("double-click to zoom back out", () => {
|
|
346
|
+
test("puts the reader back on the window they came from", () => {
|
|
347
|
+
const { onZoomOut } = renderHistogram({ canZoomOut: true });
|
|
348
|
+
|
|
349
|
+
fireEvent.doubleClick(screen.getByTestId("bar-chart"));
|
|
350
|
+
|
|
351
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("offers the affordance only once there is something to undo", () => {
|
|
355
|
+
renderHistogram();
|
|
356
|
+
|
|
357
|
+
expect(screen.queryByText(ZOOM_OUT_HINT)).toBeNull();
|
|
358
|
+
|
|
359
|
+
cleanup();
|
|
360
|
+
renderHistogram({ canZoomOut: true });
|
|
361
|
+
|
|
362
|
+
expect(screen.queryByText(ZOOM_OUT_HINT)).not.toBeNull();
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("is inert on a chart that is already on its original window", () => {
|
|
366
|
+
renderHistogram();
|
|
367
|
+
|
|
368
|
+
// The chart is not zoomed, so this must be a no-op rather than a crash.
|
|
369
|
+
fireEvent.doubleClick(screen.getByTestId("bar-chart"));
|
|
370
|
+
|
|
371
|
+
expect(screen.queryByText(ZOOM_OUT_HINT)).toBeNull();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
/*
|
|
375
|
+
* A double-click is two mousedown/mouseup pairs on one bucket. Neither
|
|
376
|
+
* pair moves, so neither may be mistaken for a drag.
|
|
377
|
+
*/
|
|
378
|
+
test("does not zoom in on its own clicks", () => {
|
|
379
|
+
const { onTimeRangeSelect, onZoomOut } = renderHistogram({
|
|
380
|
+
canZoomOut: true,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
fireEvent.mouseDown(bucketAt(MIDDLE_BUCKET));
|
|
384
|
+
fireEvent.mouseUp(bucketAt(MIDDLE_BUCKET));
|
|
385
|
+
fireEvent.mouseDown(bucketAt(MIDDLE_BUCKET));
|
|
386
|
+
fireEvent.mouseUp(bucketAt(MIDDLE_BUCKET));
|
|
387
|
+
fireEvent.doubleClick(bucketAt(MIDDLE_BUCKET));
|
|
388
|
+
|
|
389
|
+
expect(onTimeRangeSelect).not.toHaveBeenCalled();
|
|
390
|
+
expect(onZoomOut).toHaveBeenCalledTimes(1);
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/** @timezone UTC */
|
|
2
|
+
|
|
3
|
+
import { HistogramBucket } from "../../../UI/Components/LogsViewer/types";
|
|
4
|
+
import LogSeverity from "../../../Types/Log/LogSeverity";
|
|
5
|
+
import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
6
|
+
import TimeRange from "../../../Types/Time/TimeRange";
|
|
7
|
+
import "@testing-library/jest-dom";
|
|
8
|
+
import {
|
|
9
|
+
cleanup,
|
|
10
|
+
fireEvent,
|
|
11
|
+
render,
|
|
12
|
+
screen,
|
|
13
|
+
waitFor,
|
|
14
|
+
} from "@testing-library/react";
|
|
15
|
+
import React from "react";
|
|
16
|
+
import { afterEach, describe, expect, jest, test } from "@jest/globals";
|
|
17
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* LogsViewer is the piece that makes double-click-to-zoom-out possible at
|
|
21
|
+
* all: the chart only reports the window a reader dragged out, and only the
|
|
22
|
+
* viewer knows which window they were on before they dragged it. This file
|
|
23
|
+
* covers that link — the chart's own behaviour is in
|
|
24
|
+
* LogsHistogramDragTooltip.test.tsx and the bookkeeping in
|
|
25
|
+
* UseHistogramZoom.test.tsx, and both stay green while this wiring is cut.
|
|
26
|
+
*
|
|
27
|
+
* The container loads services and log attributes on mount, so both API
|
|
28
|
+
* surfaces are mocked out; nothing here depends on what they return.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
32
|
+
const postMock: MockFunction = getJestMockFunction();
|
|
33
|
+
|
|
34
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
35
|
+
return {
|
|
36
|
+
__esModule: true,
|
|
37
|
+
default: {
|
|
38
|
+
getList: (...args: Array<any>) => {
|
|
39
|
+
return getListMock(...args);
|
|
40
|
+
},
|
|
41
|
+
getCommonHeaders: () => {
|
|
42
|
+
return {};
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
jest.mock("../../../UI/Utils/API/API", () => {
|
|
49
|
+
return {
|
|
50
|
+
__esModule: true,
|
|
51
|
+
default: {
|
|
52
|
+
post: (...args: Array<any>) => {
|
|
53
|
+
return postMock(...args);
|
|
54
|
+
},
|
|
55
|
+
getFriendlyErrorMessage: (error: Error) => {
|
|
56
|
+
return error.message;
|
|
57
|
+
},
|
|
58
|
+
getFriendlyMessage: (error: Error) => {
|
|
59
|
+
return error.message;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// See LogsHistogramDragTooltip.test.tsx for why recharts is stood in for.
|
|
66
|
+
jest.mock("recharts", () => {
|
|
67
|
+
const react: typeof React = jest.requireActual("react") as typeof React;
|
|
68
|
+
|
|
69
|
+
interface StubRow {
|
|
70
|
+
time: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface StubChartProps {
|
|
74
|
+
data: Array<StubRow>;
|
|
75
|
+
children?: React.ReactNode;
|
|
76
|
+
onMouseDown?: (state: { activeLabel: string }) => void;
|
|
77
|
+
onMouseMove?: (state: { activeLabel: string }) => void;
|
|
78
|
+
onMouseUp?: (state: { activeLabel: string }) => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
__esModule: true,
|
|
83
|
+
ResponsiveContainer: (props: { children: React.ReactNode }) => {
|
|
84
|
+
return react.createElement("div", null, props.children);
|
|
85
|
+
},
|
|
86
|
+
BarChart: (props: StubChartProps) => {
|
|
87
|
+
return react.createElement(
|
|
88
|
+
"div",
|
|
89
|
+
{ "data-testid": "bar-chart" },
|
|
90
|
+
props.data.map((row: StubRow) => {
|
|
91
|
+
return react.createElement("div", {
|
|
92
|
+
key: row.time,
|
|
93
|
+
"data-testid": `bucket-${row.time}`,
|
|
94
|
+
onMouseDown: () => {
|
|
95
|
+
props.onMouseDown?.({ activeLabel: row.time });
|
|
96
|
+
},
|
|
97
|
+
onMouseMove: () => {
|
|
98
|
+
props.onMouseMove?.({ activeLabel: row.time });
|
|
99
|
+
},
|
|
100
|
+
onMouseUp: () => {
|
|
101
|
+
props.onMouseUp?.({ activeLabel: row.time });
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}),
|
|
105
|
+
props.children,
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
Bar: () => {
|
|
109
|
+
return null;
|
|
110
|
+
},
|
|
111
|
+
XAxis: () => {
|
|
112
|
+
return null;
|
|
113
|
+
},
|
|
114
|
+
YAxis: () => {
|
|
115
|
+
return null;
|
|
116
|
+
},
|
|
117
|
+
Tooltip: (props: { active?: boolean }) => {
|
|
118
|
+
return react.createElement("div", {
|
|
119
|
+
"data-testid": "tooltip",
|
|
120
|
+
"data-active": String(props.active),
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
ReferenceArea: () => {
|
|
124
|
+
return null;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
getListMock.mockImplementation(() => {
|
|
130
|
+
return Promise.resolve({ data: [], count: 0, skip: 0, limit: 0 });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
postMock.mockImplementation(() => {
|
|
134
|
+
return Promise.resolve({ data: {} });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Imported after the mocks so the container picks them up.
|
|
138
|
+
import LogsViewer from "../../../UI/Components/LogsViewer/LogsViewer";
|
|
139
|
+
|
|
140
|
+
const FIRST_BUCKET: string = "2026-08-05T11:58:00Z";
|
|
141
|
+
const LAST_BUCKET: string = "2026-08-05T12:00:00Z";
|
|
142
|
+
|
|
143
|
+
const BUCKETS: Array<HistogramBucket> = [
|
|
144
|
+
{ time: FIRST_BUCKET, severity: LogSeverity.Error, count: 3 },
|
|
145
|
+
{ time: "2026-08-05T11:59:00Z", severity: LogSeverity.Error, count: 5 },
|
|
146
|
+
{ time: LAST_BUCKET, severity: LogSeverity.Error, count: 7 },
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const PAST_DAY: RangeStartAndEndDateTime = { range: TimeRange.PAST_ONE_DAY };
|
|
150
|
+
const ZOOM_OUT_HINT: string = "Double-click to zoom out";
|
|
151
|
+
|
|
152
|
+
interface RenderedViewer {
|
|
153
|
+
onHistogramTimeRangeSelect: MockFunction;
|
|
154
|
+
onTimeRangeChange: MockFunction;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function renderViewer(): Promise<RenderedViewer> {
|
|
158
|
+
const onHistogramTimeRangeSelect: MockFunction = getJestMockFunction();
|
|
159
|
+
const onTimeRangeChange: MockFunction = getJestMockFunction();
|
|
160
|
+
|
|
161
|
+
render(
|
|
162
|
+
<LogsViewer
|
|
163
|
+
logs={[]}
|
|
164
|
+
isLoading={false}
|
|
165
|
+
filterData={{}}
|
|
166
|
+
onFilterChanged={getJestMockFunction()}
|
|
167
|
+
histogramBuckets={BUCKETS}
|
|
168
|
+
histogramLoading={false}
|
|
169
|
+
onHistogramTimeRangeSelect={onHistogramTimeRangeSelect}
|
|
170
|
+
timeRange={PAST_DAY}
|
|
171
|
+
onTimeRangeChange={onTimeRangeChange}
|
|
172
|
+
/>,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(screen.queryByTestId("bar-chart")).not.toBeNull();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
onHistogramTimeRangeSelect: onHistogramTimeRangeSelect,
|
|
181
|
+
onTimeRangeChange: onTimeRangeChange,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function dragAcrossHistogram(): void {
|
|
186
|
+
fireEvent.mouseDown(screen.getByTestId(`bucket-${FIRST_BUCKET}`));
|
|
187
|
+
fireEvent.mouseMove(screen.getByTestId(`bucket-${LAST_BUCKET}`));
|
|
188
|
+
fireEvent.mouseUp(screen.getByTestId(`bucket-${LAST_BUCKET}`));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
describe("LogsViewer wires the histogram's zoom to the window it came from", () => {
|
|
192
|
+
afterEach(() => {
|
|
193
|
+
cleanup();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("a drag still reaches the host that applies the window", async () => {
|
|
197
|
+
const { onHistogramTimeRangeSelect } = await renderViewer();
|
|
198
|
+
|
|
199
|
+
dragAcrossHistogram();
|
|
200
|
+
|
|
201
|
+
expect(onHistogramTimeRangeSelect).toHaveBeenCalledTimes(1);
|
|
202
|
+
const [start, end] = onHistogramTimeRangeSelect.mock.calls[0] as [
|
|
203
|
+
Date,
|
|
204
|
+
Date,
|
|
205
|
+
];
|
|
206
|
+
expect(start.toISOString()).toBe(new Date(FIRST_BUCKET).toISOString());
|
|
207
|
+
expect(end.toISOString()).toBe(new Date(LAST_BUCKET).toISOString());
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("the way back out only appears once a drag has zoomed in", async () => {
|
|
211
|
+
await renderViewer();
|
|
212
|
+
|
|
213
|
+
expect(screen.queryByText(ZOOM_OUT_HINT)).toBeNull();
|
|
214
|
+
|
|
215
|
+
dragAcrossHistogram();
|
|
216
|
+
|
|
217
|
+
expect(screen.queryByText(ZOOM_OUT_HINT)).not.toBeNull();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("double-clicking the chart restores the window the reader was on", async () => {
|
|
221
|
+
const { onTimeRangeChange } = await renderViewer();
|
|
222
|
+
|
|
223
|
+
dragAcrossHistogram();
|
|
224
|
+
fireEvent.doubleClick(screen.getByTestId("bar-chart"));
|
|
225
|
+
|
|
226
|
+
expect(onTimeRangeChange).toHaveBeenCalledTimes(1);
|
|
227
|
+
expect(onTimeRangeChange).toHaveBeenCalledWith(PAST_DAY);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("double-clicking before any zoom leaves the window alone", async () => {
|
|
231
|
+
const { onTimeRangeChange } = await renderViewer();
|
|
232
|
+
|
|
233
|
+
fireEvent.doubleClick(screen.getByTestId("bar-chart"));
|
|
234
|
+
|
|
235
|
+
expect(onTimeRangeChange).not.toHaveBeenCalled();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("the way back out is spent once it has been taken", async () => {
|
|
239
|
+
const { onTimeRangeChange } = await renderViewer();
|
|
240
|
+
|
|
241
|
+
dragAcrossHistogram();
|
|
242
|
+
fireEvent.doubleClick(screen.getByTestId("bar-chart"));
|
|
243
|
+
fireEvent.doubleClick(screen.getByTestId("bar-chart"));
|
|
244
|
+
|
|
245
|
+
expect(onTimeRangeChange).toHaveBeenCalledTimes(1);
|
|
246
|
+
expect(screen.queryByText(ZOOM_OUT_HINT)).toBeNull();
|
|
247
|
+
});
|
|
248
|
+
});
|