@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
@@ -0,0 +1,302 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import {
3
+ ComponentArgument,
4
+ ComponentArgumentSection,
5
+ ComponentInputType,
6
+ EntityFilterModelType,
7
+ } from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
8
+ import DashboardBaseComponent from "../../../Types/Dashboard/DashboardComponents/DashboardBaseComponent";
9
+ import {
10
+ CephDisplaySection,
11
+ CephFiltersSection,
12
+ getCephCommonArguments,
13
+ } from "../../../Utils/Dashboard/Components/DashboardCephResourceListShared";
14
+ import {
15
+ DockerSwarmDisplaySection,
16
+ DockerSwarmFiltersSection,
17
+ getDockerSwarmCommonArguments,
18
+ } from "../../../Utils/Dashboard/Components/DashboardDockerSwarmResourceListShared";
19
+ import {
20
+ ProxmoxDisplaySection,
21
+ ProxmoxFiltersSection,
22
+ getProxmoxCommonArguments,
23
+ } from "../../../Utils/Dashboard/Components/DashboardProxmoxResourceListShared";
24
+ import {
25
+ KubernetesDisplaySection,
26
+ KubernetesFiltersSection,
27
+ getKubernetesCommonArguments,
28
+ } from "../../../Utils/Dashboard/Components/DashboardKubernetesResourceListShared";
29
+
30
+ /*
31
+ * The per-provider argument builders behind the infrastructure list widgets.
32
+ *
33
+ * Ceph, Docker Swarm and Proxmox are the same widget four times over — the
34
+ * files are identical but for the provider's name — and Kubernetes is that
35
+ * shape plus a namespace filter. They were written by copying one another,
36
+ * which is fine, and it is also exactly why they drift: a fix or an addition
37
+ * made to one has no reason to fail anywhere else, so the widgets slowly stop
38
+ * behaving alike and nobody finds out until an operator asks why the Proxmox
39
+ * list has no honeycomb view.
40
+ *
41
+ * The entity filter type is the sharper risk. It is what a cluster filter
42
+ * resolves ids against, and a copy-paste that left the wrong provider's model
43
+ * behind would offer one product's clusters as the filter for another's — no
44
+ * type error, no crash, just a picker that lists the wrong things and a widget
45
+ * that silently matches nothing.
46
+ */
47
+
48
+ type AnyArgument = ComponentArgument<DashboardBaseComponent>;
49
+
50
+ interface ProviderWidget {
51
+ provider: string;
52
+ args: Array<AnyArgument>;
53
+ displaySection: ComponentArgumentSection;
54
+ filtersSection: ComponentArgumentSection;
55
+ clusterArgumentId: string;
56
+ clusterEntityType: EntityFilterModelType;
57
+ }
58
+
59
+ /*
60
+ * Kubernetes is asked for WITHOUT its namespace filter here, which is the
61
+ * arrangement in which it is meant to be the same widget as the other three.
62
+ * Its extra argument is covered separately below.
63
+ */
64
+ const WIDGETS: Array<ProviderWidget> = [
65
+ {
66
+ provider: "Ceph",
67
+ args: getCephCommonArguments<DashboardBaseComponent>(),
68
+ displaySection: CephDisplaySection,
69
+ filtersSection: CephFiltersSection,
70
+ clusterArgumentId: "cephClusterIds",
71
+ clusterEntityType: EntityFilterModelType.CephCluster,
72
+ },
73
+ {
74
+ provider: "Docker Swarm",
75
+ args: getDockerSwarmCommonArguments<DashboardBaseComponent>(),
76
+ displaySection: DockerSwarmDisplaySection,
77
+ filtersSection: DockerSwarmFiltersSection,
78
+ clusterArgumentId: "dockerSwarmClusterIds",
79
+ clusterEntityType: EntityFilterModelType.DockerSwarmCluster,
80
+ },
81
+ {
82
+ provider: "Proxmox",
83
+ args: getProxmoxCommonArguments<DashboardBaseComponent>(),
84
+ displaySection: ProxmoxDisplaySection,
85
+ filtersSection: ProxmoxFiltersSection,
86
+ clusterArgumentId: "proxmoxClusterIds",
87
+ clusterEntityType: EntityFilterModelType.ProxmoxCluster,
88
+ },
89
+ {
90
+ provider: "Kubernetes",
91
+ args: getKubernetesCommonArguments<DashboardBaseComponent>({
92
+ includeNamespaceFilter: false,
93
+ }),
94
+ displaySection: KubernetesDisplaySection,
95
+ filtersSection: KubernetesFiltersSection,
96
+ clusterArgumentId: "kubernetesClusterIds",
97
+ clusterEntityType: EntityFilterModelType.KubernetesCluster,
98
+ },
99
+ ];
100
+
101
+ function argumentById(
102
+ args: Array<AnyArgument>,
103
+ id: string,
104
+ ): AnyArgument | undefined {
105
+ return args.find((argument: AnyArgument): boolean => {
106
+ return (argument.id as unknown as string) === id;
107
+ });
108
+ }
109
+
110
+ describe("infrastructure list widget arguments", () => {
111
+ describe.each(WIDGETS)(
112
+ "$provider",
113
+ ({
114
+ args,
115
+ displaySection,
116
+ filtersSection,
117
+ clusterArgumentId,
118
+ clusterEntityType,
119
+ }: ProviderWidget) => {
120
+ /*
121
+ * These three are what the widget reads at render time. A dropped one
122
+ * does not fail to compile — the widget just stops offering the
123
+ * control, and the argument silently reads as undefined.
124
+ */
125
+ test("offers the title, row cap and view mode every one of these widgets has", () => {
126
+ for (const id of ["title", "maxRows", "viewMode"]) {
127
+ expect(argumentById(args, id)).toBeDefined();
128
+ }
129
+ });
130
+
131
+ test("view mode is a dropdown offering exactly list and honeycomb", () => {
132
+ const viewMode: AnyArgument = argumentById(args, "viewMode")!;
133
+
134
+ expect(viewMode.type).toBe(ComponentInputType.Dropdown);
135
+ expect(
136
+ (viewMode.dropdownOptions || []).map(
137
+ (option: { value: string | number | boolean }): unknown => {
138
+ return option.value;
139
+ },
140
+ ),
141
+ ).toEqual(["list", "honeycomb"]);
142
+ });
143
+
144
+ /*
145
+ * The one that cannot be caught by reading the file: a cluster filter
146
+ * pointed at another provider's model resolves ids against the wrong
147
+ * table, so the picker lists the wrong clusters and the widget matches
148
+ * nothing.
149
+ */
150
+ test("the cluster filter resolves against its own provider's clusters", () => {
151
+ const clusterFilter: AnyArgument | undefined = argumentById(
152
+ args,
153
+ clusterArgumentId,
154
+ );
155
+
156
+ expect(clusterFilter).toBeDefined();
157
+ expect(clusterFilter!.type).toBe(
158
+ ComponentInputType.EntityMultiSelectDropdown,
159
+ );
160
+ expect(clusterFilter!.entityFilterModelType).toBe(clusterEntityType);
161
+ });
162
+
163
+ /*
164
+ * Display holds what the widget looks like, Filters holds what it
165
+ * shows. An argument filed under the wrong one is not a crash, it just
166
+ * puts the control in a panel no operator thinks to open — and Filters
167
+ * starts collapsed, so a misfiled display control is invisible.
168
+ */
169
+ test("display controls and filters are filed in the right panels", () => {
170
+ for (const id of ["title", "maxRows", "viewMode"]) {
171
+ expect(argumentById(args, id)!.section).toBe(displaySection);
172
+ }
173
+
174
+ expect(argumentById(args, clusterArgumentId)!.section).toBe(
175
+ filtersSection,
176
+ );
177
+ });
178
+
179
+ test("Display is ordered ahead of Filters, which starts collapsed", () => {
180
+ expect(displaySection.order).toBeLessThan(filtersSection.order!);
181
+ expect(filtersSection.defaultCollapsed).toBe(true);
182
+ });
183
+
184
+ /*
185
+ * Ids are the keys the saved widget config is written under. A repeat
186
+ * would have two controls writing over each other.
187
+ */
188
+ test("no argument id is used twice", () => {
189
+ const ids: Array<string> = args.map((argument: AnyArgument): string => {
190
+ return argument.id as unknown as string;
191
+ });
192
+
193
+ expect(new Set(ids).size).toBe(ids.length);
194
+ });
195
+
196
+ /*
197
+ * Every one of these is optional: a required argument with no value
198
+ * blocks the widget from rendering at all, and an empty list widget
199
+ * showing everything is the intended default.
200
+ */
201
+ test("nothing is required, so a freshly added widget renders", () => {
202
+ for (const argument of args) {
203
+ expect(argument.required).toBe(false);
204
+ }
205
+ });
206
+ },
207
+ );
208
+
209
+ /*
210
+ * The point of the whole suite: the four are meant to be the same widget.
211
+ * Comparing them to each other catches a change made to one and not the
212
+ * rest, which is the way these files actually go wrong.
213
+ */
214
+ describe("the providers stay the same widget as each other", () => {
215
+ test("they expose the same arguments, in the same order", () => {
216
+ const shapes: Array<Array<string>> = WIDGETS.map(
217
+ (widget: ProviderWidget): Array<string> => {
218
+ return widget.args.map((argument: AnyArgument): string => {
219
+ const id: string = argument.id as unknown as string;
220
+
221
+ // The cluster filter is the one id that is legitimately per-provider.
222
+ return id === widget.clusterArgumentId
223
+ ? `clusterIds:${argument.type}`
224
+ : `${id}:${argument.type}`;
225
+ });
226
+ },
227
+ );
228
+
229
+ for (const shape of shapes) {
230
+ expect(shape).toEqual(shapes[0]);
231
+ }
232
+ });
233
+
234
+ test("each provider filters on its own cluster model, and no two share one", () => {
235
+ const entityTypes: Array<EntityFilterModelType> = WIDGETS.map(
236
+ (widget: ProviderWidget): EntityFilterModelType => {
237
+ return argumentById(widget.args, widget.clusterArgumentId)!
238
+ .entityFilterModelType!;
239
+ },
240
+ );
241
+
242
+ expect(new Set(entityTypes).size).toBe(WIDGETS.length);
243
+ });
244
+ });
245
+
246
+ /*
247
+ * Kubernetes is the one that differs, and it differs on request rather than
248
+ * always — the namespace filter is only meaningful for widgets listing
249
+ * namespaced objects.
250
+ */
251
+ describe("the Kubernetes namespace filter is opt-in", () => {
252
+ test("it is absent unless asked for", () => {
253
+ expect(
254
+ argumentById(
255
+ getKubernetesCommonArguments<DashboardBaseComponent>({
256
+ includeNamespaceFilter: false,
257
+ }),
258
+ "namespaces",
259
+ ),
260
+ ).toBeUndefined();
261
+ });
262
+
263
+ test("asking for it adds it to the filters panel, still optional", () => {
264
+ const namespaces: AnyArgument | undefined = argumentById(
265
+ getKubernetesCommonArguments<DashboardBaseComponent>({
266
+ includeNamespaceFilter: true,
267
+ }),
268
+ "namespaces",
269
+ );
270
+
271
+ expect(namespaces).toBeDefined();
272
+ expect(namespaces!.required).toBe(false);
273
+ expect(namespaces!.section).toBe(KubernetesFiltersSection);
274
+ });
275
+
276
+ /*
277
+ * Asking for the namespace filter must not disturb anything else — it is
278
+ * an addition, not a different widget.
279
+ */
280
+ test("it changes nothing else about the widget", () => {
281
+ const without: Array<string> =
282
+ getKubernetesCommonArguments<DashboardBaseComponent>({
283
+ includeNamespaceFilter: false,
284
+ }).map((argument: AnyArgument): string => {
285
+ return argument.id as unknown as string;
286
+ });
287
+
288
+ const with_: Array<string> =
289
+ getKubernetesCommonArguments<DashboardBaseComponent>({
290
+ includeNamespaceFilter: true,
291
+ })
292
+ .map((argument: AnyArgument): string => {
293
+ return argument.id as unknown as string;
294
+ })
295
+ .filter((id: string): boolean => {
296
+ return id !== "namespaces";
297
+ });
298
+
299
+ expect(with_).toEqual(without);
300
+ });
301
+ });
302
+ });
@@ -0,0 +1,242 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import DashboardTimeRangeZoomUtil, {
3
+ DashboardTimeRangeZoomState,
4
+ } from "../../../Utils/Dashboard/DashboardTimeRangeZoom";
5
+ import InBetween from "../../../Types/BaseDatabase/InBetween";
6
+ import RangeStartAndEndDateTime from "../../../Types/Time/RangeStartAndEndDateTime";
7
+ import TimeRange from "../../../Types/Time/TimeRange";
8
+
9
+ /*
10
+ * The state machine behind dashboard-wide drag-to-zoom. Its whole job is to
11
+ * make the gesture REVERSIBLE: a drag on any time-series panel retimes the
12
+ * whole board, and one double-click has to land back exactly where the
13
+ * investigation started — not one drag back, and not on "now".
14
+ *
15
+ * `baseline === null` is the contract the UI reads for "nothing to reset",
16
+ * so the no-op cases below are as load-bearing as the happy path: a stray
17
+ * double-click on an unzoomed board must not retime it, and must not even
18
+ * hand React a new object to re-render every widget over.
19
+ */
20
+
21
+ const ROLLING_HOUR: RangeStartAndEndDateTime = {
22
+ range: TimeRange.PAST_ONE_HOUR,
23
+ };
24
+
25
+ const PINNED_DAY: RangeStartAndEndDateTime = {
26
+ range: TimeRange.CUSTOM,
27
+ startAndEndDate: new InBetween<Date>(
28
+ new Date("2026-08-20T00:00:00.000Z"),
29
+ new Date("2026-08-21T00:00:00.000Z"),
30
+ ),
31
+ };
32
+
33
+ const DRAG_START: Date = new Date("2026-08-20T09:20:00.000Z");
34
+ const DRAG_END: Date = new Date("2026-08-20T09:35:00.000Z");
35
+ const NARROWER_START: Date = new Date("2026-08-20T09:25:00.000Z");
36
+ const NARROWER_END: Date = new Date("2026-08-20T09:27:00.000Z");
37
+
38
+ describe("DashboardTimeRangeZoomUtil", () => {
39
+ describe("getInitialState", () => {
40
+ test("starts on the given range with nothing to reset", () => {
41
+ const state: DashboardTimeRangeZoomState =
42
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR);
43
+
44
+ expect(state.current).toBe(ROLLING_HOUR);
45
+ expect(state.baseline).toBeNull();
46
+ expect(DashboardTimeRangeZoomUtil.isZoomed(state)).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe("zoomToWindow", () => {
51
+ test("pins the board to the dragged window and remembers the way back", () => {
52
+ const zoomed: DashboardTimeRangeZoomState =
53
+ DashboardTimeRangeZoomUtil.zoomToWindow(
54
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
55
+ DRAG_START,
56
+ DRAG_END,
57
+ );
58
+
59
+ expect(zoomed.current.range).toBe(TimeRange.CUSTOM);
60
+ expect(zoomed.current.startAndEndDate?.startValue).toEqual(DRAG_START);
61
+ expect(zoomed.current.startAndEndDate?.endValue).toEqual(DRAG_END);
62
+ expect(zoomed.baseline).toBe(ROLLING_HOUR);
63
+ expect(DashboardTimeRangeZoomUtil.isZoomed(zoomed)).toBe(true);
64
+ });
65
+
66
+ test("a right-to-left drag is the same window as left-to-right", () => {
67
+ const forward: DashboardTimeRangeZoomState =
68
+ DashboardTimeRangeZoomUtil.zoomToWindow(
69
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
70
+ DRAG_START,
71
+ DRAG_END,
72
+ );
73
+ const backward: DashboardTimeRangeZoomState =
74
+ DashboardTimeRangeZoomUtil.zoomToWindow(
75
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
76
+ DRAG_END,
77
+ DRAG_START,
78
+ );
79
+
80
+ expect(backward.current.startAndEndDate?.startValue).toEqual(
81
+ forward.current.startAndEndDate?.startValue,
82
+ );
83
+ expect(backward.current.startAndEndDate?.endValue).toEqual(
84
+ forward.current.startAndEndDate?.endValue,
85
+ );
86
+ });
87
+
88
+ test("zooming again keeps the ORIGINAL baseline, so one reset gets all the way out", () => {
89
+ const once: DashboardTimeRangeZoomState =
90
+ DashboardTimeRangeZoomUtil.zoomToWindow(
91
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
92
+ DRAG_START,
93
+ DRAG_END,
94
+ );
95
+ const twice: DashboardTimeRangeZoomState =
96
+ DashboardTimeRangeZoomUtil.zoomToWindow(
97
+ once,
98
+ NARROWER_START,
99
+ NARROWER_END,
100
+ );
101
+
102
+ expect(twice.current.startAndEndDate?.startValue).toEqual(NARROWER_START);
103
+ // NOT `once.current` — a reader should not have to climb out a drag at a time.
104
+ expect(twice.baseline).toBe(ROLLING_HOUR);
105
+
106
+ const reset: DashboardTimeRangeZoomState =
107
+ DashboardTimeRangeZoomUtil.resetZoom(twice);
108
+ expect(reset.current).toBe(ROLLING_HOUR);
109
+ expect(reset.baseline).toBeNull();
110
+ });
111
+
112
+ test("a zero-width selection changes nothing at all", () => {
113
+ const state: DashboardTimeRangeZoomState =
114
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR);
115
+
116
+ /*
117
+ * The chart library can hand back start === end when a drag never
118
+ * leaves its starting bucket. Same reference back, so no widget
119
+ * re-renders and no refetch is spent.
120
+ */
121
+ expect(
122
+ DashboardTimeRangeZoomUtil.zoomToWindow(state, DRAG_START, DRAG_START),
123
+ ).toBe(state);
124
+ });
125
+
126
+ test("an invalid date changes nothing at all", () => {
127
+ const state: DashboardTimeRangeZoomState =
128
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR);
129
+
130
+ expect(
131
+ DashboardTimeRangeZoomUtil.zoomToWindow(
132
+ state,
133
+ new Date("not-a-date"),
134
+ DRAG_END,
135
+ ),
136
+ ).toBe(state);
137
+ expect(
138
+ DashboardTimeRangeZoomUtil.zoomToWindow(
139
+ state,
140
+ DRAG_START,
141
+ new Date("not-a-date"),
142
+ ),
143
+ ).toBe(state);
144
+ expect(
145
+ DashboardTimeRangeZoomUtil.zoomToWindow(
146
+ state,
147
+ null as unknown as Date,
148
+ DRAG_END,
149
+ ),
150
+ ).toBe(state);
151
+ });
152
+
153
+ test("the stored window is a copy — mutating the dragged dates cannot move the board", () => {
154
+ const dragStart: Date = new Date(DRAG_START.getTime());
155
+ const zoomed: DashboardTimeRangeZoomState =
156
+ DashboardTimeRangeZoomUtil.zoomToWindow(
157
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
158
+ dragStart,
159
+ DRAG_END,
160
+ );
161
+
162
+ dragStart.setFullYear(1999);
163
+
164
+ expect(zoomed.current.startAndEndDate?.startValue).toEqual(DRAG_START);
165
+ });
166
+ });
167
+
168
+ describe("resetZoom", () => {
169
+ test("restores the range the board had before the first drag", () => {
170
+ const zoomed: DashboardTimeRangeZoomState =
171
+ DashboardTimeRangeZoomUtil.zoomToWindow(
172
+ DashboardTimeRangeZoomUtil.getInitialState(PINNED_DAY),
173
+ DRAG_START,
174
+ DRAG_END,
175
+ );
176
+
177
+ const reset: DashboardTimeRangeZoomState =
178
+ DashboardTimeRangeZoomUtil.resetZoom(zoomed);
179
+
180
+ expect(reset.current).toBe(PINNED_DAY);
181
+ expect(reset.baseline).toBeNull();
182
+ expect(DashboardTimeRangeZoomUtil.isZoomed(reset)).toBe(false);
183
+ });
184
+
185
+ test("is a true no-op when nothing is zoomed", () => {
186
+ const state: DashboardTimeRangeZoomState =
187
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR);
188
+
189
+ // Same reference: a stray double-click cannot even cost a render.
190
+ expect(DashboardTimeRangeZoomUtil.resetZoom(state)).toBe(state);
191
+ });
192
+
193
+ test("a second reset does not keep unwinding", () => {
194
+ const zoomed: DashboardTimeRangeZoomState =
195
+ DashboardTimeRangeZoomUtil.zoomToWindow(
196
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
197
+ DRAG_START,
198
+ DRAG_END,
199
+ );
200
+ const once: DashboardTimeRangeZoomState =
201
+ DashboardTimeRangeZoomUtil.resetZoom(zoomed);
202
+
203
+ expect(DashboardTimeRangeZoomUtil.resetZoom(once)).toBe(once);
204
+ });
205
+ });
206
+
207
+ describe("selectRange", () => {
208
+ test("an explicit pick becomes the new baseline and retires the reset", () => {
209
+ const zoomed: DashboardTimeRangeZoomState =
210
+ DashboardTimeRangeZoomUtil.zoomToWindow(
211
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
212
+ DRAG_START,
213
+ DRAG_END,
214
+ );
215
+
216
+ const picked: DashboardTimeRangeZoomState =
217
+ DashboardTimeRangeZoomUtil.selectRange(zoomed, PINNED_DAY);
218
+
219
+ expect(picked.current).toBe(PINNED_DAY);
220
+ /*
221
+ * The picker is not an undo stack: offering to jump back to a range
222
+ * the user has deliberately left is worse than offering nothing.
223
+ */
224
+ expect(picked.baseline).toBeNull();
225
+ expect(DashboardTimeRangeZoomUtil.isZoomed(picked)).toBe(false);
226
+ });
227
+
228
+ test("picking then zooming makes the PICKED range the way back", () => {
229
+ const picked: DashboardTimeRangeZoomState =
230
+ DashboardTimeRangeZoomUtil.selectRange(
231
+ DashboardTimeRangeZoomUtil.getInitialState(ROLLING_HOUR),
232
+ PINNED_DAY,
233
+ );
234
+ const zoomed: DashboardTimeRangeZoomState =
235
+ DashboardTimeRangeZoomUtil.zoomToWindow(picked, DRAG_START, DRAG_END);
236
+
237
+ expect(DashboardTimeRangeZoomUtil.resetZoom(zoomed).current).toBe(
238
+ PINNED_DAY,
239
+ );
240
+ });
241
+ });
242
+ });