@oneuptime/common 12.0.31 → 12.0.32
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/DatabaseModels/AlertState.ts +0 -2
- package/Models/DatabaseModels/Domain.ts +1 -1
- package/Models/DatabaseModels/Incident.ts +1 -1
- package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/MetricType.ts +0 -2
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
- package/Models/DatabaseModels/MonitorTest.ts +0 -2
- package/Models/DatabaseModels/NetworkDevice.ts +121 -2
- package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
- package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
- package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
- package/Models/DatabaseModels/StatusPageResource.ts +0 -2
- package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/DatabaseService.ts +39 -9
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/NetworkDeviceRoleService.ts +90 -0
- package/Server/Services/ProjectService.ts +85 -0
- package/Server/Services/UserNotificationSettingService.ts +0 -6
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
- package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
- package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
- package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
- package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
- package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
- package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
- package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
- package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
- package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
- package/Tests/Server/Utils/BasicCron.test.ts +283 -0
- package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
- package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
- package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
- package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
- package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
- package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
- package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
- package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
- package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
- package/Tests/Utils/Slug.test.ts +61 -1
- package/Tests/jest.setup.ts +19 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
- package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
- package/Types/Permission.ts +45 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
- package/UI/Components/Navbar/NavBar.tsx +62 -13
- package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
- package/UI/Components/SideMenu/SideMenu.tsx +12 -2
- package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
- package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
- package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
- package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
- package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
- package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
- package/Utils/Slug.ts +34 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
- package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +31 -5
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
- package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
- package/build/dist/Server/Services/ProjectService.js +65 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
- package/build/dist/Types/Permission.js +40 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
- package/build/dist/Utils/Slug.js +31 -1
- package/build/dist/Utils/Slug.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
3
|
+
import { act, cleanup, render } from "@testing-library/react";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import getJestMockFunction, { MockFunction } from "../../../MockType";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* A double-click is delivered as TWO plain clicks and only then a
|
|
9
|
+
* `dblclick`. On a dashboard panel a plain click already means something —
|
|
10
|
+
* it pins the bucket and opens the investigation inspector — so wiring
|
|
11
|
+
* double-click-to-reset naively would pop that inspector twice on the way
|
|
12
|
+
* to resetting the board, and leave it open on top of the result.
|
|
13
|
+
*
|
|
14
|
+
* The chart library solves it by holding the click open for
|
|
15
|
+
* DOUBLE_CLICK_DISAMBIGUATION_MS and cancelling it when the double-click
|
|
16
|
+
* lands. That delay is a real cost, so it is armed ONLY when a reset
|
|
17
|
+
* handler is supplied — every other chart in the product keeps instant
|
|
18
|
+
* clicks. These tests pin both halves of that bargain.
|
|
19
|
+
*
|
|
20
|
+
* recharts' own chart root is mocked here so the chart-level handlers can
|
|
21
|
+
* be invoked with a known active index: jsdom reports every element as
|
|
22
|
+
* 0x0, so a real click would never resolve to a bucket and the assertions
|
|
23
|
+
* would pass for the wrong reason. The wiring from recharts' DOM into
|
|
24
|
+
* these handlers is covered separately by ChartDoubleClickReset.test.tsx.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Deliberately NOT a React props interface: the mock chart below takes an
|
|
29
|
+
* untyped bag and this is just how the captured handlers are read back.
|
|
30
|
+
* Typing the mock's parameter would trip react/no-unused-prop-types, since
|
|
31
|
+
* the mock renders none of them.
|
|
32
|
+
*/
|
|
33
|
+
type CapturedChartHandlers = {
|
|
34
|
+
onClick?: ((chartState: Record<string, unknown>) => void) | undefined;
|
|
35
|
+
onDoubleClick?: ((...args: Array<unknown>) => void) | undefined;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mockChartPropsRef: { current: CapturedChartHandlers } = { current: {} };
|
|
39
|
+
|
|
40
|
+
jest.mock("recharts", () => {
|
|
41
|
+
const actual: Record<string, any> = jest.requireActual("recharts");
|
|
42
|
+
return {
|
|
43
|
+
...actual,
|
|
44
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
45
|
+
return children;
|
|
46
|
+
},
|
|
47
|
+
LineChart: (props: Record<string, unknown>): React.ReactElement => {
|
|
48
|
+
mockChartPropsRef.current = props as CapturedChartHandlers;
|
|
49
|
+
return React.createElement("div", { "data-testid": "chart-root" });
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
import { LineChart } from "../../../../UI/Components/Charts/ChartLibrary/LineChart/LineChart";
|
|
55
|
+
import { DOUBLE_CLICK_DISAMBIGUATION_MS } from "../../../../UI/Components/Charts/ChartLibrary/Utils/DoubleClick";
|
|
56
|
+
import {
|
|
57
|
+
CHART_DATA_POINT_DATE_KEY,
|
|
58
|
+
CHART_DATA_POINT_X_AXIS_KEY,
|
|
59
|
+
} from "../../../../UI/Components/Charts/ChartLibrary/Types/ChartDataPoint";
|
|
60
|
+
|
|
61
|
+
const BUCKET_MS: number = 60 * 1000;
|
|
62
|
+
const FIRST_BUCKET: Date = new Date("2026-08-10T00:00:00.000Z");
|
|
63
|
+
const CLICKED_ROW_INDEX: number = 2;
|
|
64
|
+
|
|
65
|
+
function buildRows(): Array<Record<string, number | string>> {
|
|
66
|
+
const rows: Array<Record<string, number | string>> = [];
|
|
67
|
+
for (let index: number = 0; index < 6; index++) {
|
|
68
|
+
const bucketDate: Date = new Date(
|
|
69
|
+
FIRST_BUCKET.getTime() + index * BUCKET_MS,
|
|
70
|
+
);
|
|
71
|
+
rows.push({
|
|
72
|
+
[CHART_DATA_POINT_X_AXIS_KEY]: bucketDate.toISOString(),
|
|
73
|
+
[CHART_DATA_POINT_DATE_KEY]: bucketDate.getTime(),
|
|
74
|
+
CPU: 10 + index,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return rows;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderChart(options: {
|
|
81
|
+
onBucketClick: MockFunction;
|
|
82
|
+
onTimeRangeReset?: MockFunction | undefined;
|
|
83
|
+
}): void {
|
|
84
|
+
render(
|
|
85
|
+
<LineChart
|
|
86
|
+
data={buildRows()}
|
|
87
|
+
index={CHART_DATA_POINT_X_AXIS_KEY}
|
|
88
|
+
categories={["CPU"]}
|
|
89
|
+
showLegend={false}
|
|
90
|
+
onBucketClick={
|
|
91
|
+
options.onBucketClick as unknown as (
|
|
92
|
+
bucketStart: Date,
|
|
93
|
+
bucketEnd: Date,
|
|
94
|
+
valuesAtBucket: Record<string, number | string>,
|
|
95
|
+
) => void
|
|
96
|
+
}
|
|
97
|
+
onTimeRangeReset={
|
|
98
|
+
options.onTimeRangeReset as unknown as (() => void) | undefined
|
|
99
|
+
}
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function clickBucket(): void {
|
|
105
|
+
act(() => {
|
|
106
|
+
mockChartPropsRef.current.onClick?.({
|
|
107
|
+
activeTooltipIndex: CLICKED_ROW_INDEX,
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function doubleClick(): void {
|
|
113
|
+
act(() => {
|
|
114
|
+
mockChartPropsRef.current.onDoubleClick?.({}, {});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function advancePastTheDoubleClickWindow(): void {
|
|
119
|
+
act(() => {
|
|
120
|
+
jest.advanceTimersByTime(DOUBLE_CLICK_DISAMBIGUATION_MS + 10);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
jest.useFakeTimers();
|
|
126
|
+
mockChartPropsRef.current = {};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
afterEach(() => {
|
|
130
|
+
cleanup();
|
|
131
|
+
jest.useRealTimers();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("chart click vs. double-click disambiguation", () => {
|
|
135
|
+
test("with no reset handler, a bucket click is delivered immediately", () => {
|
|
136
|
+
const onBucketClick: MockFunction = getJestMockFunction();
|
|
137
|
+
renderChart({ onBucketClick });
|
|
138
|
+
|
|
139
|
+
// No onDoubleClick is even handed to recharts when reset is off.
|
|
140
|
+
expect(mockChartPropsRef.current.onDoubleClick).toBeUndefined();
|
|
141
|
+
|
|
142
|
+
clickBucket();
|
|
143
|
+
|
|
144
|
+
expect(onBucketClick.mock.calls.length).toBe(1);
|
|
145
|
+
expect(onBucketClick.mock.calls[0]?.[0]).toEqual(
|
|
146
|
+
new Date(FIRST_BUCKET.getTime() + CLICKED_ROW_INDEX * BUCKET_MS),
|
|
147
|
+
);
|
|
148
|
+
// The bucket covers its full width, derived from the adjacent rows.
|
|
149
|
+
expect(onBucketClick.mock.calls[0]?.[1]).toEqual(
|
|
150
|
+
new Date(FIRST_BUCKET.getTime() + (CLICKED_ROW_INDEX + 1) * BUCKET_MS),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("with a reset handler, a single click still reaches the bucket — just later", () => {
|
|
155
|
+
const onBucketClick: MockFunction = getJestMockFunction();
|
|
156
|
+
const onTimeRangeReset: MockFunction = getJestMockFunction();
|
|
157
|
+
renderChart({ onBucketClick, onTimeRangeReset });
|
|
158
|
+
|
|
159
|
+
clickBucket();
|
|
160
|
+
|
|
161
|
+
// Held open, in case a second click is coming.
|
|
162
|
+
expect(onBucketClick.mock.calls.length).toBe(0);
|
|
163
|
+
|
|
164
|
+
advancePastTheDoubleClickWindow();
|
|
165
|
+
|
|
166
|
+
expect(onBucketClick.mock.calls.length).toBe(1);
|
|
167
|
+
expect(onTimeRangeReset.mock.calls.length).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("a double-click resets and never pins a bucket", () => {
|
|
171
|
+
const onBucketClick: MockFunction = getJestMockFunction();
|
|
172
|
+
const onTimeRangeReset: MockFunction = getJestMockFunction();
|
|
173
|
+
renderChart({ onBucketClick, onTimeRangeReset });
|
|
174
|
+
|
|
175
|
+
// Exactly what the browser delivers: click, click, then dblclick.
|
|
176
|
+
clickBucket();
|
|
177
|
+
clickBucket();
|
|
178
|
+
doubleClick();
|
|
179
|
+
|
|
180
|
+
expect(onTimeRangeReset.mock.calls.length).toBe(1);
|
|
181
|
+
|
|
182
|
+
advancePastTheDoubleClickWindow();
|
|
183
|
+
|
|
184
|
+
// Neither click may survive the reset.
|
|
185
|
+
expect(onBucketClick.mock.calls.length).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("a click after a double-click still works", () => {
|
|
189
|
+
const onBucketClick: MockFunction = getJestMockFunction();
|
|
190
|
+
const onTimeRangeReset: MockFunction = getJestMockFunction();
|
|
191
|
+
renderChart({ onBucketClick, onTimeRangeReset });
|
|
192
|
+
|
|
193
|
+
clickBucket();
|
|
194
|
+
clickBucket();
|
|
195
|
+
doubleClick();
|
|
196
|
+
advancePastTheDoubleClickWindow();
|
|
197
|
+
|
|
198
|
+
clickBucket();
|
|
199
|
+
advancePastTheDoubleClickWindow();
|
|
200
|
+
|
|
201
|
+
expect(onBucketClick.mock.calls.length).toBe(1);
|
|
202
|
+
expect(onTimeRangeReset.mock.calls.length).toBe(1);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("unmounting mid-gesture drops the pending click instead of firing it", () => {
|
|
206
|
+
const onBucketClick: MockFunction = getJestMockFunction();
|
|
207
|
+
const onTimeRangeReset: MockFunction = getJestMockFunction();
|
|
208
|
+
renderChart({ onBucketClick, onTimeRangeReset });
|
|
209
|
+
|
|
210
|
+
clickBucket();
|
|
211
|
+
cleanup();
|
|
212
|
+
advancePastTheDoubleClickWindow();
|
|
213
|
+
|
|
214
|
+
/*
|
|
215
|
+
* A widget can be unmounted by the very reset the user is performing
|
|
216
|
+
* (the board re-queries and re-renders); a timer that outlives it would
|
|
217
|
+
* call back into a dead component.
|
|
218
|
+
*/
|
|
219
|
+
expect(onBucketClick.mock.calls.length).toBe(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
3
|
+
import {
|
|
4
|
+
RenderResult,
|
|
5
|
+
cleanup,
|
|
6
|
+
fireEvent,
|
|
7
|
+
render,
|
|
8
|
+
} from "@testing-library/react";
|
|
9
|
+
import React from "react";
|
|
10
|
+
import getJestMockFunction, { MockFunction } from "../../../MockType";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* ResponsiveContainer measures its parent, which is always 0x0 in jsdom, so
|
|
14
|
+
* the chart renders nothing — including no `.recharts-wrapper` for the
|
|
15
|
+
* events below to land on — without a fixed size. Same shim the other chart
|
|
16
|
+
* suites use.
|
|
17
|
+
*/
|
|
18
|
+
jest.mock("recharts", () => {
|
|
19
|
+
const actual: Record<string, any> = jest.requireActual("recharts");
|
|
20
|
+
return {
|
|
21
|
+
...actual,
|
|
22
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
23
|
+
return React.cloneElement(children, { width: 600, height: 300 });
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
import AreaChartElement from "../../../../UI/Components/Charts/Area/AreaChart";
|
|
29
|
+
import BarChartElement from "../../../../UI/Components/Charts/Bar/BarChart";
|
|
30
|
+
import LineChartElement from "../../../../UI/Components/Charts/Line/LineChart";
|
|
31
|
+
import ChartCurve from "../../../../UI/Components/Charts/Types/ChartCurve";
|
|
32
|
+
import DataPoint from "../../../../UI/Components/Charts/Types/DataPoint";
|
|
33
|
+
import SeriesPoint from "../../../../UI/Components/Charts/Types/SeriesPoints";
|
|
34
|
+
import {
|
|
35
|
+
XAxis as ChartXAxis,
|
|
36
|
+
XAxisAggregateType,
|
|
37
|
+
} from "../../../../UI/Components/Charts/Types/XAxis/XAxis";
|
|
38
|
+
import XAxisType from "../../../../UI/Components/Charts/Types/XAxis/XAxisType";
|
|
39
|
+
import YAxis, {
|
|
40
|
+
YAxisPrecision,
|
|
41
|
+
} from "../../../../UI/Components/Charts/Types/YAxis/YAxis";
|
|
42
|
+
import YAxisType from "../../../../UI/Components/Charts/Types/YAxis/YAxisType";
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Drag-to-select on a dashboard panel now retimes the WHOLE dashboard, so
|
|
46
|
+
* every panel needs a way back out of the zoom that does not depend on
|
|
47
|
+
* finding a particular widget: double-clicking any of them.
|
|
48
|
+
*
|
|
49
|
+
* That gesture is wired at the chart library, not at the widget card, for
|
|
50
|
+
* two reasons pinned here: it must reach recharts' own plot surface (the
|
|
51
|
+
* card sits under the edit overlay and the resize handles), and it must be
|
|
52
|
+
* OPT-IN — a chart handed no reset handler has to behave exactly as it did
|
|
53
|
+
* before, because a double-click is also two plain clicks and those still
|
|
54
|
+
* mean "pin this bucket" everywhere else in the product.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
const START: Date = new Date("2026-08-10T00:00:00.000Z");
|
|
58
|
+
const END: Date = new Date("2026-08-10T01:00:00.000Z");
|
|
59
|
+
|
|
60
|
+
function buildPoints(): Array<DataPoint> {
|
|
61
|
+
const points: Array<DataPoint> = [];
|
|
62
|
+
for (let index: number = 0; index < 10; index++) {
|
|
63
|
+
points.push({
|
|
64
|
+
x: new Date(START.getTime() + index * 60 * 1000),
|
|
65
|
+
y: 90 + index,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return points;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildSeries(): Array<SeriesPoint> {
|
|
72
|
+
return [{ seriesName: "CPU", data: buildPoints() }];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const X_AXIS: ChartXAxis = {
|
|
76
|
+
legend: "Time",
|
|
77
|
+
options: {
|
|
78
|
+
type: XAxisType.Time,
|
|
79
|
+
min: START,
|
|
80
|
+
max: END,
|
|
81
|
+
aggregateType: XAxisAggregateType.Average,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const Y_AXIS: YAxis = {
|
|
86
|
+
legend: "%",
|
|
87
|
+
options: {
|
|
88
|
+
type: YAxisType.Number,
|
|
89
|
+
min: "auto",
|
|
90
|
+
max: 100,
|
|
91
|
+
precision: YAxisPrecision.TwoDecimals,
|
|
92
|
+
formatter: (value: number): string => {
|
|
93
|
+
return `${value}%`;
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
interface ChartUnderTest {
|
|
99
|
+
name: string;
|
|
100
|
+
render: (onTimeRangeReset: (() => void) | undefined) => React.ReactElement;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const CHARTS: Array<ChartUnderTest> = [
|
|
104
|
+
{
|
|
105
|
+
name: "LineChart",
|
|
106
|
+
render: (
|
|
107
|
+
onTimeRangeReset: (() => void) | undefined,
|
|
108
|
+
): React.ReactElement => {
|
|
109
|
+
return (
|
|
110
|
+
<LineChartElement
|
|
111
|
+
data={buildSeries()}
|
|
112
|
+
xAxis={X_AXIS}
|
|
113
|
+
yAxis={Y_AXIS}
|
|
114
|
+
curve={ChartCurve.MONOTONE}
|
|
115
|
+
heightInPx={300}
|
|
116
|
+
showLegend={false}
|
|
117
|
+
sync={false}
|
|
118
|
+
syncid="double-click-reset-test"
|
|
119
|
+
onTimeRangeReset={onTimeRangeReset}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "AreaChart",
|
|
126
|
+
render: (
|
|
127
|
+
onTimeRangeReset: (() => void) | undefined,
|
|
128
|
+
): React.ReactElement => {
|
|
129
|
+
return (
|
|
130
|
+
<AreaChartElement
|
|
131
|
+
data={buildSeries()}
|
|
132
|
+
xAxis={X_AXIS}
|
|
133
|
+
yAxis={Y_AXIS}
|
|
134
|
+
curve={ChartCurve.MONOTONE}
|
|
135
|
+
heightInPx={300}
|
|
136
|
+
showLegend={false}
|
|
137
|
+
sync={false}
|
|
138
|
+
syncid="double-click-reset-test"
|
|
139
|
+
onTimeRangeReset={onTimeRangeReset}
|
|
140
|
+
/>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
/*
|
|
146
|
+
* A bar panel cannot ORIGINATE a zoom — it has no drag-to-select — but
|
|
147
|
+
* it must still be able to end one, or a board whose only panel under
|
|
148
|
+
* the pointer is a bar chart would be a dead end.
|
|
149
|
+
*/
|
|
150
|
+
name: "BarChart",
|
|
151
|
+
render: (
|
|
152
|
+
onTimeRangeReset: (() => void) | undefined,
|
|
153
|
+
): React.ReactElement => {
|
|
154
|
+
return (
|
|
155
|
+
<BarChartElement
|
|
156
|
+
data={buildSeries()}
|
|
157
|
+
xAxis={X_AXIS}
|
|
158
|
+
yAxis={Y_AXIS}
|
|
159
|
+
heightInPx={300}
|
|
160
|
+
showLegend={false}
|
|
161
|
+
sync={false}
|
|
162
|
+
syncid="double-click-reset-test"
|
|
163
|
+
onTimeRangeReset={onTimeRangeReset}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
function plotSurface(rendered: RenderResult): Element {
|
|
171
|
+
const surface: Element | null =
|
|
172
|
+
rendered.container.querySelector(".recharts-wrapper");
|
|
173
|
+
if (!surface) {
|
|
174
|
+
throw new Error("chart did not render a plot surface");
|
|
175
|
+
}
|
|
176
|
+
return surface;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
cleanup();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("chart double-click to reset the time range", () => {
|
|
184
|
+
for (const chart of CHARTS) {
|
|
185
|
+
describe(chart.name, () => {
|
|
186
|
+
test("double-clicking the plot calls back exactly once", () => {
|
|
187
|
+
const onTimeRangeReset: MockFunction = getJestMockFunction();
|
|
188
|
+
const rendered: RenderResult = render(
|
|
189
|
+
chart.render(onTimeRangeReset as unknown as () => void),
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
fireEvent.doubleClick(plotSurface(rendered));
|
|
193
|
+
|
|
194
|
+
expect(onTimeRangeReset.mock.calls.length).toBe(1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a plain click is not a reset", () => {
|
|
198
|
+
const onTimeRangeReset: MockFunction = getJestMockFunction();
|
|
199
|
+
const rendered: RenderResult = render(
|
|
200
|
+
chart.render(onTimeRangeReset as unknown as () => void),
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
fireEvent.click(plotSurface(rendered));
|
|
204
|
+
|
|
205
|
+
expect(onTimeRangeReset.mock.calls.length).toBe(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("a chart with no reset handler still renders and swallows the gesture", () => {
|
|
209
|
+
const rendered: RenderResult = render(chart.render(undefined));
|
|
210
|
+
|
|
211
|
+
// No handler wired, so this must be inert rather than throwing.
|
|
212
|
+
fireEvent.doubleClick(plotSurface(rendered));
|
|
213
|
+
|
|
214
|
+
expect(
|
|
215
|
+
rendered.container.querySelector(".recharts-wrapper"),
|
|
216
|
+
).not.toBeNull();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|