@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,10 +1,12 @@
1
1
  /*
2
2
  * The dashboard's command palette owns Cmd/Ctrl+K, so the NavBar grew a
3
- * `disableCommandKShortcut` prop that stops it registering its own document
4
- * keydown for the products menu. These tests pin the contract in BOTH
5
- * directions: with the prop the menu must NOT open on Cmd+K (or the palette
6
- * and the menu would double-fire on one keystroke), and without the prop the
7
- * legacy shortcut must keep working for every other NavBar consumer.
3
+ * `disableCommandKShortcut` prop that surrenders the chord to the dashboard
4
+ * command palette. These tests pin the contract in BOTH directions: with the
5
+ * prop the menu must NOT open or advertise Cmd+K (or the two surfaces would
6
+ * claim the same shortcut), and without the prop the legacy shortcut must
7
+ * keep working for every other NavBar consumer. An already-open products menu
8
+ * still listens just long enough to close without consuming the palette's
9
+ * keydown, preventing two modal dialogs from being stacked.
8
10
  */
9
11
 
10
12
  /*
@@ -98,6 +100,10 @@ function pressCommandK(): void {
98
100
  fireEvent.keyDown(document, { key: "k", metaKey: true });
99
101
  }
100
102
 
103
+ function commandKHint(): HTMLElement | null {
104
+ return screen.queryByText(/^(Command|Control) \+ K$/);
105
+ }
106
+
101
107
  describe("NavBar Cmd/Ctrl+K gating", () => {
102
108
  afterEach(() => {
103
109
  cleanup();
@@ -110,6 +116,7 @@ describe("NavBar Cmd/Ctrl+K gating", () => {
110
116
 
111
117
  pressCommandK();
112
118
  expect(screen.getByRole("dialog")).toBeInTheDocument();
119
+ expect(commandKHint()).toBeInTheDocument();
113
120
 
114
121
  // And the same chord closes it again — it is a toggle, not just an opener.
115
122
  pressCommandK();
@@ -138,7 +145,7 @@ describe("NavBar Cmd/Ctrl+K gating", () => {
138
145
  expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
139
146
  });
140
147
 
141
- test("with disableCommandKShortcut, the menu still opens from its trigger button", () => {
148
+ test("with disableCommandKShortcut, the menu still opens from its trigger button without advertising Cmd/Ctrl+K", () => {
142
149
  /*
143
150
  * Disabling the shortcut must not disable the menu: the button path is the
144
151
  * remaining way in, so it has to keep working.
@@ -153,6 +160,94 @@ describe("NavBar Cmd/Ctrl+K gating", () => {
153
160
 
154
161
  fireEvent.click(screen.getByText("Products"));
155
162
  expect(screen.getByRole("dialog")).toBeInTheDocument();
163
+ expect(commandKHint()).not.toBeInTheDocument();
164
+ });
165
+
166
+ test("with disableCommandKShortcut, Cmd+K closes an already-open products menu without consuming the palette's event", () => {
167
+ render(
168
+ <Navbar
169
+ items={[homeItem]}
170
+ moreMenuItems={moreMenuItems}
171
+ disableCommandKShortcut={true}
172
+ />,
173
+ );
174
+
175
+ fireEvent.click(screen.getByText("Products"));
176
+ expect(screen.getByRole("dialog", { name: "Products menu" })).toBeVisible();
177
+
178
+ const defaultAllowed: boolean = fireEvent.keyDown(document, {
179
+ key: "k",
180
+ metaKey: true,
181
+ });
182
+
183
+ expect(defaultAllowed).toBe(true);
184
+ expect(
185
+ screen.queryByRole("dialog", { name: "Products menu" }),
186
+ ).not.toBeInTheDocument();
187
+ });
188
+
189
+ test("the enabled products shortcut ignores extra modifiers and a chord claimed by an earlier handler", () => {
190
+ render(<Navbar items={[homeItem]} moreMenuItems={moreMenuItems} />);
191
+
192
+ fireEvent.keyDown(document, { key: "k", metaKey: true, shiftKey: true });
193
+ fireEvent.keyDown(document, { key: "k", ctrlKey: true, altKey: true });
194
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
195
+
196
+ const claimChord: (event: KeyboardEvent) => void = (
197
+ event: KeyboardEvent,
198
+ ): void => {
199
+ event.preventDefault();
200
+ };
201
+ window.addEventListener("keydown", claimChord, { capture: true });
202
+
203
+ try {
204
+ pressCommandK();
205
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
206
+ } finally {
207
+ window.removeEventListener("keydown", claimChord, { capture: true });
208
+ }
209
+
210
+ pressCommandK();
211
+ expect(screen.getByRole("dialog", { name: "Products menu" })).toBeVisible();
212
+ });
213
+
214
+ test("a NavBar without a products menu leaves Cmd+K untouched", () => {
215
+ render(<Navbar items={[homeItem]} />);
216
+
217
+ const defaultAllowed: boolean = fireEvent.keyDown(document, {
218
+ key: "k",
219
+ metaKey: true,
220
+ });
221
+
222
+ expect(defaultAllowed).toBe(true);
223
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
224
+ });
225
+
226
+ test("the close-only listener dismisses Products even when the shortcut owner ran first", () => {
227
+ render(
228
+ <Navbar
229
+ items={[homeItem]}
230
+ moreMenuItems={moreMenuItems}
231
+ disableCommandKShortcut={true}
232
+ />,
233
+ );
234
+ fireEvent.click(screen.getByText("Products"));
235
+
236
+ const claimChord: (event: KeyboardEvent) => void = (
237
+ event: KeyboardEvent,
238
+ ): void => {
239
+ event.preventDefault();
240
+ };
241
+ window.addEventListener("keydown", claimChord, { capture: true });
242
+
243
+ try {
244
+ pressCommandK();
245
+ expect(
246
+ screen.queryByRole("dialog", { name: "Products menu" }),
247
+ ).not.toBeInTheDocument();
248
+ } finally {
249
+ window.removeEventListener("keydown", claimChord, { capture: true });
250
+ }
156
251
  });
157
252
 
158
253
  test("with disableCommandKShortcut, Cmd+K's default is not prevented (the palette may claim it)", () => {
@@ -166,8 +261,8 @@ describe("NavBar Cmd/Ctrl+K gating", () => {
166
261
 
167
262
  /*
168
263
  * fireEvent returns false when some handler called preventDefault. With
169
- * the NavBar listener unregistered, nothing here may swallow the chord —
170
- * the whole point is leaving it free for the command palette's handler.
264
+ * Products closed, no close-only listener is needed and nothing here may
265
+ * swallow the chord the palette is free to claim it.
171
266
  */
