@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.
Files changed (188) hide show
  1. package/Models/DatabaseModels/AlertState.ts +0 -2
  2. package/Models/DatabaseModels/Domain.ts +1 -1
  3. package/Models/DatabaseModels/Incident.ts +1 -1
  4. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/MetricType.ts +0 -2
  7. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  8. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  9. package/Models/DatabaseModels/NetworkDevice.ts +121 -2
  10. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
  11. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  13. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  14. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  15. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  16. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  17. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  18. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  21. package/Server/Services/DatabaseService.ts +39 -9
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  24. package/Server/Services/ProjectService.ts +85 -0
  25. package/Server/Services/UserNotificationSettingService.ts +0 -6
  26. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
  27. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  28. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  29. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  30. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  31. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  32. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
  33. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  34. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
  35. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  36. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  37. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  38. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  39. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  40. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  41. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  42. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
  43. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  44. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  45. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  46. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  47. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  48. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  49. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  50. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  51. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  52. package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
  53. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  54. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  55. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  56. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  57. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  58. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  59. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  60. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  61. package/Tests/Utils/Slug.test.ts +61 -1
  62. package/Tests/jest.setup.ts +19 -0
  63. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  64. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  65. package/Types/Permission.ts +45 -0
  66. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  67. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  68. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  69. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
  70. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
  71. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
  72. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  73. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  74. package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
  75. package/UI/Components/Navbar/NavBar.tsx +62 -13
  76. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  77. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  78. package/UI/Components/SideMenu/SideMenu.tsx +12 -2
  79. package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
  80. package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
  81. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  82. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  83. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
  86. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  87. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  88. package/Utils/Slug.ts +34 -1
  89. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  90. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  92. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  94. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  96. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  100. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  102. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  104. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  105. package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
  106. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  107. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
  108. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
  109. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  110. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  111. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  112. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  114. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  116. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  118. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  120. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  122. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  124. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  129. package/build/dist/Server/Services/DatabaseService.js +31 -5
  130. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  131. package/build/dist/Server/Services/Index.js +2 -0
  132. package/build/dist/Server/Services/Index.js.map +1 -1
  133. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  134. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  135. package/build/dist/Server/Services/ProjectService.js +65 -0
  136. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
  138. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  139. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  140. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  141. package/build/dist/Types/Permission.js +40 -0
  142. package/build/dist/Types/Permission.js.map +1 -1
  143. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  144. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  145. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  146. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  147. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  148. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  149. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
  150. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  151. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
  152. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  153. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
  154. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  155. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  156. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  157. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  158. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  159. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
  160. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  161. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  162. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  163. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  164. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  165. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  166. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  167. package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
  168. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  169. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
  170. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  171. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
  172. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  173. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  174. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  175. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  176. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  178. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  179. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  181. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  183. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  184. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  185. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  186. package/build/dist/Utils/Slug.js +31 -1
  187. package/build/dist/Utils/Slug.js.map +1 -1
  188. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  afterEach,
3
+ beforeAll,
3
4
  beforeEach,
4
5
  describe,
5
6
  expect,