172
267
  const defaultAllowed: boolean = fireEvent.keyDown(document, {
173
268
  key: "k",
@@ -135,6 +135,36 @@ describe("Side Menu Item", () => {
135
135
  expect(mainLink).toHaveClass(highlightClassList);
136
136
  });
137
137
 
138
+ it("Should use activeRoute for highlighting without changing the link target", () => {
139
+ const linkTarget: Route = new Route("/network/map?site=");
140
+ const activeRoute: Route = new Route("/network/map");
141
+
142
+ Navigation.default.isOnThisPage = getJestMockFunction().mockImplementation(
143
+ (route: Route): boolean => {
144
+ return route === activeRoute;
145
+ },
146
+ );
147
+
148
+ render(
149
+ <SideMenuItem
150
+ link={{ title: "Network Map", to: linkTarget }}
151
+ activeRoute={activeRoute}
152
+ />,
153
+ );
154
+
155
+ const mainLink: HTMLAnchorElement = screen
156
+ .getByText("Network Map")
157
+ .closest("a") as HTMLAnchorElement;
158
+
159
+ expect(Navigation.default.isOnThisPage).toHaveBeenCalledWith(activeRoute);
160
+ expect(mainLink).toHaveClass(highlightClassList);
161
+ expect(mainLink).toHaveAttribute("href", linkTarget.toString());
162
+
163
+ fireEvent.click(mainLink);
164
+
165
+ expect(Navigation.default.navigate).toHaveBeenCalledWith(linkTarget);
166
+ });
167
+
138
168
  it("Should highlights sub item link when on the same page", () => {
139
169
  const subLink: {
140
170
  title: string;
@@ -25,6 +25,7 @@ interface Row {
25
25
  hosts?: Array<{ name: string }> | undefined;
26
26
  services?: Array<{ name: string }> | undefined;
27
27
  monitors?: Array<{ name: string }> | undefined;
28
+ monitorTemplate?: { templateName?: string } | undefined;
28
29
  }
29
30
 
30
31
  /*
@@ -446,6 +447,35 @@ describe("TableColumnsToCsv", () => {
446
447
 
447
448
  expect(result).toEqual([]);
448
449
  });
450
+
451
+ test("keeps a column that is only hidden on mobile", () => {
452
+ /*
453
+ * hideOnMobile is a render-time decision taken by TableHeader / TableRow,
454
+ * and this utility never sees a viewport. The monitors list Template
455
+ * column sits at the intersection of the two different kinds of "hidden"
456
+ * a column can now have: a phone viewer who switched it on in Customize
457
+ * Columns gets it in their file, while a desktop viewer on the defaults
458
+ * does not - because the "which columns am I seeing" question is answered
459
+ * upstream in BaseModelTable, never here. Teaching the exporter about
460
+ * hideOnMobile would silently strip columns from the export of anyone on
461
+ * a narrow screen, and nobody would notice until a support ticket.
462
+ */
463
+ const result: Columns<Row> = TableColumnsToCsv.getExportableColumns([
464
+ { title: "Name", type: FieldType.Text, key: "name" },
465
+ {
466
+ title: "Template",
467
+ type: FieldType.Entity,
468
+ key: "monitorTemplate",
469
+ hideOnMobile: true,
470
+ },
471
+ ]);
472
+
473
+ expect(
474
+ result.map((c: Column<Row>) => {
475
+ return c.title;
476
+ }),
477
+ ).toEqual(["Name", "Template"]);
478
+ });
449
479
  });
450
480
 
451
481
  describe("getExportKeys", () => {
@@ -0,0 +1,187 @@
1
+ import "@testing-library/jest-dom";
2
+ import { afterEach, describe, expect, test } from "@jest/globals";
3
+ import { act, cleanup, render, screen } from "@testing-library/react";
4
+ import React, { FunctionComponent, ReactElement } from "react";
5
+ import useDashboardTimeRangeZoom, {
6
+ DashboardTimeRangeZoom,
7
+ } from "../../../UI/Utils/UseDashboardTimeRangeZoom";
8
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
9
+ import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
10
+ import TimeRange from "../../../Types/Time/TimeRange";
11
+
12
+ /*
13
+ * The dashboard shells hand this hook's three callbacks straight into a
14
+ * canvas full of React.memo'd widgets whose comparators deliberately do NOT
15
+ * compare function identity. So identity stability is not a nicety here —
16
+ * a callback that changes reference every render would be blocked from
17
+ * reaching the widget, leaving it holding a closure over a stale range, and
18
+ * the SECOND zoom of a session would silently do nothing.
19
+ *
20
+ * These tests read the hook through a real component lifecycle, and pin the
21
+ * identity contract explicitly.
22
+ */
23
+
24
+ const ROLLING_HOUR: RangeStartAndEndDateTime = {
25
+ range: TimeRange.PAST_ONE_HOUR,
26
+ };
27
+
28
+ const PINNED_DAY: RangeStartAndEndDateTime = {
29
+ range: TimeRange.CUSTOM,
30
+ startAndEndDate: new InBetween<Date>(
31
+ new Date("2026-08-20T00:00:00.000Z"),
32
+ new Date("2026-08-21T00:00:00.000Z"),
33
+ ),
34
+ };
35
+
36
+ const DRAG_START: Date = new Date("2026-08-20T09:20:00.000Z");
37
+ const DRAG_END: Date = new Date("2026-08-20T09:35:00.000Z");
38
+
39
+ let latest: DashboardTimeRangeZoom | null = null;
40
+ let renderCount: number = 0;
41
+ const seenCallbacks: {
42
+ set: Array<unknown>;
43
+ zoom: Array<unknown>;
44
+ reset: Array<unknown>;
45
+ } = { set: [], zoom: [], reset: [] };
46
+
47
+ const ZoomProbe: FunctionComponent = (): ReactElement => {
48
+ const zoom: DashboardTimeRangeZoom = useDashboardTimeRangeZoom(ROLLING_HOUR);
49
+
50
+ latest = zoom;
51
+ renderCount++;
52
+ seenCallbacks.set.push(zoom.setStartAndEndDate);
53
+ seenCallbacks.zoom.push(zoom.zoomToTimeRange);
54
+ seenCallbacks.reset.push(zoom.resetZoom);
55
+
56
+ return (
57
+ <div>
58
+ <span data-testid="range">{zoom.startAndEndDate.range}</span>
59
+ <span data-testid="start">
60
+ {zoom.startAndEndDate.startAndEndDate?.startValue?.toISOString() || ""}
61
+ </span>
62
+ <span data-testid="end">
63
+ {zoom.startAndEndDate.startAndEndDate?.endValue?.toISOString() || ""}
64
+ </span>
65
+ <span data-testid="is-zoomed">{String(zoom.isZoomed)}</span>
66
+ </div>
67
+ );
68
+ };
69
+
70
+ function read(testId: string): string {
71
+ return screen.getByTestId(testId).textContent || "";
72
+ }
73
+
74
+ function zoomApi(): DashboardTimeRangeZoom {
75
+ if (!latest) {
76
+ throw new Error("probe has not rendered");
77
+ }
78
+ return latest;
79
+ }
80
+
81
+ function renderProbe(): void {
82
+ latest = null;
83
+ renderCount = 0;
84
+ seenCallbacks.set = [];
85
+ seenCallbacks.zoom = [];
86
+ seenCallbacks.reset = [];
87
+ render(<ZoomProbe />);
88
+ }
89
+
90
+ afterEach(() => {
91
+ cleanup();
92
+ });
93
+
94
+ describe("useDashboardTimeRangeZoom", () => {
95
+ test("starts on the initial range with nothing to reset", () => {
96
+ renderProbe();
97
+
98
+ expect(read("range")).toBe(TimeRange.PAST_ONE_HOUR);
99
+ expect(read("is-zoomed")).toBe("false");
100
+ });
101
+
102
+ test("a panel drag pins the board to the selected window", () => {
103
+ renderProbe();
104
+
105
+ act(() => {
106
+ zoomApi().zoomToTimeRange(DRAG_START, DRAG_END);
107
+ });
108
+
109
+ expect(read("range")).toBe(TimeRange.CUSTOM);
110
+ expect(read("start")).toBe(DRAG_START.toISOString());
111
+ expect(read("end")).toBe(DRAG_END.toISOString());
112
+ expect(read("is-zoomed")).toBe("true");
113
+ });
114
+
115
+ test("a double-click reset puts the board back on the pre-zoom range", () => {
116
+ renderProbe();
117
+
118
+ act(() => {
119
+ zoomApi().zoomToTimeRange(DRAG_START, DRAG_END);
120
+ });
121
+ act(() => {
122
+ zoomApi().resetZoom();
123
+ });
124
+
125
+ expect(read("range")).toBe(TimeRange.PAST_ONE_HOUR);
126
+ expect(read("is-zoomed")).toBe("false");
127
+ });
128
+
129
+ test("resetting an unzoomed board does not even re-render", () => {
130
+ renderProbe();
131
+ const rendersBefore: number = renderCount;
132
+
133
+ act(() => {
134
+ zoomApi().resetZoom();
135
+ });
136
+
137
+ /*
138
+ * The util returns the same state object, so React bails out of the
139
+ * update — a stray double-click on an unzoomed dashboard costs nothing
140
+ * and cannot make every widget refetch.
141
+ */
142
+ expect(renderCount).toBe(rendersBefore);
143
+ expect(read("range")).toBe(TimeRange.PAST_ONE_HOUR);
144
+ });
145
+
146
+ test("an explicit picker change replaces the range and retires the reset", () => {
147
+ renderProbe();
148
+
149
+ act(() => {
150
+ zoomApi().zoomToTimeRange(DRAG_START, DRAG_END);
151
+ });
152
+ expect(read("is-zoomed")).toBe("true");
153
+
154
+ act(() => {
155
+ zoomApi().setStartAndEndDate(PINNED_DAY);
156
+ });
157
+
158
+ expect(read("start")).toBe("2026-08-20T00:00:00.000Z");
159
+ expect(read("is-zoomed")).toBe("false");
160
+ });
161
+
162
+ test("the three callbacks keep their identity across every re-render", () => {
163
+ renderProbe();
164
+
165
+ act(() => {
166
+ zoomApi().zoomToTimeRange(DRAG_START, DRAG_END);
167
+ });
168
+ act(() => {
169
+ zoomApi().setStartAndEndDate(PINNED_DAY);
170
+ });
171
+ act(() => {
172
+ zoomApi().zoomToTimeRange(DRAG_START, DRAG_END);
173
+ });
174
+ act(() => {
175
+ zoomApi().resetZoom();
176
+ });
177
+
178
+ expect(renderCount).toBeGreaterThan(1);
179
+ for (const captured of [
180
+ seenCallbacks.set,
181
+ seenCallbacks.zoom,
182
+ seenCallbacks.reset,
183
+ ]) {
184
+ expect(new Set(captured).size).toBe(1);
185
+ }
186
+ });
187
+ });