@@ -70,6 +71,12 @@ jest.mock("../../../UI/Utils/Navigation", () => {
70
71
  return {
71
72
  __esModule: true,
72
73
  default: {
74
+ isOnThisPage: () => {
75
+ return false;
76
+ },
77
+ isStartWith: () => {
78
+ return false;
79
+ },
73
80
  navigate: (...args: Array<unknown>) => {
74
81
  return navigateMock(...args);
75
82
  },
@@ -112,8 +119,8 @@ jest.mock("../../../UI/Utils/Project", () => {
112
119
 
113
120
  /*
114
121
  * The real catalog hook walks the full RouteMap through useTranslation; the
115
- * host only needs SOME catalog, so a single-page one keeps the suite about
116
- * the host's event wiring rather than the NavBar's contents.
122
+ * host and dashboard NavBar only need a compact catalog, so one page plus one
123
+ * product keeps the suite about shortcut ownership rather than catalog data.
117
124
  */
118
125
  jest.mock(
119
126
  "../../../../App/FeatureSet/Dashboard/src/Utils/NavigationItems",
@@ -137,7 +144,16 @@ jest.mock(
137
144
  route: new RouteClass("/dashboard/abc123/home"),
138
145
  },
139
146
  ],
140
- moreMenuItems: [],
147
+ moreMenuItems: [
148
+ {
149
+ title: "Monitors",
150
+ description: "Monitor anything that can go down.",
151
+ icon: "AltGlobe",
152
+ iconColor: "blue",
153
+ category: "Essentials",
154
+ route: new RouteClass("/dashboard/abc123/monitors"),
155
+ },
156
+ ],
141
157
  rightElement: {
142
158
  id: "user-profile-nav-bar-item",
143
159
  title: "User Profile",
@@ -158,6 +174,7 @@ jest.mock(
158
174
  );
159
175
 
160
176
  import DashboardCommandPalette from "../../../../App/FeatureSet/Dashboard/src/Components/CommandPalette/DashboardCommandPalette";
177
+ import DashboardNavbar from "../../../../App/FeatureSet/Dashboard/src/Components/NavBar/NavBar";
161
178
  import EventName from "../../../../App/FeatureSet/Dashboard/src/Utils/EventName";
162
179
  import GlobalEvents from "../../../UI/Utils/GlobalEvents";
163
180
 
@@ -165,6 +182,19 @@ function palette(): HTMLElement | null {
165
182
  return screen.queryByTestId("command-palette");
166
183
  }
167
184
 
185
+ function productsMenu(): HTMLElement | null {
186
+ return screen.queryByRole("dialog", { name: "Products menu" });
187
+ }
188
+
189
+ function renderDashboardChrome(): void {
190
+ render(
191
+ <>
192
+ <DashboardNavbar show={true} />
193
+ <DashboardCommandPalette />
194
+ </>,
195
+ );
196
+ }
197
+
168
198
  /*
169
199
  * The chord is dispatched on document.body — where real key events land when
170
200
  * no input is focused — and bubbles up to the host's document-level listener.
@@ -173,6 +203,11 @@ function pressChord(init: KeyboardEventInit): void {
173
203
  fireEvent.keyDown(document.body, init);
174
204
  }
175
205
 
206
+ beforeAll(() => {
207
+ // The products menu keeps its keyboard selection in view when it opens.
208
+ Element.prototype.scrollIntoView = (): void => {};
209
+ });
210
+
176
211
  beforeEach(() => {
177
212
  /*
178
213
  * The open panel schedules an enter-transition frame (rAF + fallback
@@ -264,6 +299,75 @@ describe("DashboardCommandPalette Cmd/Ctrl+K chord", () => {
264
299
  });
265
300
  });
266
301
 
302
+ describe("dashboard Cmd/Ctrl+K ownership with the products menu mounted", () => {
303
+ test.each([
304
+ ["Cmd+K", { key: "k", metaKey: true }],
305
+ ["Ctrl+K", { key: "k", ctrlKey: true }],
306
+ ] as Array<[string, KeyboardEventInit]>)(
307
+ "%s opens Search only",
308
+ (_label: string, chord: KeyboardEventInit) => {
309
+ renderDashboardChrome();
310
+
311
+ pressChord(chord);
312
+
313
+ expect(palette()).toBeInTheDocument();
314
+ expect(productsMenu()).not.toBeInTheDocument();
315
+ expect(screen.getAllByRole("dialog")).toHaveLength(1);
316
+ },
317
+ );
318
+
319
+ test("the Products button still opens Products without advertising Cmd/Ctrl+K", () => {
320
+ renderDashboardChrome();
321
+
322
+ fireEvent.click(screen.getByRole("button", { name: /Products/i }));
323
+
324
+ const dialog: HTMLElement = screen.getByRole("dialog", {
325
+ name: "Products menu",
326
+ });
327
+ expect(palette()).not.toBeInTheDocument();
328
+ expect(screen.getAllByRole("dialog")).toHaveLength(1);
329
+
330
+ /*
331
+ * The dashboard surrendered this chord to Search, so Products must not
332
+ * leave either the visible keycaps or their screen-reader label behind.
333
+ */
334
+ const commandKKeycaps: HTMLElement[] = Array.from(
335
+ dialog.querySelectorAll<HTMLElement>("kbd"),
336
+ ).filter((keycap: HTMLElement): boolean => {
337
+ return keycap.textContent === "K";
338
+ });
339
+ expect(commandKKeycaps).toHaveLength(0);
340
+ expect(dialog).not.toHaveTextContent(/(?:Command|Control) \+ K/);
341
+ });
342
+
343
+ test.each([
344
+ ["Cmd+K", { key: "k", metaKey: true }],
345
+ ["Ctrl+K", { key: "k", ctrlKey: true }],
346
+ ] as Array<[string, KeyboardEventInit]>)(
347
+ "%s replaces an open Products dialog with one Search dialog",
348
+ (_label: string, chord: KeyboardEventInit) => {
349
+ renderDashboardChrome();
350
+ fireEvent.click(screen.getByRole("button", { name: /Products/i }));
351
+
352
+ const productsDialog: HTMLElement = screen.getByRole("dialog", {
353
+ name: "Products menu",
354
+ });
355
+ const productsSearch: HTMLElement = screen.getByRole("combobox", {
356
+ name: "navbar.search.placeholder",
357
+ });
358
+ expect(productsDialog).toBeInTheDocument();
359
+ expect(document.activeElement).toBe(productsSearch);
360
+
361
+ // Real keystrokes originate in the focused Products search input.
362
+ fireEvent.keyDown(productsSearch, chord);
363
+
364
+ expect(productsMenu()).not.toBeInTheDocument();
365
+ expect(palette()).toBeInTheDocument();
366
+ expect(screen.getAllByRole("dialog")).toHaveLength(1);
367
+ },
368
+ );
369
+ });
370
+
267
371
  describe("DashboardCommandPalette global toggle event", () => {
268
372
  test("the COMMAND_PALETTE_TOGGLE event toggles the palette", () => {
269
373
  render(<DashboardCommandPalette />);
@@ -0,0 +1,401 @@
1
+ import "@testing-library/jest-dom";
2
+ import {
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ jest,
8
+ test,
9
+ } from "@jest/globals";
10
+ import {
11
+ RenderResult,
12
+ act,
13
+ cleanup,
14
+ fireEvent,
15
+ render,
16
+ screen,
17
+ waitFor,
18
+ } from "@testing-library/react";
19
+ import * as React from "react";
20
+ import getJestMockFunction, { MockFunction } from "../../MockType";
21
+
22
+ /*
23
+ * Issue #3530: drag-selecting a window on a dashboard time-series panel is a
24
+ * statement about the whole board, not about that one panel — the same way
25
+ * filtering works in Traces and Logs. The widget therefore hands the
26
+ * selected window UP to the dashboard shell and lets the new board-wide
27
+ * range come back down as a prop, rather than narrowing itself and leaving
28
+ * every neighbouring panel showing a different hour.
29
+ *
30
+ * Double-clicking a panel is the way back out, and it must be inert while
31
+ * there is nothing to undo.
32
+ *
33
+ * The widget-local fallback (no shell callbacks) is a separate contract and
34
+ * is covered by DashboardChartWidgetZoom.test.tsx.
35
+ */
36
+
37
+ const fetchResultsMock: MockFunction = getJestMockFunction();
38
+ const metricChartsRenderMock: MockFunction = getJestMockFunction();
39
+ const investigationDrawerRenderMock: MockFunction = getJestMockFunction();
40
+
41
+ /*
42
+ * The arrow wrappers are load bearing: jest.mock is hoisted above the
43
+ * compiled requires, so the mock variables above are still unassigned when
44
+ * the factory runs. Dereferencing them lazily, at call time, is what makes
45
+ * this work.
46
+ */
47
+ jest.mock(
48
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
49
+ () => {
50
+ return {
51
+ __esModule: true,
52
+ default: {
53
+ fetchResults: (...args: Array<any>) => {
54
+ return fetchResultsMock(...args);
55
+ },
56
+ clearQueryTopNOverridesForScope: () => {
57
+ return undefined;
58
+ },
59
+ },
60
+ };
61
+ },
62
+ );
63
+
64
+ /*
65
+ * The widget's job under test is which window it ASKS for and which
66
+ * affordances it hands down; a real recharts surface in jsdom would test
67
+ * neither.
68
+ */
69
+ jest.mock(
70
+ "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts",
71
+ () => {
72
+ return {
73
+ __esModule: true,
74
+ default: (props: Record<string, unknown>): React.ReactElement => {
75
+ metricChartsRenderMock(props);
76
+ return React.createElement(
77
+ "div",
78
+ { "data-testid": "metric-charts" },
79
+ "chart",
80
+ );
81
+ },
82
+ };
83
+ },
84
+ );
85
+
86
+ // The drawer itself pulls in the whole telemetry stack; only its mounting matters here.
87
+ jest.mock(
88
+ "../../../../App/FeatureSet/Dashboard/src/Components/Telemetry/InvestigationDrawer",
89
+ () => {
90
+ return {
91
+ __esModule: true,
92
+ default: (props: Record<string, unknown>): React.ReactElement => {
93
+ investigationDrawerRenderMock(props);
94
+ return React.createElement(
95
+ "div",
96
+ { "data-testid": "investigation-drawer" },
97
+ "drawer",
98
+ );
99
+ },
100
+ };
101
+ },
102
+ );
103
+
104
+ import DashboardChartComponentElement from "../../../../App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardChartComponent";
105
+ import { DashboardBaseComponentProps } from "../../../../App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardBaseComponent";
106
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
107
+ import DashboardChartType from "../../../Types/Dashboard/Chart/ChartType";
108
+ import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
109
+ import DashboardChartComponent from "../../../Types/Dashboard/DashboardComponents/DashboardChartComponent";
110
+ import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
111
+ import { ObjectType } from "../../../Types/JSON";
112
+ import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
113
+ import MetricViewData from "../../../Types/Metrics/MetricViewData";
114
+ import ObjectID from "../../../Types/ObjectID";
115
+ import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
116
+ import TimeRange from "../../../Types/Time/TimeRange";
117
+
118
+ const COMPONENT_ID: ObjectID = new ObjectID(
119
+ "33333333-1111-4111-8111-333333333333",
120
+ );
121
+
122
+ const BOARD_START: Date = new Date("2026-08-20T09:00:00.000Z");
123
+ const BOARD_END: Date = new Date("2026-08-20T10:00:00.000Z");
124
+ const DRAG_START: Date = new Date("2026-08-20T09:20:00.000Z");
125
+ const DRAG_END: Date = new Date("2026-08-20T09:35:00.000Z");
126
+
127
+ /*
128
+ * A CUSTOM range pins the window to fixed instants — a relative range
129
+ * would resolve against the wall clock and make the window assertions
130
+ * untestable.
131
+ */
132
+ const BOARD_RANGE: RangeStartAndEndDateTime = {
133
+ range: TimeRange.CUSTOM,
134
+ startAndEndDate: new InBetween<Date>(BOARD_START, BOARD_END),
135
+ };
136
+
137
+ /** What the shell hands back down once the drag has retimed the board. */
138
+ const ZOOMED_BOARD_RANGE: RangeStartAndEndDateTime = {
139
+ range: TimeRange.CUSTOM,
140
+ startAndEndDate: new InBetween<Date>(DRAG_START, DRAG_END),
141
+ };
142
+
143
+ const DASHBOARD_VIEW_CONFIG: DashboardViewConfig = {
144
+ _type: ObjectType.DashboardViewConfig,
145
+ components: [],
146
+ heightInDashboardUnits: 60,
147
+ };
148
+
149
+ let onDashboardTimeRangeSelectMock: MockFunction;
150
+ let onDashboardTimeRangeResetMock: MockFunction;
151
+
152
+ function buildBaseProps(
153
+ overrides: Partial<DashboardBaseComponentProps> = {},
154
+ ): DashboardBaseComponentProps {
155
+ return {
156
+ componentId: COMPONENT_ID,
157
+ isEditMode: false,
158
+ isSelected: false,
159
+ key: "chart-widget",
160
+ onComponentUpdate: (): void => {
161
+ // The widget under test never writes back through this.
162
+ },
163
+ totalCurrentDashboardWidthInPx: 1200,
164
+ dashboardCanvasTopInPx: 0,
165
+ dashboardCanvasLeftInPx: 0,
166
+ dashboardCanvasWidthInPx: 1200,
167
+ dashboardCanvasHeightInPx: 800,
168
+ dashboardComponentHeightInPx: 320,
169
+ dashboardComponentWidthInPx: 480,
170
+ dashboardViewConfig: DASHBOARD_VIEW_CONFIG,
171
+ dashboardStartAndEndDate: BOARD_RANGE,
172
+ metricTypes: [],
173
+ refreshTick: 0,
174
+ variables: undefined,
175
+ onDashboardTimeRangeSelect: onDashboardTimeRangeSelectMock as unknown as (
176
+ startTime: Date,
177
+ endTime: Date,
178
+ ) => void,
179
+ onDashboardTimeRangeReset:
180
+ onDashboardTimeRangeResetMock as unknown as () => void,
181
+ isDashboardTimeRangeZoomed: false,
182
+ ...overrides,
183
+ } as DashboardBaseComponentProps;
184
+ }
185
+
186
+ function buildChartComponent(): DashboardChartComponent {
187
+ return {
188
+ _type: ObjectType.DashboardComponent,
189
+ componentId: COMPONENT_ID,
190
+ componentType: DashboardComponentType.Chart,
191
+ topInDashboardUnits: 0,
192
+ leftInDashboardUnits: 0,
193
+ widthInDashboardUnits: 6,
194
+ heightInDashboardUnits: 3,
195
+ minWidthInDashboardUnits: 6,
196
+ minHeightInDashboardUnits: 3,
197
+ arguments: {
198
+ chartTitle: "CPU by host",
199
+ chartType: DashboardChartType.Line,
200
+ metricQueryConfig: {
201
+ metricAliasData: { metricVariable: "a" },
202
+ metricQueryData: {
203
+ filterData: {
204
+ metricName: "cpu.usage",
205
+ aggegationType: MetricsAggregationType.Avg,
206
+ },
207
+ groupByAttributeKeys: ["host.name"],
208
+ },
209
+ },
210
+ },
211
+ } as unknown as DashboardChartComponent;
212
+ }
213
+
214
+ function renderWidget(
215
+ overrides: Partial<DashboardBaseComponentProps> = {},
216
+ ): RenderResult {
217
+ return render(
218
+ <DashboardChartComponentElement
219
+ {...buildBaseProps(overrides)}
220
+ component={buildChartComponent()}
221
+ />,
222
+ );
223
+ }
224
+
225
+ interface CapturedChartProps {
226
+ metricViewData: MetricViewData;
227
+ onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
228
+ onTimeRangeReset?: (() => void) | undefined;
229
+ }
230
+
231
+ function lastChartProps(): CapturedChartProps {
232
+ const calls: Array<Array<unknown>> = metricChartsRenderMock.mock.calls;
233
+ return calls[calls.length - 1]?.[0] as CapturedChartProps;
234
+ }
235
+
236
+ function lastFetchWindow(): InBetween<Date> | null | undefined {
237
+ const calls: Array<Array<unknown>> = fetchResultsMock.mock.calls;
238
+ return (calls[calls.length - 1]?.[0] as { metricViewData: MetricViewData })
239
+ ?.metricViewData?.startAndEndDate;
240
+ }
241
+
242
+ beforeEach(() => {
243
+ onDashboardTimeRangeSelectMock = getJestMockFunction();
244
+ onDashboardTimeRangeResetMock = getJestMockFunction();
245
+ fetchResultsMock.mockReset();
246
+ metricChartsRenderMock.mockReset();
247
+ investigationDrawerRenderMock.mockReset();
248
+ fetchResultsMock.mockReturnValue(
249
+ Promise.resolve([{ data: [], truncated: false }]),
250
+ );
251
+ });
252
+
253
+ afterEach(() => {
254
+ cleanup();
255
+ });
256
+
257
+ describe("dashboard-wide drag-to-zoom", () => {
258
+ test("a drag-selection is handed to the dashboard, not applied to this panel alone", async () => {
259
+ renderWidget();
260
+
261
+ await waitFor(() => {
262
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
263
+ });
264
+
265
+ expect(lastFetchWindow()?.startValue).toEqual(BOARD_START);
266
+ const fetchCountBeforeDrag: number = fetchResultsMock.mock.calls.length;
267
+
268
+ act(() => {
269
+ lastChartProps().onTimeRangeSelect?.(DRAG_START, DRAG_END);
270
+ });
271
+
272
+ expect(onDashboardTimeRangeSelectMock.mock.calls.length).toBe(1);
273
+ expect(onDashboardTimeRangeSelectMock.mock.calls[0]).toEqual([
274
+ DRAG_START,
275
+ DRAG_END,
276
+ ]);
277
+
278
+ /*
279
+ * The widget must NOT retime itself off its own bat — the shell owns
280
+ * the range, and a panel that jumped ahead of it would be showing a
281
+ * different window from every one of its neighbours until the new
282
+ * range came back down.
283
+ */
284
+ await act(async () => {
285
+ await Promise.resolve();
286
+ });
287
+ expect(fetchResultsMock.mock.calls.length).toBe(fetchCountBeforeDrag);
288
+ expect(lastFetchWindow()?.startValue).toEqual(BOARD_START);
289
+ // …and no widget-local "Zoomed" pill appears either.
290
+ expect(screen.queryByText(/Zoomed:/)).toBeNull();
291
+ });
292
+
293
+ test("the new board range coming back down is what actually refetches the panel", async () => {
294
+ const rendered: RenderResult = renderWidget();
295
+
296
+ await waitFor(() => {
297
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
298
+ });
299
+
300
+ act(() => {
301
+ lastChartProps().onTimeRangeSelect?.(DRAG_START, DRAG_END);
302
+ });
303
+
304
+ // The shell applies the zoom and re-renders every widget with it.
305
+ rendered.rerender(
306
+ <DashboardChartComponentElement
307
+ {...buildBaseProps({
308
+ dashboardStartAndEndDate: ZOOMED_BOARD_RANGE,
309
+ isDashboardTimeRangeZoomed: true,
310
+ })}
311
+ component={buildChartComponent()}
312
+ />,
313
+ );
314
+
315
+ await waitFor(() => {
316
+ expect(lastFetchWindow()?.startValue).toEqual(DRAG_START);
317
+ });
318
+ expect(lastFetchWindow()?.endValue).toEqual(DRAG_END);
319
+ });
320
+
321
+ test("double-click-to-reset is only offered once the board is actually zoomed", async () => {
322
+ const rendered: RenderResult = renderWidget();
323
+
324
+ await waitFor(() => {
325
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
326
+ });
327
+
328
+ /*
329
+ * Nothing to undo yet. Withholding the handler is not cosmetic: the
330
+ * chart library delays every plain click while a reset handler is
331
+ * present, and a bucket click must stay instant on an unzoomed board.
332
+ */
333
+ expect(lastChartProps().onTimeRangeReset).toBeUndefined();
334
+
335
+ rendered.rerender(
336
+ <DashboardChartComponentElement
337
+ {...buildBaseProps({
338
+ dashboardStartAndEndDate: ZOOMED_BOARD_RANGE,
339
+ isDashboardTimeRangeZoomed: true,
340
+ })}
341
+ component={buildChartComponent()}
342
+ />,
343
+ );
344
+
345
+ await waitFor(() => {
346
+ expect(lastChartProps().onTimeRangeReset).toBeDefined();
347
+ });
348
+
349
+ act(() => {
350
+ lastChartProps().onTimeRangeReset?.();
351
+ });
352
+
353
+ expect(onDashboardTimeRangeResetMock.mock.calls.length).toBe(1);
354
+ });
355
+
356
+ test("edit mode offers neither zoom nor reset", async () => {
357
+ renderWidget({ isEditMode: true, isDashboardTimeRangeZoomed: true });
358
+
359
+ await waitFor(() => {
360
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
361
+ });
362
+
363
+ /*
364
+ * In edit mode the drag gesture belongs to widget move/resize, and a
365
+ * board-wide retime mid-edit would be a surprise on top of unsaved
366
+ * layout changes.
367
+ */
368
+ expect(lastChartProps().onTimeRangeSelect).toBeUndefined();
369
+ expect(lastChartProps().onTimeRangeReset).toBeUndefined();
370
+ });
371
+
372
+ test("Investigate stays reachable from the panel header, on the window it is charting", async () => {
373
+ renderWidget({
374
+ dashboardStartAndEndDate: ZOOMED_BOARD_RANGE,
375
+ isDashboardTimeRangeZoomed: true,
376
+ });
377
+
378
+ await waitFor(() => {
379
+ expect(screen.getByTestId("metric-charts")).toBeInTheDocument();
380
+ });
381
+
382
+ /*
383
+ * The old entry point lived inside the widget-local "Zoomed" pill,
384
+ * which no longer renders on a real dashboard now that the shell owns
385
+ * the drag. It has to live somewhere that always shows.
386
+ */
387
+ expect(screen.queryByText(/Zoomed:/)).toBeNull();
388
+
389
+ fireEvent.click(
390
+ screen.getByLabelText("Investigate this time window in a side panel"),
391
+ );
392
+
393
+ expect(screen.getByTestId("investigation-drawer")).toBeInTheDocument();
394
+ const drawerProps: { window?: InBetween<Date> | undefined } =
395
+ investigationDrawerRenderMock.mock.calls[
396
+ investigationDrawerRenderMock.mock.calls.length - 1
397
+ ]?.[0] as { window?: InBetween<Date> | undefined };
398
+ expect(drawerProps.window?.startValue).toEqual(DRAG_START);
399
+ expect(drawerProps.window?.endValue).toEqual(DRAG_END);
400
+ });
401
+ });